Add image upload
This commit is contained in:
parent
997f374fe4
commit
67f24adfdf
24 changed files with 614 additions and 92 deletions
10
build.gradle
10
build.gradle
|
@ -21,17 +21,19 @@ group "de.kif"
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
jcenter()
|
jcenter()
|
||||||
maven { url 'https://jitpack.io' }
|
maven { url 'https://jitpack.io' }
|
||||||
maven { url "https://dl.bintray.com/kotlin/ktor" }
|
maven { url "https://dl.bintray.com/kotlin/ktor" }
|
||||||
maven { url "https://dl.bintray.com/jetbrains/markdown" }
|
maven { url "https://dl.bintray.com/jetbrains/markdown" }
|
||||||
maven { url "https://kotlin.bintray.com/kotlinx" }
|
maven { url "https://kotlin.bintray.com/kotlinx" }
|
||||||
maven { url "https://kotlin.bintray.com/kotlin-js-wrappers" }
|
maven { url "https://kotlin.bintray.com/kotlin-js-wrappers" }
|
||||||
mavenCentral()
|
maven { url "https://dl.bintray.com/soywiz/soywiz" }
|
||||||
}
|
}
|
||||||
def ktor_version = '1.1.5'
|
def ktor_version = '1.1.5'
|
||||||
def serialization_version = '0.11.0'
|
def serialization_version = '0.11.0'
|
||||||
def observable_version = '0.9.3'
|
def observable_version = '0.9.3'
|
||||||
|
def klockVersion = "1.4.0"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm() {
|
jvm() {
|
||||||
|
@ -58,6 +60,8 @@ kotlin {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin('stdlib-common')
|
implementation kotlin('stdlib-common')
|
||||||
implementation "de.westermann:KObserve-metadata:$observable_version"
|
implementation "de.westermann:KObserve-metadata:$observable_version"
|
||||||
|
implementation "com.soywiz:klock-metadata:$klockVersion"
|
||||||
|
implementation "com.soywiz:klock-locale-metadata:$klockVersion"
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version"
|
||||||
}
|
}
|
||||||
|
@ -89,6 +93,8 @@ kotlin {
|
||||||
implementation 'org.mindrot:jbcrypt:0.4'
|
implementation 'org.mindrot:jbcrypt:0.4'
|
||||||
|
|
||||||
implementation "de.westermann:KObserve-jvm:$observable_version"
|
implementation "de.westermann:KObserve-jvm:$observable_version"
|
||||||
|
implementation "com.soywiz:klock-jvm:$klockVersion"
|
||||||
|
implementation "com.soywiz:klock-locale-jvm:$klockVersion"
|
||||||
|
|
||||||
implementation 'com.github.uchuhimo:konf:master-SNAPSHOT'
|
implementation 'com.github.uchuhimo:konf:master-SNAPSHOT'
|
||||||
implementation 'com.vladsch.flexmark:flexmark-all:0.42.10'
|
implementation 'com.vladsch.flexmark:flexmark-all:0.42.10'
|
||||||
|
@ -110,6 +116,8 @@ kotlin {
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serialization_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serialization_version"
|
||||||
|
|
||||||
implementation "de.westermann:KObserve-js:$observable_version"
|
implementation "de.westermann:KObserve-js:$observable_version"
|
||||||
|
implementation "com.soywiz:klock-js:$klockVersion"
|
||||||
|
implementation "com.soywiz:klock-locale-js:$klockVersion"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jsTest {
|
jsTest {
|
||||||
|
|
11
src/commonMain/kotlin/de/kif/common/DateFormat.kt
Normal file
11
src/commonMain/kotlin/de/kif/common/DateFormat.kt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package de.kif.common
|
||||||
|
|
||||||
|
import com.soywiz.klock.DateFormat
|
||||||
|
import com.soywiz.klock.KlockLocale
|
||||||
|
import com.soywiz.klock.format
|
||||||
|
import com.soywiz.klock.locale.german
|
||||||
|
|
||||||
|
fun formatDate(unix: Long) =
|
||||||
|
DateFormat("EEEE, d. MMMM y HH:mm")
|
||||||
|
.withLocale(KlockLocale.german)
|
||||||
|
.format(unix)
|
|
@ -5,4 +5,7 @@ import de.kif.common.SearchElement
|
||||||
interface Model {
|
interface Model {
|
||||||
val id : Long?
|
val id : Long?
|
||||||
fun createSearch(): SearchElement
|
fun createSearch(): SearchElement
|
||||||
|
|
||||||
|
val createdAt: Long
|
||||||
|
val updateAt: Long
|
||||||
}
|
}
|
|
@ -10,7 +10,10 @@ data class Post(
|
||||||
val name: String,
|
val name: String,
|
||||||
val content: String,
|
val content: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
val pinned: Boolean = false
|
val image: String?,
|
||||||
|
val pinned: Boolean,
|
||||||
|
override val createdAt: Long = 0,
|
||||||
|
override val updateAt: Long = 0
|
||||||
) : Model {
|
) : Model {
|
||||||
|
|
||||||
override fun createSearch() = SearchElement(
|
override fun createSearch() = SearchElement(
|
||||||
|
@ -22,6 +25,7 @@ data class Post(
|
||||||
companion object {
|
companion object {
|
||||||
private const val chars = "abcdefghijklmnopqrstuvwxyz"
|
private const val chars = "abcdefghijklmnopqrstuvwxyz"
|
||||||
private const val length = 32
|
private const val length = 32
|
||||||
|
|
||||||
fun generateUrl() = (0 until length).asSequence()
|
fun generateUrl() = (0 until length).asSequence()
|
||||||
.map { Random.nextInt(chars.length) }
|
.map { Random.nextInt(chars.length) }
|
||||||
.map { chars[it] }
|
.map { chars[it] }
|
||||||
|
|
|
@ -12,7 +12,9 @@ data class Room(
|
||||||
val internet: Boolean,
|
val internet: Boolean,
|
||||||
val whiteboard: Boolean,
|
val whiteboard: Boolean,
|
||||||
val blackboard: Boolean,
|
val blackboard: Boolean,
|
||||||
val accessible: Boolean
|
val accessible: Boolean,
|
||||||
|
override val createdAt: Long = 0,
|
||||||
|
override val updateAt: Long = 0
|
||||||
) : Model {
|
) : Model {
|
||||||
|
|
||||||
override fun createSearch() = SearchElement(
|
override fun createSearch() = SearchElement(
|
||||||
|
|
|
@ -9,7 +9,9 @@ data class Schedule(
|
||||||
val workGroup: WorkGroup,
|
val workGroup: WorkGroup,
|
||||||
val room: Room,
|
val room: Room,
|
||||||
val day: Int,
|
val day: Int,
|
||||||
val time: Int
|
val time: Int,
|
||||||
|
override val createdAt: Long = 0,
|
||||||
|
override val updateAt: Long = 0
|
||||||
) : Model {
|
) : Model {
|
||||||
|
|
||||||
override fun createSearch() = SearchElement(
|
override fun createSearch() = SearchElement(
|
||||||
|
|
|
@ -7,7 +7,9 @@ import kotlinx.serialization.Serializable
|
||||||
data class Track(
|
data class Track(
|
||||||
override val id: Long?,
|
override val id: Long?,
|
||||||
val name: String,
|
val name: String,
|
||||||
val color: Color
|
val color: Color,
|
||||||
|
override val createdAt: Long = 0,
|
||||||
|
override val updateAt: Long = 0
|
||||||
) : Model {
|
) : Model {
|
||||||
|
|
||||||
override fun createSearch() = SearchElement(
|
override fun createSearch() = SearchElement(
|
||||||
|
|
|
@ -8,7 +8,9 @@ data class User(
|
||||||
override val id: Long?,
|
override val id: Long?,
|
||||||
val username: String,
|
val username: String,
|
||||||
val password: String,
|
val password: String,
|
||||||
val permissions: Set<Permission>
|
val permissions: Set<Permission>,
|
||||||
|
override val createdAt: Long = 0,
|
||||||
|
override val updateAt: Long = 0
|
||||||
) : Model {
|
) : Model {
|
||||||
|
|
||||||
fun checkPermission(permission: Permission): Boolean {
|
fun checkPermission(permission: Permission): Boolean {
|
||||||
|
|
|
@ -17,7 +17,9 @@ data class WorkGroup(
|
||||||
val accessible: Boolean,
|
val accessible: Boolean,
|
||||||
val length: Int,
|
val length: Int,
|
||||||
val language: Language,
|
val language: Language,
|
||||||
val constraints: List<Constraint>
|
val constraints: List<Constraint>,
|
||||||
|
override val createdAt: Long = 0,
|
||||||
|
override val updateAt: Long = 0
|
||||||
) : Model {
|
) : Model {
|
||||||
|
|
||||||
override fun createSearch() = SearchElement(
|
override fun createSearch() = SearchElement(
|
||||||
|
|
|
@ -5,9 +5,13 @@ import de.kif.frontend.launch
|
||||||
import de.kif.frontend.repository.PostRepository
|
import de.kif.frontend.repository.PostRepository
|
||||||
import de.westermann.kobserve.event.subscribe
|
import de.westermann.kobserve.event.subscribe
|
||||||
import org.w3c.dom.HTMLElement
|
import org.w3c.dom.HTMLElement
|
||||||
|
import org.w3c.dom.HTMLInputElement
|
||||||
import org.w3c.dom.HTMLTextAreaElement
|
import org.w3c.dom.HTMLTextAreaElement
|
||||||
import org.w3c.dom.events.EventListener
|
import org.w3c.dom.events.EventListener
|
||||||
import org.w3c.dom.get
|
import org.w3c.dom.get
|
||||||
|
import org.w3c.files.File
|
||||||
|
import org.w3c.files.FileReader
|
||||||
|
import org.w3c.files.get
|
||||||
import kotlin.browser.document
|
import kotlin.browser.document
|
||||||
import kotlin.dom.clear
|
import kotlin.dom.clear
|
||||||
|
|
||||||
|
@ -50,6 +54,7 @@ fun initPosts() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initPostEdit() {
|
fun initPostEdit() {
|
||||||
|
// Content preview
|
||||||
val textArea = document.getElementById("content") as HTMLTextAreaElement
|
val textArea = document.getElementById("content") as HTMLTextAreaElement
|
||||||
val preview = document.getElementsByClassName("post-edit-right")[0] as HTMLElement
|
val preview = document.getElementsByClassName("post-edit-right")[0] as HTMLElement
|
||||||
|
|
||||||
|
@ -67,4 +72,45 @@ fun initPostEdit() {
|
||||||
launch {
|
launch {
|
||||||
preview.innerHTML = PostRepository.render(textArea.value)
|
preview.innerHTML = PostRepository.render(textArea.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Image preview
|
||||||
|
val imageView = document.getElementsByClassName("post-edit-image")[0] as HTMLElement
|
||||||
|
val uploadButton = document.getElementById("image") as HTMLInputElement
|
||||||
|
val deleteSwitch = document.getElementById("image-delete") as? HTMLInputElement
|
||||||
|
|
||||||
|
var file: File? = null
|
||||||
|
val original = imageView.style.backgroundImage
|
||||||
|
|
||||||
|
fun updateImage() {
|
||||||
|
val deleteState = deleteSwitch?.checked == true
|
||||||
|
val f = file
|
||||||
|
|
||||||
|
when {
|
||||||
|
deleteState -> {
|
||||||
|
imageView.removeAttribute("style")
|
||||||
|
uploadButton.value = ""
|
||||||
|
file = null
|
||||||
|
}
|
||||||
|
f == null -> imageView.style.backgroundImage = original
|
||||||
|
else -> {
|
||||||
|
val reader = FileReader()
|
||||||
|
reader.onload = {
|
||||||
|
val dataUrl = it.target.asDynamic().result as String
|
||||||
|
imageView.style.backgroundImage = "url(\"$dataUrl\")"
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadButton.addEventListener("change", EventListener {
|
||||||
|
val files = uploadButton.files ?: return@EventListener
|
||||||
|
file = files[0]
|
||||||
|
updateImage()
|
||||||
|
})
|
||||||
|
|
||||||
|
deleteSwitch?.addEventListener("change", EventListener {
|
||||||
|
updateImage()
|
||||||
|
})
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package de.kif.frontend.views.overview
|
package de.kif.frontend.views.overview
|
||||||
|
|
||||||
|
import de.kif.common.formatDate
|
||||||
import de.kif.frontend.launch
|
import de.kif.frontend.launch
|
||||||
import de.kif.frontend.repository.PostRepository
|
import de.kif.frontend.repository.PostRepository
|
||||||
import de.westermann.kobserve.event.emit
|
import de.westermann.kobserve.event.emit
|
||||||
|
@ -25,37 +26,44 @@ class PostView(
|
||||||
}
|
}
|
||||||
|
|
||||||
private val nameView: Link
|
private val nameView: Link
|
||||||
private val contentView: View
|
private val contentView: HTMLElement
|
||||||
|
private val footerView: HTMLElement
|
||||||
|
private val imageView: HTMLElement
|
||||||
|
|
||||||
private fun reload() {
|
private fun reload() {
|
||||||
launch {
|
launch {
|
||||||
val p = PostRepository.get(postId) ?: return@launch
|
val p = PostRepository.get(postId) ?: return@launch
|
||||||
|
|
||||||
|
classList["post-no-image"] = p.image == null
|
||||||
|
|
||||||
nameView.text = p.name
|
nameView.text = p.name
|
||||||
nameView.target = "/p/${p.url}"
|
nameView.target = "/p/${p.url}"
|
||||||
pinned = p.pinned
|
pinned = p.pinned
|
||||||
|
|
||||||
contentView.html.innerHTML = PostRepository.htmlByUrl(p.url)
|
if (p.image == null) {
|
||||||
|
imageView.removeAttribute("style")
|
||||||
|
} else {
|
||||||
|
imageView.style.backgroundImage = "url(\"/images/${p.image}\")"
|
||||||
|
}
|
||||||
|
|
||||||
|
contentView.innerHTML = PostRepository.htmlByUrl(p.url)
|
||||||
|
footerView.innerText = formatDate(p.createdAt)
|
||||||
|
|
||||||
emit(PostChangeEvent(postId))
|
emit(PostChangeEvent(postId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val nameHtml = view.getElementsByClassName("post-name")[0]
|
nameView = Link.wrap(view.getByClassOrCreate("post-name"))
|
||||||
nameView = nameHtml?.let { Link.wrap(it as HTMLAnchorElement) } ?: Link().also {
|
|
||||||
html.appendChild(it.html)
|
|
||||||
it.classList += "post-name"
|
|
||||||
}
|
|
||||||
|
|
||||||
// val editHtml = view.getElementsByClassName("post-edit")[0]
|
val postColumn = view.getByClassOrCreate<HTMLElement>("post-column")
|
||||||
// editView = editHtml?.let { Link.wrap(it as HTMLAnchorElement) } ?: Link()
|
val postColumnLeft = postColumn.getByClassOrCreate<HTMLElement>("post-column-left")
|
||||||
|
val postColumnRight = postColumn.getByClassOrCreate<HTMLElement>("post-column-right")
|
||||||
|
|
||||||
val contentHtml = view.getElementsByClassName("post-content")[0]
|
imageView = postColumnLeft.getByClassOrCreate("post-image", "figure")
|
||||||
contentView = contentHtml?.let { wrap(it as HTMLElement) } ?: wrap(createHtmlView()).also {
|
|
||||||
html.appendChild(it.html)
|
contentView = postColumnRight.getByClassOrCreate("post-content")
|
||||||
it.classList += "post-content"
|
footerView = postColumnRight.getByClassOrCreate("post-footer")
|
||||||
}
|
|
||||||
|
|
||||||
PostRepository.onUpdate {
|
PostRepository.onUpdate {
|
||||||
if (it == postId) {
|
if (it == postId) {
|
||||||
|
@ -69,7 +77,7 @@ class PostView(
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun create(postId: Long): PostView {
|
fun create(postId: Long): PostView {
|
||||||
val div = document.createElement("div") as HTMLElement
|
val div = createHtmlView<HTMLElement>()
|
||||||
div.classList.add("post")
|
div.classList.add("post")
|
||||||
div.dataset["id"] = postId.toString()
|
div.dataset["id"] = postId.toString()
|
||||||
return PostView(div).also(PostView::reload)
|
return PostView(div).also(PostView::reload)
|
||||||
|
@ -78,3 +86,16 @@ class PostView(
|
||||||
}
|
}
|
||||||
|
|
||||||
data class PostChangeEvent(val id: Long)
|
data class PostChangeEvent(val id: Long)
|
||||||
|
|
||||||
|
inline fun <reified T : HTMLElement> HTMLElement.getByClassOrCreate(name: String, newTagName: String? = null): T {
|
||||||
|
val v = this.getElementsByClassName(name)[0] as? T
|
||||||
|
|
||||||
|
if (v != null) {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
val h = createHtmlView<T>(newTagName)
|
||||||
|
h.classList.add(name)
|
||||||
|
this.appendChild(h)
|
||||||
|
return h
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ $background-primary-color: #fff;
|
||||||
$background-secondary-color: #fcfcfc;
|
$background-secondary-color: #fcfcfc;
|
||||||
|
|
||||||
$text-primary-color: #333;
|
$text-primary-color: #333;
|
||||||
|
$text-secondary-color: rgba($text-primary-color, 0.5);
|
||||||
|
|
||||||
$primary-color: #B11D33;
|
$primary-color: #B11D33;
|
||||||
$primary-text-color: #fff;
|
$primary-text-color: #fff;
|
||||||
|
@ -18,7 +19,9 @@ $primary-text-color: #fff;
|
||||||
$error-color: #D55225;
|
$error-color: #D55225;
|
||||||
$error-text-color: #fff;
|
$error-text-color: #fff;
|
||||||
|
|
||||||
$border-color: #888;
|
$input-border-color: #888;
|
||||||
|
$table-border-color: rgba($text-primary-color, 0.1);
|
||||||
|
$table-header-color: rgba($text-primary-color, 0.06);
|
||||||
|
|
||||||
$transitionTime: 150ms;
|
$transitionTime: 150ms;
|
||||||
|
|
||||||
|
@ -40,6 +43,10 @@ body, html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
|
& > *:last-child {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-select {
|
.no-select {
|
||||||
|
@ -128,7 +135,7 @@ a {
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba($primary-text-color, 0.1);
|
background-color: $table-border-color;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
background: $primary-color;
|
background: $primary-color;
|
||||||
|
@ -165,8 +172,8 @@ a {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: $background-secondary-color;
|
background-color: $background-secondary-color;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
left: 1rem;
|
left: 0;
|
||||||
right: 1rem;
|
right: 0;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
|
@ -183,7 +190,11 @@ a {
|
||||||
line-height: 3rem;
|
line-height: 3rem;
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
bottom: 0.2rem;
|
bottom: 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -220,7 +231,7 @@ a {
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
padding: 0 1rem;
|
//padding: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-layout-search {
|
.table-layout-search {
|
||||||
|
@ -252,14 +263,14 @@ a {
|
||||||
}
|
}
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
border-top: solid 1px rgba($text-primary-color, 0.1);
|
border-top: solid 1px $table-border-color;
|
||||||
|
|
||||||
&:nth-child(odd) {
|
&:nth-child(odd) {
|
||||||
//background-color: rgba($text-primary-color, 0.01);
|
//background-color: rgba($text-primary-color, 0.01);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
background-color: rgba($text-primary-color, 0.06);
|
background-color: $table-header-color;
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
line-height: 2.5rem;
|
line-height: 2.5rem;
|
||||||
}
|
}
|
||||||
|
@ -269,7 +280,7 @@ a {
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba($text-primary-color, 0.06);
|
background-color: $table-header-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -292,20 +303,20 @@ a {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
background: $background-primary-color;
|
background: $background-primary-color;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: solid 1px rgba($text-primary-color, 0.1);
|
border: solid 1px $table-border-color;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
padding: 0 0.5rem;
|
padding: 0 0.5rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba($text-primary-color, 0.06);
|
background-color: $table-header-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control {
|
.form-control {
|
||||||
border: solid 1px $border-color;
|
border: solid 1px $input-border-color;
|
||||||
outline: none;
|
outline: none;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
line-height: 2.5rem;
|
line-height: 2.5rem;
|
||||||
|
@ -412,7 +423,7 @@ select:-moz-focusring {
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-btn {
|
.form-btn {
|
||||||
border: solid 1px $border-color;
|
border: solid 1px $input-border-color;
|
||||||
outline: none;
|
outline: none;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
|
@ -588,7 +599,7 @@ form {
|
||||||
.calendar[data-editable = "true"].edit {
|
.calendar[data-editable = "true"].edit {
|
||||||
.calendar-table {
|
.calendar-table {
|
||||||
width: calc(100% - 16rem);
|
width: calc(100% - 16rem);
|
||||||
border-right: solid 1px rgba($text-primary-color, 0.1);
|
border-right: solid 1px $table-border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-edit-main {
|
.calendar-edit-main {
|
||||||
|
@ -608,8 +619,8 @@ form {
|
||||||
display: none;
|
display: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|
||||||
border: solid 1px $border-color;
|
border: solid 1px $input-border-color;
|
||||||
box-shadow: 0 0.1rem 0.2rem rgba($primary-text-color);
|
box-shadow: 0 0.1rem 0.2rem $primary-text-color;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
padding: 0.2rem;
|
padding: 0.2rem;
|
||||||
|
@ -707,14 +718,14 @@ form {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
border-left: solid 1px rgba($text-primary-color, 0.1);
|
border-left: solid 1px $table-border-color;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-row {
|
.calendar-row {
|
||||||
border-top: solid 1px rgba($text-primary-color, 0.1);
|
border-top: solid 1px $table-border-color;
|
||||||
line-height: 3rem;
|
line-height: 3rem;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
|
|
||||||
|
@ -727,12 +738,12 @@ form {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
border-left: solid 1px rgba($text-primary-color, 0.1);
|
border-left: solid 1px $table-border-color;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba($text-primary-color, 0.06);
|
background-color: $table-header-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-entry {
|
.calendar-entry {
|
||||||
|
@ -749,7 +760,7 @@ form {
|
||||||
width: 6rem;
|
width: 6rem;
|
||||||
left: 0;
|
left: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-right: solid 1px rgba($text-primary-color, 0.1);
|
border-right: solid 1px $table-border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-link {
|
.calendar-link {
|
||||||
|
@ -780,7 +791,7 @@ form {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
border-left: solid 1px rgba($text-primary-color, 0.1);
|
border-left: solid 1px $table-border-color;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -800,12 +811,12 @@ form {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
border-left: solid 1px rgba($text-primary-color, 0.1);
|
border-left: solid 1px $table-border-color;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba($text-primary-color, 0.06);
|
background-color: $table-header-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-entry {
|
.calendar-entry {
|
||||||
|
@ -826,7 +837,7 @@ form {
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-child(4n + 2) .calendar-cell::before {
|
&:nth-child(4n + 2) .calendar-cell::before {
|
||||||
border-top: solid 1px rgba($text-primary-color, 0.1);
|
border-top: solid 1px $table-border-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -939,13 +950,13 @@ form {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
background: $background-primary-color;
|
background: $background-primary-color;
|
||||||
border: solid 1px rgba($text-primary-color, 0.1);
|
border: solid 1px $table-border-color;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
padding: 0 0.5rem;
|
padding: 0 0.5rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba($text-primary-color, 0.06);
|
background-color: $table-header-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -956,11 +967,13 @@ form {
|
||||||
|
|
||||||
.overview {
|
.overview {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview-main {
|
.overview-main {
|
||||||
flex-grow: 4;
|
flex-grow: 4;
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview-side {
|
.overview-side {
|
||||||
|
@ -985,6 +998,7 @@ form {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
color: $primary-color;
|
color: $primary-color;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
|
padding: 0 1rem;
|
||||||
|
|
||||||
&:empty::before {
|
&:empty::before {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -999,11 +1013,30 @@ form {
|
||||||
.post-edit {
|
.post-edit {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 1rem;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
.post-content {
|
||||||
|
padding: 0 1rem;
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6, p {
|
h1, h2, h3, h4, h5, h6, p {
|
||||||
margin: 0.7rem 0;
|
margin: 0.7rem 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -1039,11 +1072,11 @@ form {
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
border-top: solid 1px rgba($text-primary-color, 0.1);
|
border-top: solid 1px $table-border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background-color: rgba($text-primary-color, 0.06);
|
background-color: $table-header-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
td, th {
|
td, th {
|
||||||
|
@ -1051,6 +1084,14 @@ form {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-footer {
|
||||||
|
color: $text-secondary-color;
|
||||||
|
padding: 0 1rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-top: -0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
.post-edit-container {
|
.post-edit-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -1060,29 +1101,84 @@ form {
|
||||||
margin-left: 0;
|
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 $input-border-color;
|
||||||
|
margin: 0;
|
||||||
|
background-color: $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: $table-border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@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 {
|
.post-edit-container {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
.post-edit-right {
|
.post-edit-right {
|
||||||
margin-left: 2rem;
|
margin-left: 2rem;
|
||||||
}
|
}
|
||||||
}
|
.overview {
|
||||||
|
flex-direction: row;
|
||||||
/*
|
|
||||||
.overview-post {
|
|
||||||
max-height: 20rem;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
display: block;
|
|
||||||
height: 1.5rem;
|
|
||||||
top: 18.5rem;
|
|
||||||
width: 100%;
|
|
||||||
background: linear-gradient(0deg, $background-primary-color, transparent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
|
@ -34,6 +34,10 @@ fun Application.main() {
|
||||||
files(Configuration.Path.webPath.toFile())
|
files(Configuration.Path.webPath.toFile())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static("/images") {
|
||||||
|
files(Configuration.Path.uploadsPath.toFile())
|
||||||
|
}
|
||||||
|
|
||||||
// UI routes
|
// UI routes
|
||||||
overview()
|
overview()
|
||||||
calendar()
|
calendar()
|
||||||
|
|
|
@ -71,17 +71,30 @@ object Configuration {
|
||||||
val signKey by c(SecuritySpec.signKey)
|
val signKey by c(SecuritySpec.signKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private object GeneralSpec : ConfigSpec("general") {
|
||||||
|
val allowedUploadExtensions by required<String>("allowed_upload_extensions")
|
||||||
|
}
|
||||||
|
|
||||||
|
object General {
|
||||||
|
val allowedUploadExtensions by c(GeneralSpec.allowedUploadExtensions)
|
||||||
|
val allowedUploadExtensionSet by lazy {
|
||||||
|
allowedUploadExtensions.split(",").map { it.trim().toLowerCase() }.toSet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
var config = Config {
|
var config = Config {
|
||||||
addSpec(ServerSpec)
|
addSpec(ServerSpec)
|
||||||
addSpec(PathSpec)
|
addSpec(PathSpec)
|
||||||
addSpec(ScheduleSpec)
|
addSpec(ScheduleSpec)
|
||||||
addSpec(SecuritySpec)
|
addSpec(SecuritySpec)
|
||||||
|
addSpec(GeneralSpec)
|
||||||
}.from.toml.resource("portal.toml")
|
}.from.toml.resource("portal.toml")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
config = config.from.toml.file("portal.toml")
|
config = config.from.toml.file("portal.toml")
|
||||||
} catch (_: FileNotFoundException) { }
|
} catch (_: FileNotFoundException) {
|
||||||
|
}
|
||||||
|
|
||||||
this.config = config.from.env()
|
this.config = config.from.env()
|
||||||
.from.systemProperties()
|
.from.systemProperties()
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
package de.kif.backend
|
package de.kif.backend
|
||||||
|
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import java.io.File
|
import java.io.InputStream
|
||||||
import java.net.URI
|
|
||||||
import java.nio.file.*
|
import java.nio.file.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -52,6 +51,18 @@ object Resources {
|
||||||
logger.info { "Successfully extract web content" }
|
logger.info { "Successfully extract web content" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteUpload(name: String) {
|
||||||
|
Files.deleteIfExists(Configuration.Path.uploadsPath.resolve(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun existsUpload(name: String): Boolean {
|
||||||
|
return Files.exists(Configuration.Path.uploadsPath.resolve(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createUpload(name: String, input: InputStream) {
|
||||||
|
Files.copy(input, Configuration.Path.uploadsPath.resolve(name))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of js modules to be included.
|
* List of js modules to be included.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -8,6 +8,9 @@ object DbTrack : Table() {
|
||||||
val id = long("id").autoIncrement().primaryKey()
|
val id = long("id").autoIncrement().primaryKey()
|
||||||
val name = varchar("name", 64)
|
val name = varchar("name", 64)
|
||||||
val color = varchar("color", 32)
|
val color = varchar("color", 32)
|
||||||
|
|
||||||
|
val createdAt = long("createdAt")
|
||||||
|
val updatedAt = long("updatedAt")
|
||||||
}
|
}
|
||||||
|
|
||||||
object DbWorkGroup : Table() {
|
object DbWorkGroup : Table() {
|
||||||
|
@ -28,6 +31,9 @@ object DbWorkGroup : Table() {
|
||||||
|
|
||||||
val length = integer("length")
|
val length = integer("length")
|
||||||
val constraints = text("constraints")
|
val constraints = text("constraints")
|
||||||
|
|
||||||
|
val createdAt = long("createdAt")
|
||||||
|
val updatedAt = long("updatedAt")
|
||||||
}
|
}
|
||||||
|
|
||||||
object DbRoom : Table() {
|
object DbRoom : Table() {
|
||||||
|
@ -41,6 +47,9 @@ object DbRoom : Table() {
|
||||||
val whiteboard = bool("whiteboard")
|
val whiteboard = bool("whiteboard")
|
||||||
val blackboard = bool("blackboard")
|
val blackboard = bool("blackboard")
|
||||||
val accessible = bool("accessible")
|
val accessible = bool("accessible")
|
||||||
|
|
||||||
|
val createdAt = long("createdAt")
|
||||||
|
val updatedAt = long("updatedAt")
|
||||||
}
|
}
|
||||||
|
|
||||||
object DbSchedule : Table() {
|
object DbSchedule : Table() {
|
||||||
|
@ -49,12 +58,18 @@ object DbSchedule : Table() {
|
||||||
val roomId = long("room_id").index()
|
val roomId = long("room_id").index()
|
||||||
val day = integer("day").index()
|
val day = integer("day").index()
|
||||||
val time = integer("time_slot")
|
val time = integer("time_slot")
|
||||||
|
|
||||||
|
val createdAt = long("createdAt")
|
||||||
|
val updatedAt = long("updatedAt")
|
||||||
}
|
}
|
||||||
|
|
||||||
object DbUser : Table() {
|
object DbUser : Table() {
|
||||||
val id = long("id").autoIncrement().primaryKey()
|
val id = long("id").autoIncrement().primaryKey()
|
||||||
val username = varchar("username", 64).uniqueIndex()
|
val username = varchar("username", 64).uniqueIndex()
|
||||||
val password = varchar("password", 64)
|
val password = varchar("password", 64)
|
||||||
|
|
||||||
|
val createdAt = long("createdAt")
|
||||||
|
val updatedAt = long("updatedAt")
|
||||||
}
|
}
|
||||||
|
|
||||||
object DbUserPermission : Table() {
|
object DbUserPermission : Table() {
|
||||||
|
@ -68,5 +83,9 @@ object DbPost : Table() {
|
||||||
|
|
||||||
val content = text("content")
|
val content = text("content")
|
||||||
val url = varchar("url", 64).uniqueIndex()
|
val url = varchar("url", 64).uniqueIndex()
|
||||||
|
val image = varchar("image", 64).nullable()
|
||||||
val pinned = bool("pinned")
|
val pinned = bool("pinned")
|
||||||
|
|
||||||
|
val createdAt = long("createdAt")
|
||||||
|
val updatedAt = long("updatedAt")
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import de.kif.common.model.Post
|
||||||
import de.westermann.kobserve.event.EventHandler
|
import de.westermann.kobserve.event.EventHandler
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
object PostRepository : Repository<Post> {
|
object PostRepository : Repository<Post> {
|
||||||
|
|
||||||
|
@ -22,9 +23,13 @@ object PostRepository : Repository<Post> {
|
||||||
val name = row[DbPost.name]
|
val name = row[DbPost.name]
|
||||||
val content = row[DbPost.content]
|
val content = row[DbPost.content]
|
||||||
val url = row[DbPost.url]
|
val url = row[DbPost.url]
|
||||||
|
val image = row[DbPost.image]
|
||||||
val pinned = row[DbPost.pinned]
|
val pinned = row[DbPost.pinned]
|
||||||
|
|
||||||
return Post(id, name, content, url, pinned)
|
val createdAt = row[DbPost.createdAt]
|
||||||
|
val updatedAt = row[DbPost.updatedAt]
|
||||||
|
|
||||||
|
return Post(id, name, content, url, image, pinned, createdAt, updatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun get(id: Long): Post? {
|
override suspend fun get(id: Long): Post? {
|
||||||
|
@ -40,6 +45,8 @@ object PostRepository : Repository<Post> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val now = Date().time
|
||||||
|
|
||||||
return dbQuery {
|
return dbQuery {
|
||||||
if (model.pinned) {
|
if (model.pinned) {
|
||||||
DbPost.update({ DbPost.pinned eq true }) {
|
DbPost.update({ DbPost.pinned eq true }) {
|
||||||
|
@ -51,7 +58,11 @@ object PostRepository : Repository<Post> {
|
||||||
it[name] = model.name
|
it[name] = model.name
|
||||||
it[content] = model.content
|
it[content] = model.content
|
||||||
it[url] = model.url
|
it[url] = model.url
|
||||||
|
it[image] = model.image
|
||||||
it[pinned] = model.pinned
|
it[pinned] = model.pinned
|
||||||
|
|
||||||
|
it[createdAt] = now
|
||||||
|
it[updatedAt] = now
|
||||||
}[DbPost.id] ?: throw IllegalStateException("Cannot create model!")
|
}[DbPost.id] ?: throw IllegalStateException("Cannot create model!")
|
||||||
|
|
||||||
onCreate.emit(id)
|
onCreate.emit(id)
|
||||||
|
@ -71,12 +82,17 @@ object PostRepository : Repository<Post> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val now = Date().time
|
||||||
|
|
||||||
dbQuery {
|
dbQuery {
|
||||||
DbPost.update({ DbPost.id eq model.id }) {
|
DbPost.update({ DbPost.id eq model.id }) {
|
||||||
it[name] = model.name
|
it[name] = model.name
|
||||||
it[content] = model.content
|
it[content] = model.content
|
||||||
it[url] = model.url
|
it[url] = model.url
|
||||||
|
it[image] = model.image
|
||||||
it[pinned] = model.pinned
|
it[pinned] = model.pinned
|
||||||
|
|
||||||
|
it[updatedAt] = now
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdate.emit(model.id)
|
onUpdate.emit(model.id)
|
||||||
|
@ -112,7 +128,7 @@ object PostRepository : Repository<Post> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getPinned(): List<Post> {
|
private suspend fun getPinned(): List<Post> {
|
||||||
return dbQuery {
|
return dbQuery {
|
||||||
val result = DbPost.select { DbPost.pinned eq true }
|
val result = DbPost.select { DbPost.pinned eq true }
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import de.kif.common.model.Room
|
||||||
import de.westermann.kobserve.event.EventHandler
|
import de.westermann.kobserve.event.EventHandler
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
object RoomRepository : Repository<Room> {
|
object RoomRepository : Repository<Room> {
|
||||||
|
|
||||||
|
@ -25,7 +26,21 @@ object RoomRepository : Repository<Room> {
|
||||||
val blackboard = row[DbRoom.blackboard]
|
val blackboard = row[DbRoom.blackboard]
|
||||||
val accessible = row[DbRoom.accessible]
|
val accessible = row[DbRoom.accessible]
|
||||||
|
|
||||||
return Room(id, name, places, projector, internet, whiteboard, blackboard, accessible)
|
val createdAt = row[DbRoom.createdAt]
|
||||||
|
val updatedAt = row[DbRoom.updatedAt]
|
||||||
|
|
||||||
|
return Room(
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
places,
|
||||||
|
projector,
|
||||||
|
internet,
|
||||||
|
whiteboard,
|
||||||
|
blackboard,
|
||||||
|
accessible,
|
||||||
|
createdAt,
|
||||||
|
updatedAt
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun get(id: Long): Room? {
|
override suspend fun get(id: Long): Room? {
|
||||||
|
@ -35,6 +50,8 @@ object RoomRepository : Repository<Room> {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun create(model: Room): Long {
|
override suspend fun create(model: Room): Long {
|
||||||
|
val now = Date().time
|
||||||
|
|
||||||
return dbQuery {
|
return dbQuery {
|
||||||
val id = DbRoom.insert {
|
val id = DbRoom.insert {
|
||||||
it[name] = model.name
|
it[name] = model.name
|
||||||
|
@ -44,6 +61,8 @@ object RoomRepository : Repository<Room> {
|
||||||
it[whiteboard] = model.whiteboard
|
it[whiteboard] = model.whiteboard
|
||||||
it[blackboard] = model.blackboard
|
it[blackboard] = model.blackboard
|
||||||
it[accessible] = model.accessible
|
it[accessible] = model.accessible
|
||||||
|
it[createdAt] = now
|
||||||
|
it[updatedAt] = now
|
||||||
}[DbRoom.id] ?: throw IllegalStateException("Cannot create model!")
|
}[DbRoom.id] ?: throw IllegalStateException("Cannot create model!")
|
||||||
|
|
||||||
onCreate.emit(id)
|
onCreate.emit(id)
|
||||||
|
@ -54,6 +73,9 @@ object RoomRepository : Repository<Room> {
|
||||||
|
|
||||||
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!")
|
||||||
|
|
||||||
|
val now = Date().time
|
||||||
|
|
||||||
dbQuery {
|
dbQuery {
|
||||||
DbRoom.update({ DbRoom.id eq model.id }) {
|
DbRoom.update({ DbRoom.id eq model.id }) {
|
||||||
it[name] = model.name
|
it[name] = model.name
|
||||||
|
@ -63,6 +85,7 @@ object RoomRepository : Repository<Room> {
|
||||||
it[whiteboard] = model.whiteboard
|
it[whiteboard] = model.whiteboard
|
||||||
it[blackboard] = model.blackboard
|
it[blackboard] = model.blackboard
|
||||||
it[accessible] = model.accessible
|
it[accessible] = model.accessible
|
||||||
|
it[updatedAt] = now
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdate.emit(model.id)
|
onUpdate.emit(model.id)
|
||||||
|
|
|
@ -8,6 +8,7 @@ import de.kif.common.model.Schedule
|
||||||
import de.westermann.kobserve.event.EventHandler
|
import de.westermann.kobserve.event.EventHandler
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
object ScheduleRepository : Repository<Schedule> {
|
object ScheduleRepository : Repository<Schedule> {
|
||||||
|
|
||||||
|
@ -22,12 +23,15 @@ object ScheduleRepository : Repository<Schedule> {
|
||||||
val day = row[DbSchedule.day]
|
val day = row[DbSchedule.day]
|
||||||
val time = row[DbSchedule.time]
|
val time = row[DbSchedule.time]
|
||||||
|
|
||||||
|
val createdAt = row[DbSchedule.createdAt]
|
||||||
|
val updatedAt = row[DbSchedule.updatedAt]
|
||||||
|
|
||||||
val workGroup = WorkGroupRepository.get(workGroupId)
|
val workGroup = WorkGroupRepository.get(workGroupId)
|
||||||
?: throw IllegalStateException("Work group for schedule does not exist!")
|
?: throw IllegalStateException("Work group for schedule does not exist!")
|
||||||
val room = RoomRepository.get(roomId)
|
val room = RoomRepository.get(roomId)
|
||||||
?: throw IllegalStateException("Room for schedule does not exist!")
|
?: throw IllegalStateException("Room for schedule does not exist!")
|
||||||
|
|
||||||
return Schedule(id, workGroup, room, day, time)
|
return Schedule(id, workGroup, room, day, time, createdAt, updatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun get(id: Long): Schedule? {
|
override suspend fun get(id: Long): Schedule? {
|
||||||
|
@ -41,12 +45,17 @@ object ScheduleRepository : Repository<Schedule> {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun create(model: Schedule): Long {
|
override suspend fun create(model: Schedule): Long {
|
||||||
|
val now = Date().time
|
||||||
|
|
||||||
return dbQuery {
|
return dbQuery {
|
||||||
val id = DbSchedule.insert {
|
val id = DbSchedule.insert {
|
||||||
it[workGroupId] = model.workGroup.id ?: throw IllegalArgumentException("Work group does not exist!")
|
it[workGroupId] = model.workGroup.id ?: throw IllegalArgumentException("Work group does not exist!")
|
||||||
it[roomId] = model.room.id ?: throw IllegalArgumentException("Room does not exist!")
|
it[roomId] = model.room.id ?: throw IllegalArgumentException("Room does not exist!")
|
||||||
it[day] = model.day
|
it[day] = model.day
|
||||||
it[time] = model.time
|
it[time] = model.time
|
||||||
|
|
||||||
|
it[createdAt] = now
|
||||||
|
it[updatedAt] = now
|
||||||
}[DbSchedule.id] ?: throw IllegalStateException("Cannot create model!")
|
}[DbSchedule.id] ?: throw IllegalStateException("Cannot create model!")
|
||||||
|
|
||||||
onCreate.emit(id)
|
onCreate.emit(id)
|
||||||
|
@ -57,12 +66,17 @@ object ScheduleRepository : Repository<Schedule> {
|
||||||
|
|
||||||
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!")
|
||||||
|
|
||||||
|
val now = Date().time
|
||||||
|
|
||||||
dbQuery {
|
dbQuery {
|
||||||
DbSchedule.update({ DbSchedule.id eq model.id }) {
|
DbSchedule.update({ DbSchedule.id eq model.id }) {
|
||||||
it[workGroupId] = model.workGroup.id ?: throw IllegalArgumentException("Work group does not exist!")
|
it[workGroupId] = model.workGroup.id ?: throw IllegalArgumentException("Work group does not exist!")
|
||||||
it[roomId] = model.room.id ?: throw IllegalArgumentException("Room does not exist!")
|
it[roomId] = model.room.id ?: throw IllegalArgumentException("Room does not exist!")
|
||||||
it[day] = model.day
|
it[day] = model.day
|
||||||
it[time] = model.time
|
it[time] = model.time
|
||||||
|
|
||||||
|
it[updatedAt] = now
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdate.emit(model.id)
|
onUpdate.emit(model.id)
|
||||||
|
|
|
@ -11,6 +11,7 @@ import de.kif.common.model.parseColor
|
||||||
import de.westermann.kobserve.event.EventHandler
|
import de.westermann.kobserve.event.EventHandler
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
object TrackRepository : Repository<Track> {
|
object TrackRepository : Repository<Track> {
|
||||||
|
|
||||||
|
@ -23,7 +24,10 @@ object TrackRepository : Repository<Track> {
|
||||||
val name = row[DbTrack.name]
|
val name = row[DbTrack.name]
|
||||||
val color = row[DbTrack.color].parseColor()
|
val color = row[DbTrack.color].parseColor()
|
||||||
|
|
||||||
return Track(id, name, color)
|
val createdAt = row[DbTrack.createdAt]
|
||||||
|
val updatedAt = row[DbTrack.updatedAt]
|
||||||
|
|
||||||
|
return Track(id, name, color, createdAt, updatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun get(id: Long): Track? {
|
override suspend fun get(id: Long): Track? {
|
||||||
|
@ -33,10 +37,15 @@ object TrackRepository : Repository<Track> {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun create(model: Track): Long {
|
override suspend fun create(model: Track): Long {
|
||||||
|
val now = Date().time
|
||||||
|
|
||||||
return dbQuery {
|
return dbQuery {
|
||||||
val id = DbTrack.insert {
|
val id = DbTrack.insert {
|
||||||
it[name] = model.name
|
it[name] = model.name
|
||||||
it[color] = model.color.toString()
|
it[color] = model.color.toString()
|
||||||
|
|
||||||
|
it[createdAt] = now
|
||||||
|
it[updatedAt] = now
|
||||||
}[DbTrack.id] ?: throw IllegalStateException("Cannot create model!")
|
}[DbTrack.id] ?: throw IllegalStateException("Cannot create model!")
|
||||||
|
|
||||||
onCreate.emit(id)
|
onCreate.emit(id)
|
||||||
|
@ -47,10 +56,15 @@ object TrackRepository : Repository<Track> {
|
||||||
|
|
||||||
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!")
|
||||||
|
|
||||||
|
val now = Date().time
|
||||||
|
|
||||||
dbQuery {
|
dbQuery {
|
||||||
DbTrack.update({ DbTrack.id eq model.id }) {
|
DbTrack.update({ DbTrack.id eq model.id }) {
|
||||||
it[name] = model.name
|
it[name] = model.name
|
||||||
it[color] = model.color.toString()
|
it[color] = model.color.toString()
|
||||||
|
|
||||||
|
it[updatedAt] = now
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdate.emit(model.id)
|
onUpdate.emit(model.id)
|
||||||
|
|
|
@ -11,6 +11,7 @@ import de.kif.common.model.User
|
||||||
import de.westermann.kobserve.event.EventHandler
|
import de.westermann.kobserve.event.EventHandler
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
object UserRepository : Repository<User> {
|
object UserRepository : Repository<User> {
|
||||||
|
|
||||||
|
@ -23,11 +24,14 @@ object UserRepository : Repository<User> {
|
||||||
val username = row[DbUser.username]
|
val username = row[DbUser.username]
|
||||||
val password = row[DbUser.password]
|
val password = row[DbUser.password]
|
||||||
|
|
||||||
|
val createdAt = row[DbUser.createdAt]
|
||||||
|
val updatedAt = row[DbUser.updatedAt]
|
||||||
|
|
||||||
val permissions = DbUserPermission.slice(DbUserPermission.permission).select {
|
val permissions = DbUserPermission.slice(DbUserPermission.permission).select {
|
||||||
DbUserPermission.userId eq id
|
DbUserPermission.userId eq id
|
||||||
}.map { it[DbUserPermission.permission] }.toSet()
|
}.map { it[DbUserPermission.permission] }.toSet()
|
||||||
|
|
||||||
return User(id, username, password, permissions)
|
return User(id, username, password, permissions, createdAt, updatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun get(id: Long): User? {
|
override suspend fun get(id: Long): User? {
|
||||||
|
@ -37,10 +41,15 @@ object UserRepository : Repository<User> {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun create(model: User): Long {
|
override suspend fun create(model: User): Long {
|
||||||
|
val now = Date().time
|
||||||
|
|
||||||
return dbQuery {
|
return dbQuery {
|
||||||
val id = DbUser.insert {
|
val id = DbUser.insert {
|
||||||
it[username] = model.username
|
it[username] = model.username
|
||||||
it[password] = model.password
|
it[password] = model.password
|
||||||
|
|
||||||
|
it[createdAt] = now
|
||||||
|
it[updatedAt] = now
|
||||||
}[DbUser.id] ?: throw IllegalStateException("Cannot create model!")
|
}[DbUser.id] ?: throw IllegalStateException("Cannot create model!")
|
||||||
|
|
||||||
for (permission in model.permissions) {
|
for (permission in model.permissions) {
|
||||||
|
@ -58,10 +67,15 @@ object UserRepository : Repository<User> {
|
||||||
|
|
||||||
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!")
|
||||||
|
|
||||||
|
val now = Date().time
|
||||||
|
|
||||||
dbQuery {
|
dbQuery {
|
||||||
DbUser.update({ DbUser.id eq model.id }) {
|
DbUser.update({ DbUser.id eq model.id }) {
|
||||||
it[username] = model.username
|
it[username] = model.username
|
||||||
it[password] = model.password
|
it[password] = model.password
|
||||||
|
|
||||||
|
it[updatedAt] = now
|
||||||
}
|
}
|
||||||
|
|
||||||
DbUserPermission.deleteWhere { DbUserPermission.userId eq model.id }
|
DbUserPermission.deleteWhere { DbUserPermission.userId eq model.id }
|
||||||
|
|
|
@ -13,6 +13,7 @@ import de.westermann.kobserve.event.EventHandler
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.list
|
import kotlinx.serialization.list
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
object WorkGroupRepository : Repository<WorkGroup> {
|
object WorkGroupRepository : Repository<WorkGroup> {
|
||||||
|
|
||||||
|
@ -35,6 +36,9 @@ object WorkGroupRepository : Repository<WorkGroup> {
|
||||||
val language = row[DbWorkGroup.language]
|
val language = row[DbWorkGroup.language]
|
||||||
val constraints = Message.json.parse(Constraint.serializer().list, row[DbWorkGroup.constraints])
|
val constraints = Message.json.parse(Constraint.serializer().list, row[DbWorkGroup.constraints])
|
||||||
|
|
||||||
|
val createdAt = row[DbWorkGroup.createdAt]
|
||||||
|
val updatedAt = row[DbWorkGroup.updatedAt]
|
||||||
|
|
||||||
val track = trackId?.let { TrackRepository.get(it) }
|
val track = trackId?.let { TrackRepository.get(it) }
|
||||||
|
|
||||||
return WorkGroup(
|
return WorkGroup(
|
||||||
|
@ -50,7 +54,9 @@ object WorkGroupRepository : Repository<WorkGroup> {
|
||||||
accessible,
|
accessible,
|
||||||
length,
|
length,
|
||||||
language,
|
language,
|
||||||
constraints
|
constraints,
|
||||||
|
createdAt,
|
||||||
|
updatedAt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,6 +71,8 @@ object WorkGroupRepository : Repository<WorkGroup> {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun create(model: WorkGroup): Long {
|
override suspend fun create(model: WorkGroup): Long {
|
||||||
|
val now = Date().time
|
||||||
|
|
||||||
return dbQuery {
|
return dbQuery {
|
||||||
val id = DbWorkGroup.insert {
|
val id = DbWorkGroup.insert {
|
||||||
it[name] = model.name
|
it[name] = model.name
|
||||||
|
@ -79,6 +87,9 @@ object WorkGroupRepository : Repository<WorkGroup> {
|
||||||
it[length] = model.length
|
it[length] = model.length
|
||||||
it[language] = model.language
|
it[language] = model.language
|
||||||
it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints)
|
it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints)
|
||||||
|
|
||||||
|
it[createdAt] = now
|
||||||
|
it[updatedAt] = now
|
||||||
}[DbWorkGroup.id] ?: throw IllegalStateException("Cannot create model!")
|
}[DbWorkGroup.id] ?: throw IllegalStateException("Cannot create model!")
|
||||||
|
|
||||||
onCreate.emit(id)
|
onCreate.emit(id)
|
||||||
|
@ -89,6 +100,9 @@ object WorkGroupRepository : Repository<WorkGroup> {
|
||||||
|
|
||||||
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!")
|
||||||
|
|
||||||
|
val now = Date().time
|
||||||
|
|
||||||
dbQuery {
|
dbQuery {
|
||||||
DbWorkGroup.update({ DbWorkGroup.id eq model.id }) {
|
DbWorkGroup.update({ DbWorkGroup.id eq model.id }) {
|
||||||
it[name] = model.name
|
it[name] = model.name
|
||||||
|
@ -103,6 +117,8 @@ object WorkGroupRepository : Repository<WorkGroup> {
|
||||||
it[length] = model.length
|
it[length] = model.length
|
||||||
it[language] = model.language
|
it[language] = model.language
|
||||||
it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints)
|
it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints)
|
||||||
|
|
||||||
|
it[updatedAt] = now
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdate.emit(model.id)
|
onUpdate.emit(model.id)
|
||||||
|
|
|
@ -1,30 +1,39 @@
|
||||||
package de.kif.backend.route
|
package de.kif.backend.route
|
||||||
|
|
||||||
|
import de.kif.backend.Configuration
|
||||||
|
import de.kif.backend.Resources
|
||||||
import de.kif.backend.authenticateOrRedirect
|
import de.kif.backend.authenticateOrRedirect
|
||||||
import de.kif.backend.isAuthenticated
|
import de.kif.backend.isAuthenticated
|
||||||
import de.kif.backend.repository.PostRepository
|
import de.kif.backend.repository.PostRepository
|
||||||
import de.kif.backend.util.markdownToHtml
|
import de.kif.backend.util.markdownToHtml
|
||||||
import de.kif.backend.view.MainTemplate
|
import de.kif.backend.view.MainTemplate
|
||||||
import de.kif.backend.view.MenuTemplate
|
import de.kif.backend.view.MenuTemplate
|
||||||
|
import de.kif.common.formatDate
|
||||||
import de.kif.common.model.Permission
|
import de.kif.common.model.Permission
|
||||||
import de.kif.common.model.Post
|
import de.kif.common.model.Post
|
||||||
import io.ktor.application.call
|
import io.ktor.application.call
|
||||||
import io.ktor.html.respondHtmlTemplate
|
import io.ktor.html.respondHtmlTemplate
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.request.receiveParameters
|
import io.ktor.http.content.PartData
|
||||||
|
import io.ktor.http.content.forEachPart
|
||||||
|
import io.ktor.http.content.streamProvider
|
||||||
|
import io.ktor.request.receiveMultipart
|
||||||
import io.ktor.response.respond
|
import io.ktor.response.respond
|
||||||
import io.ktor.response.respondRedirect
|
import io.ktor.response.respondRedirect
|
||||||
import io.ktor.routing.Route
|
import io.ktor.routing.Route
|
||||||
import io.ktor.routing.get
|
import io.ktor.routing.get
|
||||||
import io.ktor.routing.post
|
import io.ktor.routing.post
|
||||||
import io.ktor.util.toMap
|
|
||||||
import kotlinx.html.*
|
import kotlinx.html.*
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
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"
|
||||||
if (additionalClasses.isNotBlank()) {
|
if (additionalClasses.isNotBlank()) {
|
||||||
classes += " $additionalClasses"
|
classes += " $additionalClasses"
|
||||||
}
|
}
|
||||||
|
if (post.image == null) {
|
||||||
|
classes += " post-no-image"
|
||||||
|
}
|
||||||
div(classes) {
|
div(classes) {
|
||||||
attributes["data-id"] = post.id.toString()
|
attributes["data-id"] = post.id.toString()
|
||||||
attributes["data-pinned"] = post.pinned.toString()
|
attributes["data-pinned"] = post.pinned.toString()
|
||||||
|
@ -37,11 +46,26 @@ fun DIV.createPost(post: Post, editable: Boolean = false, additionalClasses: Str
|
||||||
i("material-icons") { +"edit" }
|
i("material-icons") { +"edit" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div("post-column") {
|
||||||
|
div("post-column-left") {
|
||||||
|
figure(classes = "post-image") {
|
||||||
|
if (post.image != null) {
|
||||||
|
attributes["style"] = "background-image: url(\"/images/${post.image}\")"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div("post-column-right") {
|
||||||
div("post-content") {
|
div("post-content") {
|
||||||
unsafe {
|
unsafe {
|
||||||
raw(markdownToHtml(post.content))
|
raw(markdownToHtml(post.content))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
div("post-footer") {
|
||||||
|
+formatDate(post.createdAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +142,7 @@ fun Route.overview() {
|
||||||
h1 { +"Edit post" }
|
h1 { +"Edit post" }
|
||||||
div("post-edit-container") {
|
div("post-edit-container") {
|
||||||
div("post-edit-left") {
|
div("post-edit-left") {
|
||||||
form(method = FormMethod.post) {
|
form(method = FormMethod.post, encType = FormEncType.multipartFormData) {
|
||||||
div("form-group") {
|
div("form-group") {
|
||||||
label {
|
label {
|
||||||
htmlFor = "name"
|
htmlFor = "name"
|
||||||
|
@ -149,6 +173,54 @@ fun Route.overview() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
htmlFor = "image"
|
||||||
|
+"Image"
|
||||||
|
}
|
||||||
|
div("post-edit-image-box") {
|
||||||
|
div {
|
||||||
|
figure(classes = "post-edit-image") {
|
||||||
|
if (editPost.image != null) {
|
||||||
|
attributes["style"] =
|
||||||
|
"background-image: url(\"/images/${editPost.image}\")"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
div("form-group") {
|
||||||
|
input(
|
||||||
|
name = "image",
|
||||||
|
classes = "form-btn",
|
||||||
|
type = InputType.file
|
||||||
|
) {
|
||||||
|
id = "image"
|
||||||
|
value = "Upload image"
|
||||||
|
accept =
|
||||||
|
Configuration.General
|
||||||
|
.allowedUploadExtensionSet
|
||||||
|
.joinToString(",") {
|
||||||
|
".$it"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div("form-group form-switch") {
|
||||||
|
input(
|
||||||
|
name = "image-delete",
|
||||||
|
classes = "form-control",
|
||||||
|
type = InputType.checkBox
|
||||||
|
) {
|
||||||
|
id = "image-delete"
|
||||||
|
checked = false
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
htmlFor = "image-delete"
|
||||||
|
+"Delete image"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
div("form-switch-group") {
|
div("form-switch-group") {
|
||||||
div("form-group form-switch") {
|
div("form-group form-switch") {
|
||||||
input(
|
input(
|
||||||
|
@ -208,9 +280,37 @@ fun Route.overview() {
|
||||||
post("/post/{id}") {
|
post("/post/{id}") {
|
||||||
authenticateOrRedirect(Permission.POST) { user ->
|
authenticateOrRedirect(Permission.POST) { user ->
|
||||||
val postId = call.parameters["id"]?.toLongOrNull() ?: return@post
|
val postId = call.parameters["id"]?.toLongOrNull() ?: return@post
|
||||||
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
|
|
||||||
list.firstOrNull()
|
var imageUploadName: String? = null
|
||||||
|
|
||||||
|
val params = mutableMapOf<String, String>()
|
||||||
|
call.receiveMultipart().forEachPart { part ->
|
||||||
|
val name = part.name ?: return@forEachPart
|
||||||
|
when (part) {
|
||||||
|
is PartData.FormItem -> {
|
||||||
|
params[name] = part.value
|
||||||
}
|
}
|
||||||
|
is PartData.FileItem -> {
|
||||||
|
val extension = File(part.originalFileName).extension
|
||||||
|
|
||||||
|
if (extension.toLowerCase() !in Configuration.General.allowedUploadExtensionSet) return@forEachPart
|
||||||
|
|
||||||
|
var uploadName = Post.generateUrl() + "." + extension
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (Resources.existsUpload(uploadName)) {
|
||||||
|
uploadName = Post.generateUrl() + "." + extension
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Resources.createUpload(uploadName, part.streamProvider())
|
||||||
|
imageUploadName = uploadName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var post = PostRepository.get(postId) ?: return@post
|
var post = PostRepository.get(postId) ?: return@post
|
||||||
|
|
||||||
params["name"]?.let { post = post.copy(name = it) }
|
params["name"]?.let { post = post.copy(name = it) }
|
||||||
|
@ -218,6 +318,28 @@ fun Route.overview() {
|
||||||
params["content"]?.let { post = post.copy(content = it) }
|
params["content"]?.let { post = post.copy(content = it) }
|
||||||
params["pinned"]?.let { post = post.copy(pinned = it == "on") }
|
params["pinned"]?.let { post = post.copy(pinned = it == "on") }
|
||||||
|
|
||||||
|
if (params["image-delete"] == "on") {
|
||||||
|
val currentImage = post.image
|
||||||
|
if (currentImage != null) {
|
||||||
|
post = post.copy(image = null)
|
||||||
|
Resources.deleteUpload(currentImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
val upload = imageUploadName
|
||||||
|
if (upload != null) {
|
||||||
|
Resources.deleteUpload(upload)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
imageUploadName?.let {
|
||||||
|
val currentImage = post.image
|
||||||
|
if (currentImage != null) {
|
||||||
|
Resources.deleteUpload(currentImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
post = post.copy(image = it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PostRepository.update(post)
|
PostRepository.update(post)
|
||||||
|
|
||||||
call.respondRedirect("/")
|
call.respondRedirect("/")
|
||||||
|
@ -235,7 +357,7 @@ fun Route.overview() {
|
||||||
h1 { +"Create post" }
|
h1 { +"Create post" }
|
||||||
div("post-edit-container") {
|
div("post-edit-container") {
|
||||||
div("post-edit-left") {
|
div("post-edit-left") {
|
||||||
form(method = FormMethod.post) {
|
form(method = FormMethod.post, encType = FormEncType.multipartFormData) {
|
||||||
div("form-group") {
|
div("form-group") {
|
||||||
label {
|
label {
|
||||||
htmlFor = "name"
|
htmlFor = "name"
|
||||||
|
@ -265,6 +387,34 @@ fun Route.overview() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
htmlFor = "image"
|
||||||
|
+"Image"
|
||||||
|
}
|
||||||
|
div("post-edit-image-box") {
|
||||||
|
div {
|
||||||
|
figure(classes = "post-edit-image") {}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
div("form-group") {
|
||||||
|
input(
|
||||||
|
name = "image",
|
||||||
|
classes = "form-btn",
|
||||||
|
type = InputType.file
|
||||||
|
) {
|
||||||
|
id = "image"
|
||||||
|
value = "Upload image"
|
||||||
|
accept =
|
||||||
|
Configuration.General
|
||||||
|
.allowedUploadExtensionSet
|
||||||
|
.joinToString(",") {
|
||||||
|
".$it"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
div("form-switch-group") {
|
div("form-switch-group") {
|
||||||
div("form-group form-switch") {
|
div("form-group form-switch") {
|
||||||
input(
|
input(
|
||||||
|
@ -318,8 +468,34 @@ fun Route.overview() {
|
||||||
|
|
||||||
post("/post/new") {
|
post("/post/new") {
|
||||||
authenticateOrRedirect(Permission.POST) { user ->
|
authenticateOrRedirect(Permission.POST) { user ->
|
||||||
val params = call.receiveParameters().toMap().mapValues { (_, list) ->
|
var imageUploadName: String? = null
|
||||||
list.firstOrNull()
|
|
||||||
|
val params = mutableMapOf<String, String>()
|
||||||
|
call.receiveMultipart().forEachPart { part ->
|
||||||
|
val name = part.name ?: return@forEachPart
|
||||||
|
when (part) {
|
||||||
|
is PartData.FormItem -> {
|
||||||
|
params[name] = part.value
|
||||||
|
}
|
||||||
|
is PartData.FileItem -> {
|
||||||
|
val extension = File(part.originalFileName).extension
|
||||||
|
|
||||||
|
if (extension.toLowerCase() !in Configuration.General.allowedUploadExtensionSet) return@forEachPart
|
||||||
|
|
||||||
|
var uploadName = Post.generateUrl() + "." + extension
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (Resources.existsUpload(uploadName)) {
|
||||||
|
uploadName = Post.generateUrl() + "." + extension
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Resources.createUpload(uploadName, part.streamProvider())
|
||||||
|
imageUploadName = uploadName
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val name = params["name"] ?: return@post
|
val name = params["name"] ?: return@post
|
||||||
|
@ -327,7 +503,7 @@ fun Route.overview() {
|
||||||
val url = params["url"] ?: return@post
|
val url = params["url"] ?: return@post
|
||||||
val pinned = params["pinned"] == "on"
|
val pinned = params["pinned"] == "on"
|
||||||
|
|
||||||
val post = Post(null, name, content, url, pinned)
|
val post = Post(null, name, content, url, imageUploadName, pinned)
|
||||||
|
|
||||||
PostRepository.create(post)
|
PostRepository.create(post)
|
||||||
|
|
||||||
|
|
|
@ -14,3 +14,6 @@ reference = "2019-03-27"
|
||||||
[security]
|
[security]
|
||||||
session_name = "SESSION"
|
session_name = "SESSION"
|
||||||
sign_key = "d1 20 23 8c 01 f8 f0 0d 9d 7c ff 68 21 97 75 31 38 3f fb 91 20 3a 8d 86 d4 e9 d8 50 f8 71 f1 dc"
|
sign_key = "d1 20 23 8c 01 f8 f0 0d 9d 7c ff 68 21 97 75 31 38 3f fb 91 20 3a 8d 86 d4 e9 d8 50 f8 71 f1 dc"
|
||||||
|
|
||||||
|
[general]
|
||||||
|
allowed_upload_extensions = "png, jpg, jpeg"
|
Loading…
Reference in a new issue