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"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven { url 'https://jitpack.io' }
|
||||
maven { url "https://dl.bintray.com/kotlin/ktor" }
|
||||
maven { url "https://dl.bintray.com/jetbrains/markdown" }
|
||||
maven { url "https://kotlin.bintray.com/kotlinx" }
|
||||
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 serialization_version = '0.11.0'
|
||||
def observable_version = '0.9.3'
|
||||
def klockVersion = "1.4.0"
|
||||
|
||||
kotlin {
|
||||
jvm() {
|
||||
|
@ -58,6 +60,8 @@ kotlin {
|
|||
dependencies {
|
||||
implementation kotlin('stdlib-common')
|
||||
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"
|
||||
}
|
||||
|
@ -89,6 +93,8 @@ kotlin {
|
|||
implementation 'org.mindrot:jbcrypt:0.4'
|
||||
|
||||
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.vladsch.flexmark:flexmark-all:0.42.10'
|
||||
|
@ -110,6 +116,8 @@ kotlin {
|
|||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serialization_version"
|
||||
|
||||
implementation "de.westermann:KObserve-js:$observable_version"
|
||||
implementation "com.soywiz:klock-js:$klockVersion"
|
||||
implementation "com.soywiz:klock-locale-js:$klockVersion"
|
||||
}
|
||||
}
|
||||
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 {
|
||||
val id : Long?
|
||||
fun createSearch(): SearchElement
|
||||
}
|
||||
|
||||
val createdAt: Long
|
||||
val updateAt: Long
|
||||
}
|
||||
|
|
|
@ -10,7 +10,10 @@ data class Post(
|
|||
val name: String,
|
||||
val content: 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 {
|
||||
|
||||
override fun createSearch() = SearchElement(
|
||||
|
@ -22,6 +25,7 @@ data class Post(
|
|||
companion object {
|
||||
private const val chars = "abcdefghijklmnopqrstuvwxyz"
|
||||
private const val length = 32
|
||||
|
||||
fun generateUrl() = (0 until length).asSequence()
|
||||
.map { Random.nextInt(chars.length) }
|
||||
.map { chars[it] }
|
||||
|
|
|
@ -12,7 +12,9 @@ data class Room(
|
|||
val internet: Boolean,
|
||||
val whiteboard: Boolean,
|
||||
val blackboard: Boolean,
|
||||
val accessible: Boolean
|
||||
val accessible: Boolean,
|
||||
override val createdAt: Long = 0,
|
||||
override val updateAt: Long = 0
|
||||
) : Model {
|
||||
|
||||
override fun createSearch() = SearchElement(
|
||||
|
|
|
@ -9,7 +9,9 @@ data class Schedule(
|
|||
val workGroup: WorkGroup,
|
||||
val room: Room,
|
||||
val day: Int,
|
||||
val time: Int
|
||||
val time: Int,
|
||||
override val createdAt: Long = 0,
|
||||
override val updateAt: Long = 0
|
||||
) : Model {
|
||||
|
||||
override fun createSearch() = SearchElement(
|
||||
|
|
|
@ -7,7 +7,9 @@ import kotlinx.serialization.Serializable
|
|||
data class Track(
|
||||
override val id: Long?,
|
||||
val name: String,
|
||||
val color: Color
|
||||
val color: Color,
|
||||
override val createdAt: Long = 0,
|
||||
override val updateAt: Long = 0
|
||||
) : Model {
|
||||
|
||||
override fun createSearch() = SearchElement(
|
||||
|
|
|
@ -8,7 +8,9 @@ data class User(
|
|||
override val id: Long?,
|
||||
val username: String,
|
||||
val password: String,
|
||||
val permissions: Set<Permission>
|
||||
val permissions: Set<Permission>,
|
||||
override val createdAt: Long = 0,
|
||||
override val updateAt: Long = 0
|
||||
) : Model {
|
||||
|
||||
fun checkPermission(permission: Permission): Boolean {
|
||||
|
|
|
@ -17,7 +17,9 @@ data class WorkGroup(
|
|||
val accessible: Boolean,
|
||||
val length: Int,
|
||||
val language: Language,
|
||||
val constraints: List<Constraint>
|
||||
val constraints: List<Constraint>,
|
||||
override val createdAt: Long = 0,
|
||||
override val updateAt: Long = 0
|
||||
) : Model {
|
||||
|
||||
override fun createSearch() = SearchElement(
|
||||
|
|
|
@ -5,9 +5,13 @@ import de.kif.frontend.launch
|
|||
import de.kif.frontend.repository.PostRepository
|
||||
import de.westermann.kobserve.event.subscribe
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import org.w3c.dom.HTMLTextAreaElement
|
||||
import org.w3c.dom.events.EventListener
|
||||
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.dom.clear
|
||||
|
||||
|
@ -50,6 +54,7 @@ fun initPosts() {
|
|||
}
|
||||
|
||||
fun initPostEdit() {
|
||||
// Content preview
|
||||
val textArea = document.getElementById("content") as HTMLTextAreaElement
|
||||
val preview = document.getElementsByClassName("post-edit-right")[0] as HTMLElement
|
||||
|
||||
|
@ -67,4 +72,45 @@ fun initPostEdit() {
|
|||
launch {
|
||||
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
|
||||
|
||||
import de.kif.common.formatDate
|
||||
import de.kif.frontend.launch
|
||||
import de.kif.frontend.repository.PostRepository
|
||||
import de.westermann.kobserve.event.emit
|
||||
|
@ -25,37 +26,44 @@ class PostView(
|
|||
}
|
||||
|
||||
private val nameView: Link
|
||||
private val contentView: View
|
||||
private val contentView: HTMLElement
|
||||
private val footerView: HTMLElement
|
||||
private val imageView: HTMLElement
|
||||
|
||||
private fun reload() {
|
||||
launch {
|
||||
val p = PostRepository.get(postId) ?: return@launch
|
||||
|
||||
classList["post-no-image"] = p.image == null
|
||||
|
||||
nameView.text = p.name
|
||||
nameView.target = "/p/${p.url}"
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val nameHtml = view.getElementsByClassName("post-name")[0]
|
||||
nameView = nameHtml?.let { Link.wrap(it as HTMLAnchorElement) } ?: Link().also {
|
||||
html.appendChild(it.html)
|
||||
it.classList += "post-name"
|
||||
}
|
||||
nameView = Link.wrap(view.getByClassOrCreate("post-name"))
|
||||
|
||||
// val editHtml = view.getElementsByClassName("post-edit")[0]
|
||||
// editView = editHtml?.let { Link.wrap(it as HTMLAnchorElement) } ?: Link()
|
||||
val postColumn = view.getByClassOrCreate<HTMLElement>("post-column")
|
||||
val postColumnLeft = postColumn.getByClassOrCreate<HTMLElement>("post-column-left")
|
||||
val postColumnRight = postColumn.getByClassOrCreate<HTMLElement>("post-column-right")
|
||||
|
||||
val contentHtml = view.getElementsByClassName("post-content")[0]
|
||||
contentView = contentHtml?.let { wrap(it as HTMLElement) } ?: wrap(createHtmlView()).also {
|
||||
html.appendChild(it.html)
|
||||
it.classList += "post-content"
|
||||
}
|
||||
imageView = postColumnLeft.getByClassOrCreate("post-image", "figure")
|
||||
|
||||
contentView = postColumnRight.getByClassOrCreate("post-content")
|
||||
footerView = postColumnRight.getByClassOrCreate("post-footer")
|
||||
|
||||
PostRepository.onUpdate {
|
||||
if (it == postId) {
|
||||
|
@ -69,7 +77,7 @@ class PostView(
|
|||
|
||||
companion object {
|
||||
fun create(postId: Long): PostView {
|
||||
val div = document.createElement("div") as HTMLElement
|
||||
val div = createHtmlView<HTMLElement>()
|
||||
div.classList.add("post")
|
||||
div.dataset["id"] = postId.toString()
|
||||
return PostView(div).also(PostView::reload)
|
||||
|
@ -78,3 +86,16 @@ class PostView(
|
|||
}
|
||||
|
||||
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;
|
||||
|
||||
$text-primary-color: #333;
|
||||
$text-secondary-color: rgba($text-primary-color, 0.5);
|
||||
|
||||
$primary-color: #B11D33;
|
||||
$primary-text-color: #fff;
|
||||
|
@ -18,7 +19,9 @@ $primary-text-color: #fff;
|
|||
$error-color: #D55225;
|
||||
$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;
|
||||
|
||||
|
@ -40,6 +43,10 @@ body, html {
|
|||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
& > *:last-child {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.no-select {
|
||||
|
@ -128,7 +135,7 @@ a {
|
|||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($primary-text-color, 0.1);
|
||||
background-color: $table-border-color;
|
||||
|
||||
&::after {
|
||||
background: $primary-color;
|
||||
|
@ -165,8 +172,8 @@ a {
|
|||
position: absolute;
|
||||
background-color: $background-secondary-color;
|
||||
z-index: 5;
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
|
@ -183,7 +190,11 @@ a {
|
|||
line-height: 3rem;
|
||||
|
||||
&:after {
|
||||
bottom: 0.2rem;
|
||||
bottom: 0.55rem;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -220,7 +231,7 @@ a {
|
|||
}
|
||||
|
||||
.main {
|
||||
padding: 0 1rem;
|
||||
//padding: 0 1rem;
|
||||
}
|
||||
|
||||
.table-layout-search {
|
||||
|
@ -252,14 +263,14 @@ a {
|
|||
}
|
||||
|
||||
tr {
|
||||
border-top: solid 1px rgba($text-primary-color, 0.1);
|
||||
border-top: solid 1px $table-border-color;
|
||||
|
||||
&:nth-child(odd) {
|
||||
//background-color: rgba($text-primary-color, 0.01);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
background-color: rgba($text-primary-color, 0.06);
|
||||
background-color: $table-header-color;
|
||||
height: 2.5rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
|
@ -269,7 +280,7 @@ a {
|
|||
line-height: 2rem;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($text-primary-color, 0.06);
|
||||
background-color: $table-header-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -292,20 +303,20 @@ a {
|
|||
z-index: 1;
|
||||
background: $background-primary-color;
|
||||
width: 100%;
|
||||
border: solid 1px rgba($text-primary-color, 0.1);
|
||||
border: solid 1px $table-border-color;
|
||||
|
||||
span {
|
||||
padding: 0 0.5rem;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($text-primary-color, 0.06);
|
||||
background-color: $table-header-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border: solid 1px $border-color;
|
||||
border: solid 1px $input-border-color;
|
||||
outline: none;
|
||||
padding: 0 1rem;
|
||||
line-height: 2.5rem;
|
||||
|
@ -412,7 +423,7 @@ select:-moz-focusring {
|
|||
}
|
||||
|
||||
.form-btn {
|
||||
border: solid 1px $border-color;
|
||||
border: solid 1px $input-border-color;
|
||||
outline: none;
|
||||
padding: 0 1rem;
|
||||
line-height: 2rem;
|
||||
|
@ -588,7 +599,7 @@ form {
|
|||
.calendar[data-editable = "true"].edit {
|
||||
.calendar-table {
|
||||
width: calc(100% - 16rem);
|
||||
border-right: solid 1px rgba($text-primary-color, 0.1);
|
||||
border-right: solid 1px $table-border-color;
|
||||
}
|
||||
|
||||
.calendar-edit-main {
|
||||
|
@ -608,8 +619,8 @@ form {
|
|||
display: none;
|
||||
z-index: 10;
|
||||
|
||||
border: solid 1px $border-color;
|
||||
box-shadow: 0 0.1rem 0.2rem rgba($primary-text-color);
|
||||
border: solid 1px $input-border-color;
|
||||
box-shadow: 0 0.1rem 0.2rem $primary-text-color;
|
||||
|
||||
a {
|
||||
padding: 0.2rem;
|
||||
|
@ -707,14 +718,14 @@ form {
|
|||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-left: solid 1px rgba($text-primary-color, 0.1);
|
||||
border-left: solid 1px $table-border-color;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-row {
|
||||
border-top: solid 1px rgba($text-primary-color, 0.1);
|
||||
border-top: solid 1px $table-border-color;
|
||||
line-height: 3rem;
|
||||
height: 3rem;
|
||||
|
||||
|
@ -727,12 +738,12 @@ form {
|
|||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-left: solid 1px rgba($text-primary-color, 0.1);
|
||||
border-left: solid 1px $table-border-color;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($text-primary-color, 0.06);
|
||||
background-color: $table-header-color;
|
||||
}
|
||||
|
||||
.calendar-entry {
|
||||
|
@ -749,7 +760,7 @@ form {
|
|||
width: 6rem;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
border-right: solid 1px rgba($text-primary-color, 0.1);
|
||||
border-right: solid 1px $table-border-color;
|
||||
}
|
||||
|
||||
.calendar-link {
|
||||
|
@ -780,7 +791,7 @@ form {
|
|||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-left: solid 1px rgba($text-primary-color, 0.1);
|
||||
border-left: solid 1px $table-border-color;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
@ -800,12 +811,12 @@ form {
|
|||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-left: solid 1px rgba($text-primary-color, 0.1);
|
||||
border-left: solid 1px $table-border-color;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($text-primary-color, 0.06);
|
||||
background-color: $table-header-color;
|
||||
}
|
||||
|
||||
.calendar-entry {
|
||||
|
@ -826,7 +837,7 @@ form {
|
|||
}
|
||||
|
||||
&: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;
|
||||
|
||||
background: $background-primary-color;
|
||||
border: solid 1px rgba($text-primary-color, 0.1);
|
||||
border: solid 1px $table-border-color;
|
||||
|
||||
span {
|
||||
padding: 0 0.5rem;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($text-primary-color, 0.06);
|
||||
background-color: $table-header-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -956,11 +967,13 @@ form {
|
|||
|
||||
.overview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.overview-main {
|
||||
flex-grow: 4;
|
||||
margin-right: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.overview-side {
|
||||
|
@ -985,6 +998,7 @@ form {
|
|||
font-size: 1.2rem;
|
||||
color: $primary-color;
|
||||
line-height: 2rem;
|
||||
padding: 0 1rem;
|
||||
|
||||
&:empty::before {
|
||||
display: block;
|
||||
|
@ -999,11 +1013,30 @@ form {
|
|||
.post-edit {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
right: 1rem;
|
||||
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 {
|
||||
padding: 0 1rem;
|
||||
|
||||
h1, h2, h3, h4, h5, h6, p {
|
||||
margin: 0.7rem 0;
|
||||
padding: 0;
|
||||
|
@ -1039,11 +1072,11 @@ form {
|
|||
}
|
||||
|
||||
td {
|
||||
border-top: solid 1px rgba($text-primary-color, 0.1);
|
||||
border-top: solid 1px $table-border-color;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: rgba($text-primary-color, 0.06);
|
||||
background-color: $table-header-color;
|
||||
}
|
||||
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -1060,29 +1101,84 @@ form {
|
|||
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) {
|
||||
.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-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);
|
||||
.overview {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -34,6 +34,10 @@ fun Application.main() {
|
|||
files(Configuration.Path.webPath.toFile())
|
||||
}
|
||||
|
||||
static("/images") {
|
||||
files(Configuration.Path.uploadsPath.toFile())
|
||||
}
|
||||
|
||||
// UI routes
|
||||
overview()
|
||||
calendar()
|
||||
|
|
|
@ -71,17 +71,30 @@ object Configuration {
|
|||
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 {
|
||||
var config = Config {
|
||||
addSpec(ServerSpec)
|
||||
addSpec(PathSpec)
|
||||
addSpec(ScheduleSpec)
|
||||
addSpec(SecuritySpec)
|
||||
addSpec(GeneralSpec)
|
||||
}.from.toml.resource("portal.toml")
|
||||
|
||||
try {
|
||||
config = config.from.toml.file("portal.toml")
|
||||
} catch (_: FileNotFoundException) { }
|
||||
} catch (_: FileNotFoundException) {
|
||||
}
|
||||
|
||||
this.config = config.from.env()
|
||||
.from.systemProperties()
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
package de.kif.backend
|
||||
|
||||
import mu.KotlinLogging
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.io.InputStream
|
||||
import java.nio.file.*
|
||||
|
||||
/**
|
||||
|
@ -52,6 +51,18 @@ object Resources {
|
|||
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.
|
||||
*/
|
||||
|
|
|
@ -8,6 +8,9 @@ object DbTrack : Table() {
|
|||
val id = long("id").autoIncrement().primaryKey()
|
||||
val name = varchar("name", 64)
|
||||
val color = varchar("color", 32)
|
||||
|
||||
val createdAt = long("createdAt")
|
||||
val updatedAt = long("updatedAt")
|
||||
}
|
||||
|
||||
object DbWorkGroup : Table() {
|
||||
|
@ -28,6 +31,9 @@ object DbWorkGroup : Table() {
|
|||
|
||||
val length = integer("length")
|
||||
val constraints = text("constraints")
|
||||
|
||||
val createdAt = long("createdAt")
|
||||
val updatedAt = long("updatedAt")
|
||||
}
|
||||
|
||||
object DbRoom : Table() {
|
||||
|
@ -41,6 +47,9 @@ object DbRoom : Table() {
|
|||
val whiteboard = bool("whiteboard")
|
||||
val blackboard = bool("blackboard")
|
||||
val accessible = bool("accessible")
|
||||
|
||||
val createdAt = long("createdAt")
|
||||
val updatedAt = long("updatedAt")
|
||||
}
|
||||
|
||||
object DbSchedule : Table() {
|
||||
|
@ -49,12 +58,18 @@ object DbSchedule : Table() {
|
|||
val roomId = long("room_id").index()
|
||||
val day = integer("day").index()
|
||||
val time = integer("time_slot")
|
||||
|
||||
val createdAt = long("createdAt")
|
||||
val updatedAt = long("updatedAt")
|
||||
}
|
||||
|
||||
object DbUser : Table() {
|
||||
val id = long("id").autoIncrement().primaryKey()
|
||||
val username = varchar("username", 64).uniqueIndex()
|
||||
val password = varchar("password", 64)
|
||||
|
||||
val createdAt = long("createdAt")
|
||||
val updatedAt = long("updatedAt")
|
||||
}
|
||||
|
||||
object DbUserPermission : Table() {
|
||||
|
@ -68,5 +83,9 @@ object DbPost : Table() {
|
|||
|
||||
val content = text("content")
|
||||
val url = varchar("url", 64).uniqueIndex()
|
||||
val image = varchar("image", 64).nullable()
|
||||
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 kotlinx.coroutines.runBlocking
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import java.util.Date
|
||||
|
||||
object PostRepository : Repository<Post> {
|
||||
|
||||
|
@ -22,9 +23,13 @@ object PostRepository : Repository<Post> {
|
|||
val name = row[DbPost.name]
|
||||
val content = row[DbPost.content]
|
||||
val url = row[DbPost.url]
|
||||
val image = row[DbPost.image]
|
||||
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? {
|
||||
|
@ -40,6 +45,8 @@ object PostRepository : Repository<Post> {
|
|||
}
|
||||
}
|
||||
|
||||
val now = Date().time
|
||||
|
||||
return dbQuery {
|
||||
if (model.pinned) {
|
||||
DbPost.update({ DbPost.pinned eq true }) {
|
||||
|
@ -51,7 +58,11 @@ object PostRepository : Repository<Post> {
|
|||
it[name] = model.name
|
||||
it[content] = model.content
|
||||
it[url] = model.url
|
||||
it[image] = model.image
|
||||
it[pinned] = model.pinned
|
||||
|
||||
it[createdAt] = now
|
||||
it[updatedAt] = now
|
||||
}[DbPost.id] ?: throw IllegalStateException("Cannot create model!")
|
||||
|
||||
onCreate.emit(id)
|
||||
|
@ -71,12 +82,17 @@ object PostRepository : Repository<Post> {
|
|||
}
|
||||
}
|
||||
|
||||
val now = Date().time
|
||||
|
||||
dbQuery {
|
||||
DbPost.update({ DbPost.id eq model.id }) {
|
||||
it[name] = model.name
|
||||
it[content] = model.content
|
||||
it[url] = model.url
|
||||
it[image] = model.image
|
||||
it[pinned] = model.pinned
|
||||
|
||||
it[updatedAt] = now
|
||||
}
|
||||
|
||||
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 {
|
||||
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 kotlinx.coroutines.runBlocking
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import java.util.Date
|
||||
|
||||
object RoomRepository : Repository<Room> {
|
||||
|
||||
|
@ -25,7 +26,21 @@ object RoomRepository : Repository<Room> {
|
|||
val blackboard = row[DbRoom.blackboard]
|
||||
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? {
|
||||
|
@ -35,6 +50,8 @@ object RoomRepository : Repository<Room> {
|
|||
}
|
||||
|
||||
override suspend fun create(model: Room): Long {
|
||||
val now = Date().time
|
||||
|
||||
return dbQuery {
|
||||
val id = DbRoom.insert {
|
||||
it[name] = model.name
|
||||
|
@ -44,6 +61,8 @@ object RoomRepository : Repository<Room> {
|
|||
it[whiteboard] = model.whiteboard
|
||||
it[blackboard] = model.blackboard
|
||||
it[accessible] = model.accessible
|
||||
it[createdAt] = now
|
||||
it[updatedAt] = now
|
||||
}[DbRoom.id] ?: throw IllegalStateException("Cannot create model!")
|
||||
|
||||
onCreate.emit(id)
|
||||
|
@ -54,6 +73,9 @@ object RoomRepository : Repository<Room> {
|
|||
|
||||
override suspend fun update(model: Room) {
|
||||
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
|
||||
|
||||
val now = Date().time
|
||||
|
||||
dbQuery {
|
||||
DbRoom.update({ DbRoom.id eq model.id }) {
|
||||
it[name] = model.name
|
||||
|
@ -63,6 +85,7 @@ object RoomRepository : Repository<Room> {
|
|||
it[whiteboard] = model.whiteboard
|
||||
it[blackboard] = model.blackboard
|
||||
it[accessible] = model.accessible
|
||||
it[updatedAt] = now
|
||||
}
|
||||
|
||||
onUpdate.emit(model.id)
|
||||
|
|
|
@ -8,6 +8,7 @@ import de.kif.common.model.Schedule
|
|||
import de.westermann.kobserve.event.EventHandler
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import java.util.Date
|
||||
|
||||
object ScheduleRepository : Repository<Schedule> {
|
||||
|
||||
|
@ -22,12 +23,15 @@ object ScheduleRepository : Repository<Schedule> {
|
|||
val day = row[DbSchedule.day]
|
||||
val time = row[DbSchedule.time]
|
||||
|
||||
val createdAt = row[DbSchedule.createdAt]
|
||||
val updatedAt = row[DbSchedule.updatedAt]
|
||||
|
||||
val workGroup = WorkGroupRepository.get(workGroupId)
|
||||
?: throw IllegalStateException("Work group for schedule does not exist!")
|
||||
val room = RoomRepository.get(roomId)
|
||||
?: 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? {
|
||||
|
@ -41,12 +45,17 @@ object ScheduleRepository : Repository<Schedule> {
|
|||
}
|
||||
|
||||
override suspend fun create(model: Schedule): Long {
|
||||
val now = Date().time
|
||||
|
||||
return dbQuery {
|
||||
val id = DbSchedule.insert {
|
||||
it[workGroupId] = model.workGroup.id ?: throw IllegalArgumentException("Work group does not exist!")
|
||||
it[roomId] = model.room.id ?: throw IllegalArgumentException("Room does not exist!")
|
||||
it[day] = model.day
|
||||
it[time] = model.time
|
||||
|
||||
it[createdAt] = now
|
||||
it[updatedAt] = now
|
||||
}[DbSchedule.id] ?: throw IllegalStateException("Cannot create model!")
|
||||
|
||||
onCreate.emit(id)
|
||||
|
@ -57,12 +66,17 @@ object ScheduleRepository : Repository<Schedule> {
|
|||
|
||||
override suspend fun update(model: Schedule) {
|
||||
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
|
||||
|
||||
val now = Date().time
|
||||
|
||||
dbQuery {
|
||||
DbSchedule.update({ DbSchedule.id eq model.id }) {
|
||||
it[workGroupId] = model.workGroup.id ?: throw IllegalArgumentException("Work group does not exist!")
|
||||
it[roomId] = model.room.id ?: throw IllegalArgumentException("Room does not exist!")
|
||||
it[day] = model.day
|
||||
it[time] = model.time
|
||||
|
||||
it[updatedAt] = now
|
||||
}
|
||||
|
||||
onUpdate.emit(model.id)
|
||||
|
|
|
@ -11,6 +11,7 @@ import de.kif.common.model.parseColor
|
|||
import de.westermann.kobserve.event.EventHandler
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import java.util.Date
|
||||
|
||||
object TrackRepository : Repository<Track> {
|
||||
|
||||
|
@ -23,7 +24,10 @@ object TrackRepository : Repository<Track> {
|
|||
val name = row[DbTrack.name]
|
||||
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? {
|
||||
|
@ -33,10 +37,15 @@ object TrackRepository : Repository<Track> {
|
|||
}
|
||||
|
||||
override suspend fun create(model: Track): Long {
|
||||
val now = Date().time
|
||||
|
||||
return dbQuery {
|
||||
val id = DbTrack.insert {
|
||||
it[name] = model.name
|
||||
it[color] = model.color.toString()
|
||||
|
||||
it[createdAt] = now
|
||||
it[updatedAt] = now
|
||||
}[DbTrack.id] ?: throw IllegalStateException("Cannot create model!")
|
||||
|
||||
onCreate.emit(id)
|
||||
|
@ -47,10 +56,15 @@ object TrackRepository : Repository<Track> {
|
|||
|
||||
override suspend fun update(model: Track) {
|
||||
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
|
||||
|
||||
val now = Date().time
|
||||
|
||||
dbQuery {
|
||||
DbTrack.update({ DbTrack.id eq model.id }) {
|
||||
it[name] = model.name
|
||||
it[color] = model.color.toString()
|
||||
|
||||
it[updatedAt] = now
|
||||
}
|
||||
|
||||
onUpdate.emit(model.id)
|
||||
|
|
|
@ -11,6 +11,7 @@ import de.kif.common.model.User
|
|||
import de.westermann.kobserve.event.EventHandler
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import java.util.Date
|
||||
|
||||
object UserRepository : Repository<User> {
|
||||
|
||||
|
@ -23,11 +24,14 @@ object UserRepository : Repository<User> {
|
|||
val username = row[DbUser.username]
|
||||
val password = row[DbUser.password]
|
||||
|
||||
val createdAt = row[DbUser.createdAt]
|
||||
val updatedAt = row[DbUser.updatedAt]
|
||||
|
||||
val permissions = DbUserPermission.slice(DbUserPermission.permission).select {
|
||||
DbUserPermission.userId eq id
|
||||
}.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? {
|
||||
|
@ -37,10 +41,15 @@ object UserRepository : Repository<User> {
|
|||
}
|
||||
|
||||
override suspend fun create(model: User): Long {
|
||||
val now = Date().time
|
||||
|
||||
return dbQuery {
|
||||
val id = DbUser.insert {
|
||||
it[username] = model.username
|
||||
it[password] = model.password
|
||||
|
||||
it[createdAt] = now
|
||||
it[updatedAt] = now
|
||||
}[DbUser.id] ?: throw IllegalStateException("Cannot create model!")
|
||||
|
||||
for (permission in model.permissions) {
|
||||
|
@ -58,10 +67,15 @@ object UserRepository : Repository<User> {
|
|||
|
||||
override suspend fun update(model: User) {
|
||||
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
|
||||
|
||||
val now = Date().time
|
||||
|
||||
dbQuery {
|
||||
DbUser.update({ DbUser.id eq model.id }) {
|
||||
it[username] = model.username
|
||||
it[password] = model.password
|
||||
|
||||
it[updatedAt] = now
|
||||
}
|
||||
|
||||
DbUserPermission.deleteWhere { DbUserPermission.userId eq model.id }
|
||||
|
|
|
@ -13,6 +13,7 @@ import de.westermann.kobserve.event.EventHandler
|
|||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.list
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import java.util.Date
|
||||
|
||||
object WorkGroupRepository : Repository<WorkGroup> {
|
||||
|
||||
|
@ -35,6 +36,9 @@ object WorkGroupRepository : Repository<WorkGroup> {
|
|||
val language = row[DbWorkGroup.language]
|
||||
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) }
|
||||
|
||||
return WorkGroup(
|
||||
|
@ -50,7 +54,9 @@ object WorkGroupRepository : Repository<WorkGroup> {
|
|||
accessible,
|
||||
length,
|
||||
language,
|
||||
constraints
|
||||
constraints,
|
||||
createdAt,
|
||||
updatedAt
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -65,6 +71,8 @@ object WorkGroupRepository : Repository<WorkGroup> {
|
|||
}
|
||||
|
||||
override suspend fun create(model: WorkGroup): Long {
|
||||
val now = Date().time
|
||||
|
||||
return dbQuery {
|
||||
val id = DbWorkGroup.insert {
|
||||
it[name] = model.name
|
||||
|
@ -79,6 +87,9 @@ object WorkGroupRepository : Repository<WorkGroup> {
|
|||
it[length] = model.length
|
||||
it[language] = model.language
|
||||
it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints)
|
||||
|
||||
it[createdAt] = now
|
||||
it[updatedAt] = now
|
||||
}[DbWorkGroup.id] ?: throw IllegalStateException("Cannot create model!")
|
||||
|
||||
onCreate.emit(id)
|
||||
|
@ -89,6 +100,9 @@ object WorkGroupRepository : Repository<WorkGroup> {
|
|||
|
||||
override suspend fun update(model: WorkGroup) {
|
||||
if (model.id == null) throw IllegalStateException("Cannot update model which was not created!")
|
||||
|
||||
val now = Date().time
|
||||
|
||||
dbQuery {
|
||||
DbWorkGroup.update({ DbWorkGroup.id eq model.id }) {
|
||||
it[name] = model.name
|
||||
|
@ -103,6 +117,8 @@ object WorkGroupRepository : Repository<WorkGroup> {
|
|||
it[length] = model.length
|
||||
it[language] = model.language
|
||||
it[constraints] = Message.json.stringify(Constraint.serializer().list, model.constraints)
|
||||
|
||||
it[updatedAt] = now
|
||||
}
|
||||
|
||||
onUpdate.emit(model.id)
|
||||
|
|
|
@ -1,30 +1,39 @@
|
|||
package de.kif.backend.route
|
||||
|
||||
import de.kif.backend.Configuration
|
||||
import de.kif.backend.Resources
|
||||
import de.kif.backend.authenticateOrRedirect
|
||||
import de.kif.backend.isAuthenticated
|
||||
import de.kif.backend.repository.PostRepository
|
||||
import de.kif.backend.util.markdownToHtml
|
||||
import de.kif.backend.view.MainTemplate
|
||||
import de.kif.backend.view.MenuTemplate
|
||||
import de.kif.common.formatDate
|
||||
import de.kif.common.model.Permission
|
||||
import de.kif.common.model.Post
|
||||
import io.ktor.application.call
|
||||
import io.ktor.html.respondHtmlTemplate
|
||||
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.respondRedirect
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.routing.get
|
||||
import io.ktor.routing.post
|
||||
import io.ktor.util.toMap
|
||||
import kotlinx.html.*
|
||||
import java.io.File
|
||||
|
||||
fun DIV.createPost(post: Post, editable: Boolean = false, additionalClasses: String = "") {
|
||||
var classes = "post"
|
||||
if (additionalClasses.isNotBlank()) {
|
||||
classes += " $additionalClasses"
|
||||
}
|
||||
if (post.image == null) {
|
||||
classes += " post-no-image"
|
||||
}
|
||||
div(classes) {
|
||||
attributes["data-id"] = post.id.toString()
|
||||
attributes["data-pinned"] = post.pinned.toString()
|
||||
|
@ -37,9 +46,24 @@ fun DIV.createPost(post: Post, editable: Boolean = false, additionalClasses: Str
|
|||
i("material-icons") { +"edit" }
|
||||
}
|
||||
}
|
||||
div("post-content") {
|
||||
unsafe {
|
||||
raw(markdownToHtml(post.content))
|
||||
|
||||
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") {
|
||||
unsafe {
|
||||
raw(markdownToHtml(post.content))
|
||||
}
|
||||
}
|
||||
div("post-footer") {
|
||||
+formatDate(post.createdAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -118,7 +142,7 @@ fun Route.overview() {
|
|||
h1 { +"Edit post" }
|
||||
div("post-edit-container") {
|
||||
div("post-edit-left") {
|
||||
form(method = FormMethod.post) {
|
||||
form(method = FormMethod.post, encType = FormEncType.multipartFormData) {
|
||||
div("form-group") {
|
||||
label {
|
||||
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-group form-switch") {
|
||||
input(
|
||||
|
@ -208,9 +280,37 @@ fun Route.overview() {
|
|||
post("/post/{id}") {
|
||||
authenticateOrRedirect(Permission.POST) { user ->
|
||||
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
|
||||
|
||||
params["name"]?.let { post = post.copy(name = it) }
|
||||
|
@ -218,6 +318,28 @@ fun Route.overview() {
|
|||
params["content"]?.let { post = post.copy(content = it) }
|
||||
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)
|
||||
|
||||
call.respondRedirect("/")
|
||||
|
@ -235,7 +357,7 @@ fun Route.overview() {
|
|||
h1 { +"Create post" }
|
||||
div("post-edit-container") {
|
||||
div("post-edit-left") {
|
||||
form(method = FormMethod.post) {
|
||||
form(method = FormMethod.post, encType = FormEncType.multipartFormData) {
|
||||
div("form-group") {
|
||||
label {
|
||||
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-group form-switch") {
|
||||
input(
|
||||
|
@ -318,8 +468,34 @@ fun Route.overview() {
|
|||
|
||||
post("/post/new") {
|
||||
authenticateOrRedirect(Permission.POST) { user ->
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val name = params["name"] ?: return@post
|
||||
|
@ -327,7 +503,7 @@ fun Route.overview() {
|
|||
val url = params["url"] ?: return@post
|
||||
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)
|
||||
|
||||
|
|
|
@ -14,3 +14,6 @@ reference = "2019-03-27"
|
|||
[security]
|
||||
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"
|
||||
|
||||
[general]
|
||||
allowed_upload_extensions = "png, jpg, jpeg"
|
Loading…
Reference in a new issue