Separate views into a maven module
This commit is contained in:
@@ -6,6 +6,7 @@ import be.simplenotes.domain.domainModule
|
||||
import be.simplenotes.persistance.migrationModule
|
||||
import be.simplenotes.persistance.persistanceModule
|
||||
import be.simplenotes.search.searchModule
|
||||
import be.simplenotes.views.viewModule
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.context.unloadKoinModules
|
||||
|
||||
@@ -16,10 +17,8 @@ fun main() {
|
||||
persistanceModule,
|
||||
migrationModule,
|
||||
configModule,
|
||||
baseModule,
|
||||
userModule,
|
||||
noteModule,
|
||||
settingsModule,
|
||||
viewModule,
|
||||
controllerModule,
|
||||
domainModule,
|
||||
searchModule,
|
||||
apiModule,
|
||||
|
||||
@@ -4,8 +4,8 @@ import be.simplenotes.app.extensions.auto
|
||||
import be.simplenotes.app.utils.parseSearchTerms
|
||||
import be.simplenotes.types.PersistedNote
|
||||
import be.simplenotes.types.PersistedNoteMetadata
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import be.simplenotes.domain.usecases.NoteService
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -20,27 +20,27 @@ import java.util.*
|
||||
|
||||
class ApiNoteController(private val noteService: NoteService, private val json: Json) {
|
||||
|
||||
fun createNote(request: Request, jwtPayload: JwtPayload): Response {
|
||||
fun createNote(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val content = noteContentLens(request)
|
||||
return noteService.create(jwtPayload.userId, content).fold(
|
||||
return noteService.create(loggedInUser.userId, content).fold(
|
||||
{ Response(BAD_REQUEST) },
|
||||
{ uuidContentLens(UuidContent(it.uuid), Response(OK)) }
|
||||
)
|
||||
}
|
||||
|
||||
fun notes(request: Request, jwtPayload: JwtPayload): Response {
|
||||
val notes = noteService.paginatedNotes(jwtPayload.userId, page = 1).notes
|
||||
fun notes(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val notes = noteService.paginatedNotes(loggedInUser.userId, page = 1).notes
|
||||
return persistedNotesMetadataLens(notes, Response(OK))
|
||||
}
|
||||
|
||||
fun note(request: Request, jwtPayload: JwtPayload): Response =
|
||||
noteService.find(jwtPayload.userId, uuidLens(request))
|
||||
fun note(request: Request, loggedInUser: LoggedInUser): Response =
|
||||
noteService.find(loggedInUser.userId, uuidLens(request))
|
||||
?.let { persistedNoteLens(it, Response(OK)) }
|
||||
?: Response(NOT_FOUND)
|
||||
|
||||
fun update(request: Request, jwtPayload: JwtPayload): Response {
|
||||
fun update(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val content = noteContentLens(request)
|
||||
return noteService.update(jwtPayload.userId, uuidLens(request), content).fold({
|
||||
return noteService.update(loggedInUser.userId, uuidLens(request), content).fold({
|
||||
Response(BAD_REQUEST)
|
||||
}, {
|
||||
if (it == null) Response(NOT_FOUND)
|
||||
@@ -48,10 +48,10 @@ class ApiNoteController(private val noteService: NoteService, private val json:
|
||||
})
|
||||
}
|
||||
|
||||
fun search(request: Request, jwtPayload: JwtPayload): Response {
|
||||
fun search(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val query = searchContentLens(request)
|
||||
val terms = parseSearchTerms(query)
|
||||
val notes = noteService.search(jwtPayload.userId, terms)
|
||||
val notes = noteService.search(loggedInUser.userId, terms)
|
||||
return persistedNotesMetadataLens(notes, Response(OK))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package be.simplenotes.app.controllers
|
||||
|
||||
import be.simplenotes.app.extensions.html
|
||||
import be.simplenotes.app.views.BaseView
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import be.simplenotes.views.BaseView
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.Response
|
||||
import org.http4k.core.Status.Companion.OK
|
||||
|
||||
class BaseController(private val view: BaseView) {
|
||||
fun index(@Suppress("UNUSED_PARAMETER") request: Request, jwtPayload: JwtPayload?) =
|
||||
Response(OK).html(view.renderHome(jwtPayload))
|
||||
fun index(@Suppress("UNUSED_PARAMETER") request: Request, loggedInUser: LoggedInUser?) =
|
||||
Response(OK).html(view.renderHome(loggedInUser))
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ package be.simplenotes.app.controllers
|
||||
import be.simplenotes.app.extensions.html
|
||||
import be.simplenotes.app.extensions.redirect
|
||||
import be.simplenotes.app.utils.parseSearchTerms
|
||||
import be.simplenotes.app.views.NoteView
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import be.simplenotes.views.NoteView
|
||||
import be.simplenotes.domain.usecases.NoteService
|
||||
import be.simplenotes.domain.usecases.markdown.InvalidMeta
|
||||
import be.simplenotes.domain.usecases.markdown.MissingMeta
|
||||
import be.simplenotes.domain.usecases.markdown.ValidationError
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import org.http4k.core.Method
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.Response
|
||||
@@ -25,18 +25,18 @@ class NoteController(
|
||||
private val noteService: NoteService,
|
||||
) {
|
||||
|
||||
fun new(request: Request, jwtPayload: JwtPayload): Response {
|
||||
if (request.method == Method.GET) return Response(OK).html(view.noteEditor(jwtPayload))
|
||||
fun new(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
if (request.method == Method.GET) return Response(OK).html(view.noteEditor(loggedInUser))
|
||||
|
||||
val markdownForm = request.form("markdown") ?: ""
|
||||
|
||||
return noteService.create(jwtPayload.userId, markdownForm).fold(
|
||||
return noteService.create(loggedInUser.userId, markdownForm).fold(
|
||||
{
|
||||
val html = when (it) {
|
||||
MissingMeta -> view.noteEditor(jwtPayload, error = "Missing note metadata", textarea = markdownForm)
|
||||
InvalidMeta -> view.noteEditor(jwtPayload, error = "Invalid note metadata", textarea = markdownForm)
|
||||
MissingMeta -> view.noteEditor(loggedInUser, error = "Missing note metadata", textarea = markdownForm)
|
||||
InvalidMeta -> view.noteEditor(loggedInUser, error = "Invalid note metadata", textarea = markdownForm)
|
||||
is ValidationError -> view.noteEditor(
|
||||
jwtPayload,
|
||||
loggedInUser,
|
||||
validationErrors = it.validationErrors,
|
||||
textarea = markdownForm
|
||||
)
|
||||
@@ -49,66 +49,66 @@ class NoteController(
|
||||
)
|
||||
}
|
||||
|
||||
fun list(request: Request, jwtPayload: JwtPayload): Response {
|
||||
fun list(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1
|
||||
val tag = request.query("tag")
|
||||
val (pages, notes) = noteService.paginatedNotes(jwtPayload.userId, currentPage, tag = tag)
|
||||
val deletedCount = noteService.countDeleted(jwtPayload.userId)
|
||||
return Response(OK).html(view.notes(jwtPayload, notes, currentPage, pages, deletedCount, tag = tag))
|
||||
val (pages, notes) = noteService.paginatedNotes(loggedInUser.userId, currentPage, tag = tag)
|
||||
val deletedCount = noteService.countDeleted(loggedInUser.userId)
|
||||
return Response(OK).html(view.notes(loggedInUser, notes, currentPage, pages, deletedCount, tag = tag))
|
||||
}
|
||||
|
||||
fun search(request: Request, jwtPayload: JwtPayload): Response {
|
||||
fun search(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val query = request.form("search") ?: ""
|
||||
val terms = parseSearchTerms(query)
|
||||
val notes = noteService.search(jwtPayload.userId, terms)
|
||||
val deletedCount = noteService.countDeleted(jwtPayload.userId)
|
||||
return Response(OK).html(view.search(jwtPayload, notes, query, deletedCount))
|
||||
val notes = noteService.search(loggedInUser.userId, terms)
|
||||
val deletedCount = noteService.countDeleted(loggedInUser.userId)
|
||||
return Response(OK).html(view.search(loggedInUser, notes, query, deletedCount))
|
||||
}
|
||||
|
||||
fun note(request: Request, jwtPayload: JwtPayload): Response {
|
||||
fun note(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
|
||||
|
||||
if (request.method == Method.POST) {
|
||||
if (request.form("delete") != null) {
|
||||
return if (noteService.trash(jwtPayload.userId, noteUuid))
|
||||
return if (noteService.trash(loggedInUser.userId, noteUuid))
|
||||
Response.redirect("/notes") // TODO: flash cookie to show success ?
|
||||
else
|
||||
Response(NOT_FOUND) // TODO: show an error
|
||||
}
|
||||
if (request.form("public") != null) {
|
||||
if (!noteService.makePublic(jwtPayload.userId, noteUuid)) return Response(NOT_FOUND)
|
||||
if (!noteService.makePublic(loggedInUser.userId, noteUuid)) return Response(NOT_FOUND)
|
||||
} else if (request.form("private") != null) {
|
||||
if (!noteService.makePrivate(jwtPayload.userId, noteUuid)) return Response(NOT_FOUND)
|
||||
if (!noteService.makePrivate(loggedInUser.userId, noteUuid)) return Response(NOT_FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
val note = noteService.find(jwtPayload.userId, noteUuid) ?: return Response(NOT_FOUND)
|
||||
return Response(OK).html(view.renderedNote(jwtPayload, note, shared = false))
|
||||
val note = noteService.find(loggedInUser.userId, noteUuid) ?: return Response(NOT_FOUND)
|
||||
return Response(OK).html(view.renderedNote(loggedInUser, note, shared = false))
|
||||
}
|
||||
|
||||
fun public(request: Request, jwtPayload: JwtPayload?): Response {
|
||||
fun public(request: Request, loggedInUser: LoggedInUser?): Response {
|
||||
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
|
||||
val note = noteService.findPublic(noteUuid) ?: return Response(NOT_FOUND)
|
||||
return Response(OK).html(view.renderedNote(jwtPayload, note, shared = true))
|
||||
return Response(OK).html(view.renderedNote(loggedInUser, note, shared = true))
|
||||
}
|
||||
|
||||
fun edit(request: Request, jwtPayload: JwtPayload): Response {
|
||||
fun edit(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
|
||||
val note = noteService.find(jwtPayload.userId, noteUuid) ?: return Response(NOT_FOUND)
|
||||
val note = noteService.find(loggedInUser.userId, noteUuid) ?: return Response(NOT_FOUND)
|
||||
|
||||
if (request.method == Method.GET) {
|
||||
return Response(OK).html(view.noteEditor(jwtPayload, textarea = note.markdown))
|
||||
return Response(OK).html(view.noteEditor(loggedInUser, textarea = note.markdown))
|
||||
}
|
||||
|
||||
val markdownForm = request.form("markdown") ?: ""
|
||||
|
||||
return noteService.update(jwtPayload.userId, note.uuid, markdownForm).fold(
|
||||
return noteService.update(loggedInUser.userId, note.uuid, markdownForm).fold(
|
||||
{
|
||||
val html = when (it) {
|
||||
MissingMeta -> view.noteEditor(jwtPayload, error = "Missing note metadata", textarea = markdownForm)
|
||||
InvalidMeta -> view.noteEditor(jwtPayload, error = "Invalid note metadata", textarea = markdownForm)
|
||||
MissingMeta -> view.noteEditor(loggedInUser, error = "Missing note metadata", textarea = markdownForm)
|
||||
InvalidMeta -> view.noteEditor(loggedInUser, error = "Invalid note metadata", textarea = markdownForm)
|
||||
is ValidationError -> view.noteEditor(
|
||||
jwtPayload,
|
||||
loggedInUser,
|
||||
validationErrors = it.validationErrors,
|
||||
textarea = markdownForm
|
||||
)
|
||||
@@ -121,21 +121,21 @@ class NoteController(
|
||||
)
|
||||
}
|
||||
|
||||
fun trash(request: Request, jwtPayload: JwtPayload): Response {
|
||||
fun trash(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1
|
||||
val tag = request.query("tag")
|
||||
val (pages, notes) = noteService.paginatedNotes(jwtPayload.userId, currentPage, tag = tag, deleted = true)
|
||||
return Response(OK).html(view.trash(jwtPayload, notes, currentPage, pages))
|
||||
val (pages, notes) = noteService.paginatedNotes(loggedInUser.userId, currentPage, tag = tag, deleted = true)
|
||||
return Response(OK).html(view.trash(loggedInUser, notes, currentPage, pages))
|
||||
}
|
||||
|
||||
fun deleted(request: Request, jwtPayload: JwtPayload): Response {
|
||||
fun deleted(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val uuid = request.uuidPath() ?: return Response(NOT_FOUND)
|
||||
return if (request.form("delete") != null)
|
||||
if (noteService.delete(jwtPayload.userId, uuid))
|
||||
if (noteService.delete(loggedInUser.userId, uuid))
|
||||
Response.redirect("/notes/trash")
|
||||
else
|
||||
Response(NOT_FOUND)
|
||||
else if (noteService.restore(jwtPayload.userId, uuid))
|
||||
else if (noteService.restore(loggedInUser.userId, uuid))
|
||||
Response.redirect("/notes/$uuid")
|
||||
else
|
||||
Response(NOT_FOUND)
|
||||
|
||||
+14
-14
@@ -2,11 +2,11 @@ package be.simplenotes.app.controllers
|
||||
|
||||
import be.simplenotes.app.extensions.html
|
||||
import be.simplenotes.app.extensions.redirect
|
||||
import be.simplenotes.app.views.SettingView
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import be.simplenotes.views.SettingView
|
||||
import be.simplenotes.domain.usecases.UserService
|
||||
import be.simplenotes.domain.usecases.users.delete.DeleteError
|
||||
import be.simplenotes.domain.usecases.users.delete.DeleteForm
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import org.http4k.core.*
|
||||
import org.http4k.core.body.form
|
||||
import org.http4k.core.cookie.invalidateCookie
|
||||
@@ -15,11 +15,11 @@ class SettingsController(
|
||||
private val userService: UserService,
|
||||
private val settingView: SettingView,
|
||||
) {
|
||||
fun settings(request: Request, jwtPayload: JwtPayload): Response {
|
||||
fun settings(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
if (request.method == Method.GET)
|
||||
return Response(Status.OK).html(settingView.settings(jwtPayload))
|
||||
return Response(Status.OK).html(settingView.settings(loggedInUser))
|
||||
|
||||
val deleteForm = request.deleteForm(jwtPayload)
|
||||
val deleteForm = request.deleteForm(loggedInUser)
|
||||
val result = userService.delete(deleteForm)
|
||||
|
||||
return result.fold(
|
||||
@@ -28,13 +28,13 @@ class SettingsController(
|
||||
DeleteError.Unregistered -> Response.redirect("/").invalidateCookie("Bearer")
|
||||
DeleteError.WrongPassword -> Response(Status.OK).html(
|
||||
settingView.settings(
|
||||
jwtPayload,
|
||||
loggedInUser,
|
||||
error = "Wrong password"
|
||||
)
|
||||
)
|
||||
is DeleteError.InvalidForm -> Response(Status.OK).html(
|
||||
settingView.settings(
|
||||
jwtPayload,
|
||||
loggedInUser,
|
||||
validationErrors = it.validationErrors
|
||||
)
|
||||
)
|
||||
@@ -53,23 +53,23 @@ class SettingsController(
|
||||
.header("Content-Type", contentType)
|
||||
}
|
||||
|
||||
fun export(request: Request, jwtPayload: JwtPayload): Response {
|
||||
fun export(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val isDownload = request.form("download") != null
|
||||
|
||||
return if (isDownload) {
|
||||
val filename = "simplenotes-export-${jwtPayload.username}"
|
||||
val filename = "simplenotes-export-${loggedInUser.username}"
|
||||
if (request.form("format") == "zip") {
|
||||
val zip = userService.exportAsZip(jwtPayload.userId)
|
||||
val zip = userService.exportAsZip(loggedInUser.userId)
|
||||
Response(Status.OK)
|
||||
.with(attachment("$filename.zip", "application/zip"))
|
||||
.body(zip)
|
||||
} else
|
||||
Response(Status.OK)
|
||||
.with(attachment("$filename.json", "application/json"))
|
||||
.body(userService.exportAsJson(jwtPayload.userId))
|
||||
} else Response(Status.OK).body(userService.exportAsJson(jwtPayload.userId)).header("Content-Type", "application/json")
|
||||
.body(userService.exportAsJson(loggedInUser.userId))
|
||||
} else Response(Status.OK).body(userService.exportAsJson(loggedInUser.userId)).header("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
private fun Request.deleteForm(jwtPayload: JwtPayload) =
|
||||
DeleteForm(jwtPayload.username, form("password"), form("checked") != null)
|
||||
private fun Request.deleteForm(loggedInUser: LoggedInUser) =
|
||||
DeleteForm(loggedInUser.username, form("password"), form("checked") != null)
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ package be.simplenotes.app.controllers
|
||||
import be.simplenotes.app.extensions.html
|
||||
import be.simplenotes.app.extensions.isSecure
|
||||
import be.simplenotes.app.extensions.redirect
|
||||
import be.simplenotes.app.views.UserView
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import be.simplenotes.views.UserView
|
||||
import be.simplenotes.domain.usecases.UserService
|
||||
import be.simplenotes.domain.usecases.users.login.*
|
||||
import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm
|
||||
import be.simplenotes.domain.usecases.users.register.RegisterForm
|
||||
import be.simplenotes.domain.usecases.users.register.UserExists
|
||||
import be.simplenotes.config.JwtConfig
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import org.http4k.core.Method.GET
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.Response
|
||||
@@ -27,9 +27,9 @@ class UserController(
|
||||
private val userView: UserView,
|
||||
private val jwtConfig: JwtConfig,
|
||||
) {
|
||||
fun register(request: Request, jwtPayload: JwtPayload?): Response {
|
||||
fun register(request: Request, loggedInUser: LoggedInUser?): Response {
|
||||
if (request.method == GET) return Response(OK).html(
|
||||
userView.register(jwtPayload)
|
||||
userView.register(loggedInUser)
|
||||
)
|
||||
|
||||
val result = userService.register(request.registerForm())
|
||||
@@ -38,12 +38,12 @@ class UserController(
|
||||
{
|
||||
val html = when (it) {
|
||||
UserExists -> userView.register(
|
||||
jwtPayload,
|
||||
loggedInUser,
|
||||
error = "User already exists"
|
||||
)
|
||||
is InvalidRegisterForm ->
|
||||
userView.register(
|
||||
jwtPayload,
|
||||
loggedInUser,
|
||||
validationErrors = it.validationErrors
|
||||
)
|
||||
}
|
||||
@@ -58,9 +58,9 @@ class UserController(
|
||||
private fun Request.registerForm() = RegisterForm(form("username"), form("password"))
|
||||
private fun Request.loginForm(): LoginForm = registerForm()
|
||||
|
||||
fun login(request: Request, jwtPayload: JwtPayload?): Response {
|
||||
fun login(request: Request, loggedInUser: LoggedInUser?): Response {
|
||||
if (request.method == GET) return Response(OK).html(
|
||||
userView.login(jwtPayload)
|
||||
userView.login(loggedInUser)
|
||||
)
|
||||
|
||||
val result = userService.login(request.loginForm())
|
||||
@@ -70,17 +70,17 @@ class UserController(
|
||||
val html = when (it) {
|
||||
Unregistered ->
|
||||
userView.login(
|
||||
jwtPayload,
|
||||
loggedInUser,
|
||||
error = "User does not exist"
|
||||
)
|
||||
WrongPassword ->
|
||||
userView.login(
|
||||
jwtPayload,
|
||||
loggedInUser,
|
||||
error = "Wrong password"
|
||||
)
|
||||
is InvalidLoginForm ->
|
||||
userView.login(
|
||||
jwtPayload,
|
||||
loggedInUser,
|
||||
validationErrors = it.validationErrors
|
||||
)
|
||||
}
|
||||
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
package be.simplenotes.app.extensions
|
||||
|
||||
import kotlinx.html.*
|
||||
|
||||
class SUMMARY(consumer: TagConsumer<*>) :
|
||||
HTMLTag(
|
||||
"summary", consumer, emptyMap(),
|
||||
inlineTag = true,
|
||||
emptyTag = false
|
||||
),
|
||||
HtmlInlineTag
|
||||
|
||||
fun DETAILS.summary(block: SUMMARY.() -> Unit = {}) {
|
||||
SUMMARY(consumer).visit(block)
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package be.simplenotes.app.filters
|
||||
|
||||
import be.simplenotes.app.extensions.redirect
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import be.simplenotes.domain.security.JwtPayloadExtractor
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import org.http4k.core.*
|
||||
import org.http4k.core.Status.Companion.UNAUTHORIZED
|
||||
import org.http4k.core.cookie.cookie
|
||||
@@ -40,7 +40,7 @@ class AuthFilter(
|
||||
}
|
||||
}
|
||||
|
||||
fun Request.jwtPayload(ctx: RequestContexts): JwtPayload? = ctx[this][authKey]
|
||||
fun Request.jwtPayload(ctx: RequestContexts): LoggedInUser? = ctx[this][authKey]
|
||||
|
||||
enum class JwtSource {
|
||||
Header, Cookie
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package be.simplenotes.app.filters
|
||||
|
||||
import be.simplenotes.app.extensions.html
|
||||
import be.simplenotes.app.views.ErrorView
|
||||
import be.simplenotes.app.views.ErrorView.Type.*
|
||||
import be.simplenotes.views.ErrorView
|
||||
import be.simplenotes.views.ErrorView.Type.*
|
||||
import org.http4k.core.*
|
||||
import org.http4k.core.Status.Companion.INTERNAL_SERVER_ERROR
|
||||
import org.http4k.core.Status.Companion.NOT_FOUND
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package be.simplenotes.app.modules
|
||||
|
||||
import be.simplenotes.app.Config
|
||||
import be.simplenotes.app.utils.StaticFileResolver
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val configModule = module {
|
||||
|
||||
@@ -1,29 +1,12 @@
|
||||
package be.simplenotes.app.modules
|
||||
|
||||
import be.simplenotes.app.controllers.*
|
||||
import be.simplenotes.app.views.BaseView
|
||||
import be.simplenotes.app.views.NoteView
|
||||
import be.simplenotes.app.views.SettingView
|
||||
import be.simplenotes.app.views.UserView
|
||||
import org.koin.dsl.module
|
||||
|
||||
val userModule = module {
|
||||
val controllerModule = module {
|
||||
single { UserController(get(), get(), get()) }
|
||||
single { UserView(get()) }
|
||||
}
|
||||
|
||||
val baseModule = module {
|
||||
single { HealthCheckController(get()) }
|
||||
single { BaseController(get()) }
|
||||
single { BaseView(get()) }
|
||||
}
|
||||
|
||||
val noteModule = module {
|
||||
single { NoteController(get(), get()) }
|
||||
single { NoteView(get()) }
|
||||
}
|
||||
|
||||
val settingsModule = module {
|
||||
single { SettingsController(get(), get()) }
|
||||
single { SettingView(get()) }
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import be.simplenotes.app.jetty.Jetty
|
||||
import be.simplenotes.app.routes.Router
|
||||
import be.simplenotes.app.utils.StaticFileResolver
|
||||
import be.simplenotes.app.utils.StaticFileResolverImpl
|
||||
import be.simplenotes.app.views.ErrorView
|
||||
import be.simplenotes.views.ErrorView
|
||||
import be.simplenotes.config.ServerConfig
|
||||
import org.eclipse.jetty.server.ServerConnector
|
||||
import org.http4k.core.Filter
|
||||
@@ -59,5 +59,5 @@ val serverModule = module {
|
||||
single<Filter>(AuthType.Required.qualifier) { AuthFilter(get(), AuthType.Required, get()) }
|
||||
single { ErrorFilter(get()) }
|
||||
single { TransactionFilter(get()) }
|
||||
single { ErrorView(get()) }
|
||||
single(named("styles")) { get<StaticFileResolver>().resolve("styles.css") }
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import be.simplenotes.app.api.ApiNoteController
|
||||
import be.simplenotes.app.api.ApiUserController
|
||||
import be.simplenotes.app.controllers.*
|
||||
import be.simplenotes.app.filters.*
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import org.http4k.core.*
|
||||
import org.http4k.core.Method.*
|
||||
import org.http4k.filter.ResponseFilters.GZip
|
||||
@@ -102,5 +102,5 @@ class Router(
|
||||
this to transactionFilter.then { handler(it, it.jwtPayload(contexts)) }
|
||||
}
|
||||
|
||||
private typealias PublicHandler = (Request, JwtPayload?) -> Response
|
||||
private typealias ProtectedHandler = (Request, JwtPayload) -> Response
|
||||
private typealias PublicHandler = (Request, LoggedInUser?) -> Response
|
||||
private typealias ProtectedHandler = (Request, LoggedInUser) -> Response
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package be.simplenotes.app.utils
|
||||
|
||||
import org.ocpsoft.prettytime.PrettyTime
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.util.*
|
||||
|
||||
private val prettyTime = PrettyTime()
|
||||
|
||||
fun LocalDateTime.toTimeAgo(): String = prettyTime.format(Date.from(atZone(ZoneId.systemDefault()).toInstant()))
|
||||
@@ -1,108 +0,0 @@
|
||||
package be.simplenotes.app.views
|
||||
|
||||
import be.simplenotes.app.utils.StaticFileResolver
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import kotlinx.html.*
|
||||
import kotlinx.html.ThScope.col
|
||||
|
||||
class BaseView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) {
|
||||
fun renderHome(jwtPayload: JwtPayload?) = renderPage(
|
||||
title = "Home",
|
||||
description = "A fast and simple note taking website",
|
||||
jwtPayload = jwtPayload
|
||||
) {
|
||||
section("text-center my-2 p-2") {
|
||||
h1("text-5xl casual") {
|
||||
span("text-teal-300") { +"SimpleNotes " }
|
||||
+"- access your notes anywhere"
|
||||
}
|
||||
}
|
||||
|
||||
div("container mx-auto flex flex-wrap justify-center content-center") {
|
||||
|
||||
div("md:order-1 order-2 flipped p-4 my-10 w-full md:w-1/2") {
|
||||
attributes["aria-label"] = "demo"
|
||||
div("flex justify-between mb-4") {
|
||||
h1("text-2xl underline") { +"Notes" }
|
||||
span {
|
||||
span("btn btn-teal pointer-events-none") { +"Trash (3)" }
|
||||
span("ml-2 btn btn-green pointer-events-none") { +"New" }
|
||||
}
|
||||
}
|
||||
form(classes = "md:space-x-2") {
|
||||
id = "search"
|
||||
input {
|
||||
attributes["aria-label"] = "demo-search"
|
||||
attributes["name"] = "search"
|
||||
attributes["disabled"] = ""
|
||||
attributes["value"] = "tag:\"demo\""
|
||||
}
|
||||
span {
|
||||
id = "buttons"
|
||||
button(type = ButtonType.button, classes = "btn btn-green pointer-events-none") {
|
||||
attributes["disabled"] = ""
|
||||
+"search"
|
||||
}
|
||||
span("btn btn-red pointer-events-none") { +"clear" }
|
||||
}
|
||||
}
|
||||
div("overflow-x-auto") {
|
||||
demoTable()
|
||||
}
|
||||
}
|
||||
welcome()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
private inline fun DIV.demoTable() {
|
||||
table {
|
||||
id = "notes"
|
||||
thead {
|
||||
tr {
|
||||
th(scope = col, classes = "w-1/2") { +"Title" }
|
||||
th(scope = col, classes = "w-1/4") { +"Updated" }
|
||||
th(scope = col, classes = "w-1/4") { +"Tags" }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
listOf(
|
||||
Triple("Formula 1", "moments ago", arrayOf("#demo")),
|
||||
Triple("Syntax highlighting", "2 hours ago", arrayOf("#features", "#demo")),
|
||||
Triple("report", "5 days ago", arrayOf("#study", "#demo")),
|
||||
).forEach { (title, ago, tags) ->
|
||||
tr {
|
||||
td { span("text-blue-200 font-semibold underline") { +title } }
|
||||
td("text-center") { +ago }
|
||||
td {
|
||||
ul("inline flex flex-wrap justify-center") {
|
||||
tags.forEach { tag ->
|
||||
li("mx-2 my-1") { span("tag disabled") { +tag } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
private inline fun DIV.welcome() {
|
||||
div("w-full my-auto md:w-1/2 md:order-2 order-1 text-center") {
|
||||
div("m-4 rounded-lg p-6") {
|
||||
h2("text-3xl text-teal-400 underline") { +"Features:" }
|
||||
ul("list-disc text-lg list-inside") {
|
||||
li { +"Markdown support" }
|
||||
li { +"Full text search" }
|
||||
li { +"Structured search" }
|
||||
li { +"Code highlighting" }
|
||||
li { +"Fast and lightweight" }
|
||||
li { +"No tracking" }
|
||||
li { +"Works without javascript" }
|
||||
li { +"Data export" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
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 kotlinx.html.a
|
||||
import kotlinx.html.div
|
||||
|
||||
class ErrorView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) {
|
||||
|
||||
enum class Type(val title: String) {
|
||||
SqlTransientError("Database unavailable"),
|
||||
NotFound("Not Found"),
|
||||
Other("Error"),
|
||||
}
|
||||
|
||||
fun error(errorType: Type) = renderPage(errorType.title, jwtPayload = null) {
|
||||
div("container mx-auto p-4") {
|
||||
when (errorType) {
|
||||
Type.SqlTransientError -> alert(
|
||||
Alert.Warning,
|
||||
errorType.title,
|
||||
"Please try again later",
|
||||
multiline = true
|
||||
)
|
||||
Type.NotFound -> alert(Alert.Warning, errorType.title, "Page not found", multiline = true)
|
||||
Type.Other -> alert(Alert.Warning, errorType.title)
|
||||
}
|
||||
div {
|
||||
a(href = "/", classes = "btn btn-green") { +"Go back to the homepage" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
package be.simplenotes.app.views
|
||||
|
||||
import be.simplenotes.app.utils.StaticFileResolver
|
||||
import be.simplenotes.app.views.components.*
|
||||
import be.simplenotes.types.PersistedNote
|
||||
import be.simplenotes.types.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") {
|
||||
error?.let { alert(Alert.Warning, error) }
|
||||
validationErrors.forEach {
|
||||
alert(Alert.Warning, it.dataPath.substringAfter('.') + ": " + it.message)
|
||||
}
|
||||
form(method = FormMethod.post) {
|
||||
textArea {
|
||||
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,
|
||||
numberOfDeletedNotes: Int,
|
||||
tag: String?,
|
||||
) = renderPage(title = "Notes", jwtPayload = jwtPayload) {
|
||||
div("container mx-auto p-4") {
|
||||
noteListHeader(numberOfDeletedNotes)
|
||||
if (notes.isNotEmpty())
|
||||
noteTable(notes)
|
||||
else
|
||||
span {
|
||||
if (numberOfPages > 1) +"You went too far"
|
||||
else +"No notes yet"
|
||||
}
|
||||
|
||||
if (numberOfPages > 1) pagination(currentPage, numberOfPages, tag)
|
||||
}
|
||||
}
|
||||
|
||||
fun search(
|
||||
jwtPayload: JwtPayload,
|
||||
notes: List<PersistedNoteMetadata>,
|
||||
query: String,
|
||||
numberOfDeletedNotes: Int,
|
||||
) = renderPage("Notes", jwtPayload = jwtPayload) {
|
||||
div("container mx-auto p-4") {
|
||||
noteListHeader(numberOfDeletedNotes, query)
|
||||
noteTable(notes)
|
||||
}
|
||||
}
|
||||
|
||||
fun trash(
|
||||
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") { +"Deleted notes" }
|
||||
}
|
||||
if (notes.isNotEmpty())
|
||||
deletedNoteTable(notes)
|
||||
else
|
||||
span {
|
||||
if (numberOfPages > 1) +"You went too far"
|
||||
else +"No deleted notes"
|
||||
}
|
||||
|
||||
if (numberOfPages > 1) pagination(currentPage, numberOfPages, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DIV.pagination(currentPage: Int, numberOfPages: Int, tag: String?) {
|
||||
val links = mutableListOf<Pair<String, String>>()
|
||||
// if (currentPage > 1) links += "Previous" to "?page=${currentPage - 1}"
|
||||
links += (1..numberOfPages).map { page ->
|
||||
"$page" to (tag?.let { "?page=$page&tag=$it" } ?: "?page=$page")
|
||||
}
|
||||
// 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, shared: Boolean) = renderPage(
|
||||
note.meta.title,
|
||||
jwtPayload = jwtPayload,
|
||||
scripts = listOf("/highlight.10.1.2.js", "/init-highlight.0.0.1.js")
|
||||
) {
|
||||
div("container mx-auto p-4") {
|
||||
|
||||
if (shared) {
|
||||
p("p-4 bg-gray-800") {
|
||||
+"You are viewing a public note "
|
||||
}
|
||||
|
||||
hr { }
|
||||
}
|
||||
|
||||
div("flex items-center justify-between mb-4") {
|
||||
h1("text-3xl fond-bold underline") { +note.meta.title }
|
||||
span("space-x-2") {
|
||||
note.meta.tags.forEach {
|
||||
a(href = "/notes?tag=$it", classes = "tag") {
|
||||
+"#$it"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!shared) {
|
||||
noteActionForm(note)
|
||||
|
||||
if (note.public) {
|
||||
p("my-4") {
|
||||
+"You can share this link : "
|
||||
a(href = "/notes/public/${note.uuid}", classes = "text-blue-300 underline") {
|
||||
+"/notes/public/${note.uuid}"
|
||||
}
|
||||
}
|
||||
hr { }
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
attributes["id"] = "note"
|
||||
unsafe {
|
||||
+note.html
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DIV.noteActionForm(note: PersistedNote) {
|
||||
form(method = FormMethod.post, classes = "inline flex space-x-2 justify-end mb-4") {
|
||||
a(
|
||||
href = "/notes/${note.uuid}/edit",
|
||||
classes = "btn btn-green"
|
||||
) { +"Edit" }
|
||||
span {
|
||||
button(
|
||||
type = ButtonType.submit,
|
||||
name = if (note.public) "private" else "public",
|
||||
classes = "font-semibold border-b-4 ${if (note.public) "border-teal-200" else "border-green-500"}" +
|
||||
" p-2 rounded-l bg-teal-200 text-gray-800"
|
||||
) {
|
||||
+"Private"
|
||||
}
|
||||
button(
|
||||
type = ButtonType.submit,
|
||||
name = if (note.public) "private" else "public",
|
||||
classes = "font-semibold border-b-4 ${if (!note.public) "border-teal-200" else "border-green-500"}" +
|
||||
" p-2 rounded-r bg-teal-200 text-gray-800"
|
||||
) {
|
||||
+"Public"
|
||||
}
|
||||
}
|
||||
button(
|
||||
type = ButtonType.submit,
|
||||
name = "delete",
|
||||
classes = "btn btn-red"
|
||||
) { +"Delete" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package be.simplenotes.app.views
|
||||
|
||||
import be.simplenotes.app.extensions.summary
|
||||
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.domain.security.JwtPayload
|
||||
import io.konform.validation.ValidationError
|
||||
import kotlinx.html.*
|
||||
import kotlinx.html.ButtonType.submit
|
||||
|
||||
class SettingView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) {
|
||||
|
||||
fun settings(
|
||||
jwtPayload: JwtPayload,
|
||||
error: String? = null,
|
||||
validationErrors: List<ValidationError> = emptyList(),
|
||||
) = renderPage("Settings", jwtPayload = jwtPayload) {
|
||||
div("container mx-auto") {
|
||||
|
||||
section("m-4 p-4 bg-gray-800 rounded") {
|
||||
h1("text-xl") {
|
||||
+"Welcome "
|
||||
span("text-teal-200 font-semibold") { +jwtPayload.username }
|
||||
}
|
||||
}
|
||||
|
||||
section("m-4 p-2 bg-gray-800 rounded flex flex-wrap justify-around items-end") {
|
||||
|
||||
form(classes = "m-2", method = FormMethod.post, action = "/export") {
|
||||
button(name = "display",
|
||||
classes = "inline btn btn-teal block",
|
||||
type = submit) { +"Display my data" }
|
||||
}
|
||||
|
||||
form(classes = "m-2", method = FormMethod.post, action = "/export") {
|
||||
|
||||
div {
|
||||
listOf("json", "zip").forEach { format ->
|
||||
radioInput(name = "format") {
|
||||
id = format
|
||||
attributes["value"] = format
|
||||
if (format == "json") attributes["checked"] = ""
|
||||
else attributes["class"] = "ml-4"
|
||||
}
|
||||
label(classes = "ml-2") {
|
||||
attributes["for"] = format
|
||||
+format
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button(name = "download", classes = "inline btn btn-green block mt-2", type = submit) {
|
||||
+"Download my data"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(classes = "m-4 p-4 bg-gray-800 rounded") {
|
||||
h2(classes = "mb-4 text-red-400 text-lg font-semibold") {
|
||||
+"Delete my account"
|
||||
}
|
||||
|
||||
error?.let { alert(Alert.Warning, error) }
|
||||
|
||||
details {
|
||||
|
||||
if (error != null || validationErrors.isNotEmpty()) {
|
||||
attributes["open"] = ""
|
||||
}
|
||||
|
||||
summary {
|
||||
span(classes = "mb-4 font-semibold underline") {
|
||||
+"Are you sure? "
|
||||
+"You are about to delete this user, and this process is irreversible !"
|
||||
}
|
||||
}
|
||||
|
||||
form(classes = "mt-4", method = FormMethod.post) {
|
||||
input(
|
||||
id = "password",
|
||||
placeholder = "Password",
|
||||
autoComplete = "off",
|
||||
type = InputType.password,
|
||||
error = validationErrors.find { it.dataPath == ".password" }?.message
|
||||
)
|
||||
checkBoxInput(name = "checked") {
|
||||
id = "checked"
|
||||
attributes["required"] = ""
|
||||
label {
|
||||
attributes["for"] = "checked"
|
||||
+" Do you want to proceed ?"
|
||||
}
|
||||
}
|
||||
button(
|
||||
type = submit,
|
||||
classes = "block mt-4 btn btn-red",
|
||||
name = "delete"
|
||||
) { +"I'm sure" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
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",
|
||||
"Registration page",
|
||||
jwtPayload,
|
||||
error,
|
||||
validationErrors,
|
||||
"Create an account",
|
||||
"Register"
|
||||
) {
|
||||
+"Already have an account? "
|
||||
a(href = "/login", classes = "no-underline text-blue-500 hover:text-blue-400 font-bold") { +"Sign In" }
|
||||
}
|
||||
|
||||
fun login(
|
||||
jwtPayload: JwtPayload?,
|
||||
error: String? = null,
|
||||
validationErrors: List<ValidationError> = emptyList(),
|
||||
new: Boolean = false,
|
||||
) = accountForm("Login", "Login page", jwtPayload, error, validationErrors, "Sign In", "Sign In", new) {
|
||||
+"Don't have an account yet? "
|
||||
a(href = "/register", classes = "no-underline text-blue-500 hover:text-blue-400 font-bold") {
|
||||
+"Create an account"
|
||||
}
|
||||
}
|
||||
|
||||
private fun accountForm(
|
||||
title: String,
|
||||
description: String,
|
||||
jwtPayload: JwtPayload?,
|
||||
error: String? = null,
|
||||
validationErrors: List<ValidationError> = emptyList(),
|
||||
h1: String,
|
||||
submit: String,
|
||||
new: Boolean = false,
|
||||
footer: FlowContent.() -> Unit,
|
||||
) = renderPage(title = title, description, 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") {
|
||||
div("p-8 mb-6") {
|
||||
h1("font-semibold text-lg mb-6 text-center") { +h1 }
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
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(staticFileResolver: StaticFileResolver) {
|
||||
|
||||
private val styles = staticFileResolver.resolve("styles.css")!!
|
||||
|
||||
fun renderPage(
|
||||
title: String,
|
||||
description: String? = null,
|
||||
jwtPayload: JwtPayload?,
|
||||
scripts: List<String> = emptyList(),
|
||||
body: MAIN.() -> 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 = "preload", href = "/recursive-0.0.1.woff2"){
|
||||
attributes["as"] = "font"
|
||||
attributes["type"] = "font/woff2"
|
||||
attributes["crossorigin"] = "anonymous"
|
||||
}
|
||||
link(rel = "stylesheet", href = styles)
|
||||
icons()
|
||||
scripts.forEach { src ->
|
||||
script(src = src) {}
|
||||
}
|
||||
}
|
||||
body("bg-gray-900 text-white") {
|
||||
navbar(jwtPayload)
|
||||
main { body() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
private inline fun HEAD.icons() {
|
||||
link(rel = "apple-touch-icon", href = "/apple-touch-icon.png") { attributes["sizes"] = "180x180" }
|
||||
link(rel = "icon", href = "/favicon-32x32.png", type = "image/png") { attributes["sizes"] = "32x32" }
|
||||
link(rel = "icon", href = "/favicon-16x16.png", type = "image/png") { attributes["sizes"] = "16x16" }
|
||||
link(rel = "manifest", href = "/site.webmanifest")
|
||||
link(rel = "mask-icon", href = "/safari-pinned-tab.svg") { attributes["color"] = "#2c7a7b" }
|
||||
meta(name = "msapplication-TileColor", content = "#00aba9")
|
||||
meta(name = "theme-color", content = "#2c7a7b")
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package be.simplenotes.app.views.components
|
||||
|
||||
import kotlinx.html.*
|
||||
|
||||
fun FlowContent.alert(type: Alert, title: String, details: String? = null, multiline: Boolean = false) {
|
||||
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 {
|
||||
if (multiline) p { +details }
|
||||
else span("block sm:inline") { +details }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class Alert {
|
||||
Success, Warning
|
||||
}
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
package be.simplenotes.app.views.components
|
||||
|
||||
import be.simplenotes.app.utils.toTimeAgo
|
||||
import be.simplenotes.types.PersistedNoteMetadata
|
||||
import kotlinx.html.*
|
||||
import kotlinx.html.ButtonType.submit
|
||||
import kotlinx.html.FormMethod.post
|
||||
import kotlinx.html.ThScope.col
|
||||
|
||||
fun FlowContent.deletedNoteTable(notes: List<PersistedNoteMetadata>) = div("overflow-x-auto") {
|
||||
table {
|
||||
id = "notes"
|
||||
thead {
|
||||
tr {
|
||||
th(col, "w-1/4") { +"Title" }
|
||||
th(col, "w-1/4") { +"Updated" }
|
||||
th(col, "w-1/4") { +"Tags" }
|
||||
th(col, "w-1/4") { +"Restore" }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
notes.forEach { (title, tags, updatedAt, uuid) ->
|
||||
tr {
|
||||
td { +title }
|
||||
td("text-center") { +updatedAt.toTimeAgo() }
|
||||
td { tags(tags) }
|
||||
td("text-center") {
|
||||
form(method = post, action = "/notes/deleted/$uuid") {
|
||||
button(classes = "btn btn-red mb-2", type = submit, name = "delete") {
|
||||
+"Delete permanently"
|
||||
}
|
||||
button(classes = "ml-2 btn btn-green", type = submit, name = "restore") {
|
||||
+"Restore"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun FlowContent.tags(tags: List<String>) {
|
||||
ul("inline flex flex-wrap justify-center") {
|
||||
tags.forEach { tag ->
|
||||
li("mx-2 my-1") {
|
||||
span("tag disabled") { +"#$tag" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
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 = "btn btn-teal w-full"
|
||||
) { +text }
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package be.simplenotes.app.views.components
|
||||
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import kotlinx.html.*
|
||||
|
||||
fun BODY.navbar(jwtPayload: JwtPayload?) {
|
||||
nav {
|
||||
id = "navbar"
|
||||
a("/") {
|
||||
id = "home"
|
||||
+"SimpleNotes"
|
||||
}
|
||||
ul("space-x-2") {
|
||||
id = "navigation"
|
||||
if (jwtPayload != null) {
|
||||
val links = listOf(
|
||||
"/notes" to "Notes",
|
||||
"/settings" to "Settings",
|
||||
)
|
||||
links.forEach { (href, name) ->
|
||||
li("txt") {
|
||||
a(href = href) { +name }
|
||||
}
|
||||
}
|
||||
li {
|
||||
form(action = "/logout", method = FormMethod.post) {
|
||||
button(type = ButtonType.submit, classes = "btn btn-green") { +"Logout" }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
li {
|
||||
a(href = "/login", classes = "btn btn-green") { +"Sign In" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package be.simplenotes.app.views.components
|
||||
|
||||
import kotlinx.html.*
|
||||
import kotlinx.html.ButtonType.submit
|
||||
import kotlinx.html.FormMethod.post
|
||||
|
||||
fun DIV.noteListHeader(numberOfDeletedNotes: Int, query: String = "") {
|
||||
div("flex justify-between mb-4") {
|
||||
h1("text-2xl underline") { +"Notes" }
|
||||
span {
|
||||
a(
|
||||
href = "/notes/trash",
|
||||
classes = "btn btn-teal"
|
||||
) { +"Trash ($numberOfDeletedNotes)" }
|
||||
a(
|
||||
href = "/notes/new",
|
||||
classes = "ml-2 btn btn-green"
|
||||
) { +"New" }
|
||||
}
|
||||
}
|
||||
form(method = post, classes = "md:space-x-2") {
|
||||
id = "search"
|
||||
input(name = "search") {
|
||||
attributes["value"] = query
|
||||
attributes["aria-label"] = "search"
|
||||
}
|
||||
span {
|
||||
id = "buttons"
|
||||
button(type = submit, classes = "btn btn-green") {
|
||||
+"search"
|
||||
}
|
||||
a(href = "/notes", classes = "btn btn-red") {
|
||||
+"clear"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package be.simplenotes.app.views.components
|
||||
|
||||
import be.simplenotes.app.utils.toTimeAgo
|
||||
import be.simplenotes.types.PersistedNoteMetadata
|
||||
import kotlinx.html.*
|
||||
import kotlinx.html.ThScope.col
|
||||
|
||||
fun FlowContent.noteTable(notes: List<PersistedNoteMetadata>) = div("overflow-x-auto") {
|
||||
table {
|
||||
id = "notes"
|
||||
thead {
|
||||
tr {
|
||||
th(col, "w-1/2") { +"Title" }
|
||||
th(col, "w-1/4") { +"Updated" }
|
||||
th(col, "w-1/4") { +"Tags" }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
notes.forEach { (title, tags, updatedAt, uuid) ->
|
||||
tr {
|
||||
td {
|
||||
a(classes = "text-blue-200 font-semibold underline", href = "/notes/$uuid") { +title }
|
||||
}
|
||||
td("text-center") {
|
||||
+updatedAt.toTimeAgo()
|
||||
}
|
||||
td { tags(tags) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun FlowContent.tags(tags: List<String>) {
|
||||
ul("inline flex flex-wrap justify-center") {
|
||||
tags.forEach { tag ->
|
||||
li("mx-2 my-1") {
|
||||
a(href = "?tag=$tag", classes = "tag") { +"#$tag" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user