Merge http4k
This commit is contained in:
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user