Merge http4k

This commit is contained in:
2020-08-13 19:37:39 +02:00
parent b41b2103f0
commit 24aabd494e
176 changed files with 4965 additions and 8607 deletions
+18
View File
@@ -0,0 +1,18 @@
package be.simplenotes.app.views
import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.domain.security.JwtPayload
import kotlinx.html.div
import kotlinx.html.h1
class BaseView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) {
fun renderHome(jwtPayload: JwtPayload?) = renderPage(title = "Home", jwtPayload = jwtPayload) {
div("centered container mx-auto flex justify-center items-center") {
div("bg-gray-800 md:w-1/3 w-full rounded-lg m-4 p-6 text-center") {
h1("text-3xl") { +"SimpleNotes" }
div("text-teal-400") { +"Welcome" }
div("text-gray-200") { +"TODO" }
}
}
}
}
+131
View File
@@ -0,0 +1,131 @@
package be.simplenotes.app.views
import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.app.views.components.Alert
import be.simplenotes.app.views.components.alert
import be.simplenotes.app.views.components.submitButton
import be.simplenotes.domain.model.PersistedNote
import be.simplenotes.domain.model.PersistedNoteMetadata
import be.simplenotes.domain.security.JwtPayload
import io.konform.validation.ValidationError
import kotlinx.html.*
class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) {
fun noteEditor(
jwtPayload: JwtPayload,
error: String? = null,
textarea: String? = null,
validationErrors: List<ValidationError> = emptyList(),
) = renderPage(title = "New note", jwtPayload = jwtPayload) {
div("container mx-auto p-4") {
// TODO: error
error?.let { alert(Alert.Warning, error) }
validationErrors.forEach {
alert(Alert.Warning, it.dataPath.substringAfter('.') + ": " + it.message)
}
form(method = FormMethod.post) {
textArea(classes = "w-full bg-gray-800 p-5 outline-none font-mono") {
attributes.also {
it["rows"] = "20"
it["id"] = "markdown"
it["name"] = "markdown"
it["aria-label"] = "markdown text area"
it["spellcheck"] = "false"
}
textarea?.let {
+it
} ?: +"""
|---
|title: ''
|tags: []
|---
|
""".trimMargin("|")
}
submitButton("Save")
}
}
}
fun notes(jwtPayload: JwtPayload, notes: List<PersistedNoteMetadata>, currentPage: Int, numberOfPages: Int) =
renderPage(title = "Notes", jwtPayload = jwtPayload) {
div("container mx-auto p-4") {
div("flex justify-between mb-4") {
h1("text-2xl underline") { +"Notes" }
a(
href = "/notes/new",
classes = "text-gray-800 bg-green-500 hover:bg-green-700 " +
"inline ml-2 text-md font-semibold rounded px-4 py-2"
) { +"New" }
}
if (notes.isNotEmpty()) {
ul {
notes.forEach { (title, tags, _, uuid) ->
li("flex justify-between") {
a(classes = "text-blue-200 text-xl hover:underline", href = "/notes/${uuid}") {
+title
}
span {
tags.forEach {
span("tag ml-2") { +"#$it" }
}
}
}
}
}
if (numberOfPages > 1)
pagination(currentPage, numberOfPages)
} else
span { +"No notes yet" } // FIXME if too far in pagination, it it displayed
}
}
private fun DIV.pagination(currentPage: Int, numberOfPages: Int) {
val links = mutableListOf<Pair<String, String>>()
//if (currentPage > 1) links += "Previous" to "?page=${currentPage - 1}"
links += (1..numberOfPages).map { "$it" to "?page=$it" }
//if (currentPage < numberOfPages) links += "Next" to "?page=${currentPage + 1}"
nav("pages") {
links.forEach { (name, href) ->
a(href, classes = if (name == currentPage.toString()) "active" else null) { +name }
}
}
}
fun renderedNote(jwtPayload: JwtPayload, note: PersistedNote) = renderPage(note.meta.title, jwtPayload = jwtPayload) {
div("container mx-auto p-4") {
div("flex items-center justify-between mb-4") {
h1("text-3xl fond-bold underline") { +note.meta.title }
span {
note.meta.tags.forEach {
span("tag ml-2") { +"#$it" }
}
}
}
span("flex justify-end mb-4") {
a(
href = "/notes/${note.uuid}/edit",
classes = "mx-2 bg-teal-500 hover:bg-teal-600 focus:bg-teal-600" +
" focus:outline-none text-white font-bold py-2 px-4 rounded"
) { +"Edit" }
form(method = FormMethod.post, classes = "inline") {
button(
type = ButtonType.submit,
name = "delete",
classes = "mx-2 bg-red-500 hover:bg-red-600 focus:bg-red-600" +
" focus:outline-none text-white font-bold py-2 px-4 rounded"
) { +"Delete" }
}
}
div {
attributes["id"] = "note"
unsafe {
+note.html
}
}
}
}
}
+73
View File
@@ -0,0 +1,73 @@
package be.simplenotes.app.views
import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.app.views.components.Alert
import be.simplenotes.app.views.components.alert
import be.simplenotes.app.views.components.input
import be.simplenotes.app.views.components.submitButton
import be.simplenotes.domain.security.JwtPayload
import io.konform.validation.ValidationError
import kotlinx.html.*
class UserView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) {
fun register(
jwtPayload: JwtPayload?,
error: String? = null,
validationErrors: List<ValidationError> = emptyList(),
) = accountForm("Register", jwtPayload, error, validationErrors, "Create an account", "Register") {
+"Already have an account? "
a(href = "/login", classes = "no-underline text-blue-500 font-bold") { +"Sign In" }
}
fun login(
jwtPayload: JwtPayload?,
error: String? = null,
validationErrors: List<ValidationError> = emptyList(),
new: Boolean = false,
) = accountForm("Login", jwtPayload, error, validationErrors, "Sign In", "Sign In", new) {
+"Don't have an account yet? "
a(href = "/register", classes = "no-underline text-blue-500 font-bold") { +"Create an account" }
}
private fun accountForm(
title: String,
jwtPayload: JwtPayload?,
error: String? = null,
validationErrors: List<ValidationError> = emptyList(),
h1: String,
submit: String,
new: Boolean = false,
footer: FlowContent.() -> Unit,
) = renderPage(title = title, jwtPayload = jwtPayload) {
div("centered container mx-auto flex justify-center items-center") {
div("w-full md:w-1/2 lg:w-1/3 m-4") {
h1("font-semibold text-lg mb-6 text-center") { +h1 }
div("p-8 mb-6") {
if (new) alert(Alert.Success, "Your account has been created")
error?.let { alert(Alert.Warning, error) }
form(method = FormMethod.post) {
input(
id = "username",
placeholder = "Username",
autoComplete = "username",
error = validationErrors.find { it.dataPath == ".username" }?.message
)
input(
id = "password",
placeholder = "Password",
autoComplete = "new-password",
type = InputType.password,
error = validationErrors.find { it.dataPath == ".password" }?.message
)
submitButton(submit)
}
}
div("text-center") {
p("text-gray-200 text-sm") {
footer()
}
}
}
}
}
}
+36
View File
@@ -0,0 +1,36 @@
package be.simplenotes.app.views
import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.app.views.components.navbar
import be.simplenotes.domain.security.JwtPayload
import kotlinx.html.*
import kotlinx.html.stream.appendHTML
abstract class View(private val staticFileResolver: StaticFileResolver) {
private val styles = staticFileResolver.resolve("styles.css")!!
fun renderPage(
title: String,
description: String? = null,
jwtPayload: JwtPayload?,
body: BODY.() -> Unit = {},
) = buildString {
appendLine("<!DOCTYPE html>")
appendHTML().html {
attributes["lang"] = "en"
head {
meta(charset = "UTF-8")
meta(name = "viewport", content = "width=device-width, initial-scale=1")
title("$title - SimpleNotes")
description?.let { meta(name = "description", content = it) }
link(rel = "stylesheet", href = styles)
link(rel = "shortcut icon", href="/favicon.ico", type = "image/x-icon")
}
body("bg-gray-900 text-white") {
navbar(jwtPayload)
main { this@body.body() }
}
}
}
}
@@ -0,0 +1,19 @@
package be.simplenotes.app.views.components
import kotlinx.html.*
fun FlowContent.alert(type: Alert, title: String, details: String? = null) {
val colors = when (type) {
Alert.Success -> "bg-green-500 border border-green-400 text-gray-800"
Alert.Warning -> "bg-red-500 border border-red-400 text-red-200"
}
div("$colors px-4 py-3 mb-4 rounded relative") {
attributes["role"] = "alert"
strong("font-bold") { +title }
details?.let { span("block sm:inline") { +details } }
}
}
enum class Alert {
Success, Warning
}
@@ -0,0 +1,37 @@
package be.simplenotes.app.views.components
import kotlinx.html.*
import kotlinx.html.ButtonType.submit
fun FlowContent.input(
type: InputType = InputType.text,
placeholder: String,
id: String,
autoComplete: String? = null,
error: String? = null
) {
val colors = "bg-gray-800 border-gray-700 focus:border-teal-500 text-white"
div("mb-8") {
input(
type = type,
classes = "$colors rounded w-full border appearance-none focus:outline-none text-base p-2"
) {
attributes["placeholder"] = placeholder
attributes["aria-label"] = placeholder
attributes["name"] = id
attributes["id"] = id
autoComplete?.let { attributes["autocomplete"] = it }
}
error?.let { p("mt-2 text-red-500 text-sm italic") { +"$placeholder $error" } }
}
}
fun FlowContent.submitButton(text: String) {
div("flex items-center mt-6") {
button(
type = submit,
classes = "bg-teal-500 hover:bg-teal-600 focus:bg-teal-600" +
" w-full focus:outline-none text-white font-bold py-2 px-4 rounded"
) { +text }
}
}
@@ -0,0 +1,29 @@
package be.simplenotes.app.views.components
import be.simplenotes.domain.security.JwtPayload
import kotlinx.html.*
fun BODY.navbar(jwtPayload: JwtPayload?) {
nav("nav bg-teal-700 shadow-md flex items-center justify-between px-4") {
a(href = "/", classes = "text-2xl text-gray-100 font-bold") { +"SimpleNotes" }
ul {
if (jwtPayload != null) {
li("inline text-gray-100 ml-2 text-md font-semibold") {
a(href = "/notes") { +"Notes" }
}
li(
"text-gray-800 bg-green-500 hover:bg-green-700" +
" inline ml-2 text-md font-semibold rounded px-4 py-2"
) {
form(classes = "inline", action = "/logout", method = FormMethod.post) {
button(type = ButtonType.submit) { +"Logout" }
}
}
} else {
li("inline text-gray-100 pl-2 text-md font-semibold") {
a(href = "/login") { +"Sign In" }
}
}
}
}
}