From: Lanius Trolling Date: Sun, 19 Feb 2023 17:46:04 +0000 (-0500) Subject: Allow comments on directory pages as well X-Git-Url: https://gitweb.starshipfights.net/?a=commitdiff_plain;h=cb26addb94e716d5f11d458ef5e69e88b1cce7bc;p=factbooks Allow comments on directory pages as well --- diff --git a/src/main/kotlin/info/mechyrdia/Factbooks.kt b/src/main/kotlin/info/mechyrdia/Factbooks.kt index 9bcdde7..82ad2fd 100644 --- a/src/main/kotlin/info/mechyrdia/Factbooks.kt +++ b/src/main/kotlin/info/mechyrdia/Factbooks.kt @@ -89,8 +89,8 @@ fun Application.factbooks() { exception { call, _ -> call.respondHtml(HttpStatusCode.Forbidden, call.error403()) } - exception { call, _ -> - call.respondHtml(HttpStatusCode.Forbidden, call.error403PageExpired()) + exception { call, (_, params) -> + call.respondHtml(HttpStatusCode.Forbidden, call.error403PageExpired(params)) } exception { call, _ -> call.respondHtml(HttpStatusCode.NotFound, call.error404()) @@ -99,7 +99,7 @@ fun Application.factbooks() { call.respondHtml(HttpStatusCode.NotFound, call.error404()) } - exception { call, ex -> + exception { call, ex -> call.application.log.error("Got uncaught exception from serving call ${call.callId}", ex) call.respondHtml(HttpStatusCode.InternalServerError, call.error500()) diff --git a/src/main/kotlin/info/mechyrdia/auth/csrf.kt b/src/main/kotlin/info/mechyrdia/auth/csrf.kt index f8b130b..b5cfaec 100644 --- a/src/main/kotlin/info/mechyrdia/auth/csrf.kt +++ b/src/main/kotlin/info/mechyrdia/auth/csrf.kt @@ -1,9 +1,12 @@ package info.mechyrdia.auth +import info.mechyrdia.data.Id +import info.mechyrdia.data.NationData import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.plugins.* import io.ktor.server.request.* +import io.ktor.server.sessions.* import io.ktor.server.util.* import kotlinx.html.FORM import kotlinx.html.hiddenInput @@ -14,14 +17,16 @@ data class CsrfPayload( val route: String, val remoteAddress: String, val userAgent: String?, - val expires: Instant = Instant.now().plusSeconds(3600) + val userAccount: Id?, + val expires: Instant ) -fun ApplicationCall.csrfPayload(route: String, withExpiration: Instant = Instant.now().plusSeconds(3600)) = +fun ApplicationCall.csrfPayload(route: String, withExpiration: Instant = Instant.now().plusSeconds(7200)) = CsrfPayload( route = route, remoteAddress = request.origin.remoteAddress, userAgent = request.userAgent(), + userAccount = sessions.get()?.nationId, expires = withExpiration ) @@ -42,14 +47,14 @@ suspend fun ApplicationCall.verifyCsrfToken(route: String = request.origin.uri): val params = receive() val token = params.getOrFail("csrf-token") - val check = csrfMap.remove(token) ?: throw CsrfFailedException("CSRF token does not exist") + val check = csrfMap.remove(token) ?: throw CsrfFailedException("The submitted CSRF token does not exist", params) val payload = csrfPayload(route, check.expires) if (check != payload) - throw CsrfFailedException("CSRF token does not match") + throw CsrfFailedException("The submitted CSRF token does not match", params) if (payload.expires < Instant.now()) - throw CsrfFailedException("CSRF token has expired") + throw CsrfFailedException("The submitted CSRF token has expired", params) return params } -class CsrfFailedException(override val message: String) : RuntimeException(message) +data class CsrfFailedException(override val message: String, val formData: Parameters) : RuntimeException(message) diff --git a/src/main/kotlin/info/mechyrdia/auth/views_login.kt b/src/main/kotlin/info/mechyrdia/auth/views_login.kt index a857944..c3cf8bf 100644 --- a/src/main/kotlin/info/mechyrdia/auth/views_login.kt +++ b/src/main/kotlin/info/mechyrdia/auth/views_login.kt @@ -65,7 +65,7 @@ suspend fun ApplicationCall.loginRoute(): Nothing { val nation = postParams.getOrFail("nation").toNationId() val checksum = postParams.getOrFail("checksum") - val token = nsTokenMap[postParams.getOrFail("token")] + val token = nsTokenMap.remove(postParams.getOrFail("token")) ?: throw MissingRequestParameterException("token") val result = NSAPI diff --git a/src/main/kotlin/info/mechyrdia/data/comments.kt b/src/main/kotlin/info/mechyrdia/data/comments.kt index 2fa4feb..42b9e72 100644 --- a/src/main/kotlin/info/mechyrdia/data/comments.kt +++ b/src/main/kotlin/info/mechyrdia/data/comments.kt @@ -26,7 +26,7 @@ data class Comment( val contents: String ) : DataDocument { companion object : TableHolder { - override val Table: DocumentTable = DocumentTable() + override val Table = DocumentTable() override suspend fun initialize() { Table.index(Comment::submittedBy, Comment::submittedAt) @@ -54,7 +54,7 @@ data class CommentReplyLink( val repliedAt: @Contextual Instant = Instant.now(), ) : DataDocument { companion object : TableHolder { - override val Table: DocumentTable = DocumentTable() + override val Table = DocumentTable() override suspend fun initialize() { Table.index(CommentReplyLink::originalPost) @@ -79,7 +79,9 @@ data class CommentReplyLink( ) } - suspend fun deleteComment(deletedReply: Id) = updateComment(deletedReply, emptySet()) + suspend fun deleteComment(deletedReply: Id) { + Table.remove(CommentReplyLink::replyingPost eq deletedReply) + } suspend fun getReplies(original: Id): List> { return Table.filter(CommentReplyLink::originalPost eq original) diff --git a/src/main/kotlin/info/mechyrdia/data/data.kt b/src/main/kotlin/info/mechyrdia/data/data.kt index ffc0d4d..d2bb3cf 100644 --- a/src/main/kotlin/info/mechyrdia/data/data.kt +++ b/src/main/kotlin/info/mechyrdia/data/data.kt @@ -11,7 +11,8 @@ import kotlinx.coroutines.runBlocking import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder @@ -38,24 +39,19 @@ value class Id(val id: String) { } } -fun Id.reinterpret() = Id(id) - private val secureRandom = SecureRandom.getInstanceStrong() private val alphabet = "ABCDEFGHILMNOPQRSTVXYZ0123456789".toCharArray() fun Id() = Id(NanoIdUtils.randomNanoId(secureRandom, alphabet, 24)) object IdSerializer : KSerializer> { - private val inner = String.serializer() - - override val descriptor: SerialDescriptor - get() = inner.descriptor + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Id", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: Id<*>) { - inner.serialize(encoder, value.id) + encoder.encodeString(value.id) } override fun deserialize(decoder: Decoder): Id<*> { - return Id(inner.deserialize(decoder)) + return Id(decoder.decodeString()) } } @@ -103,8 +99,8 @@ interface DataDocument> { val id: Id } -class DocumentTable>(val kclass: KClass) { - private suspend fun collection() = ConnectionHolder.getDatabase().database.getCollection(kclass.simpleName!!, kclass.java).coroutine +class DocumentTable>(val kClass: KClass) { + private suspend fun collection() = ConnectionHolder.getDatabase().database.getCollection(kClass.simpleName!!, kClass.java).coroutine suspend fun index(vararg properties: KProperty1) { collection().ensureIndex(*properties) diff --git a/src/main/kotlin/info/mechyrdia/data/nations.kt b/src/main/kotlin/info/mechyrdia/data/nations.kt index c90e736..c808d9e 100644 --- a/src/main/kotlin/info/mechyrdia/data/nations.kt +++ b/src/main/kotlin/info/mechyrdia/data/nations.kt @@ -44,6 +44,14 @@ data class NationData( } } +val CallNationCacheAttribute = AttributeKey, NationData>>("NationCache") + +val ApplicationCall.nationCache: MutableMap, NationData> + get() = attributes.getOrNull(CallNationCacheAttribute) + ?: mutableMapOf, NationData>().also { + attributes.put(CallNationCacheAttribute, it) + } + suspend fun MutableMap, NationData>.getNation(id: Id): NationData { return getOrPut(id) { NationData.get(id) @@ -56,6 +64,6 @@ suspend fun ApplicationCall.currentNation(): NationData? { attributes.getOrNull(CallCurrentNationAttribute)?.let { return it } return sessions.get()?.nationId?.let { id -> - NationData.get(id) + nationCache.getNation(id) }?.also { attributes.put(CallCurrentNationAttribute, it) } } diff --git a/src/main/kotlin/info/mechyrdia/data/view_comments.kt b/src/main/kotlin/info/mechyrdia/data/view_comments.kt index 427835f..390967e 100644 --- a/src/main/kotlin/info/mechyrdia/data/view_comments.kt +++ b/src/main/kotlin/info/mechyrdia/data/view_comments.kt @@ -8,6 +8,7 @@ import info.mechyrdia.lore.TextParserState import info.mechyrdia.lore.dateTime import io.ktor.server.application.* import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.html.* import java.time.Instant @@ -47,7 +48,7 @@ data class CommentRenderData( replyLinks = CommentReplyLink.getReplies(comment.id), ) } - }.map { it.await() } + }.awaitAll() } } } diff --git a/src/main/kotlin/info/mechyrdia/data/views_comment.kt b/src/main/kotlin/info/mechyrdia/data/views_comment.kt index 28c590d..be3a69f 100644 --- a/src/main/kotlin/info/mechyrdia/data/views_comment.kt +++ b/src/main/kotlin/info/mechyrdia/data/views_comment.kt @@ -30,7 +30,13 @@ suspend fun ApplicationCall.recentCommentsPage(): HTML.() -> Unit { ).formUrlEncode() ) - val comments = CommentRenderData(Comment.Table.sorted(descending(Comment::submittedAt)).take(limit).toList()) + val comments = CommentRenderData( + Comment.Table + .sorted(descending(Comment::submittedAt)) + .take(limit) + .toList(), + nationCache + ) return page("Recent Comments", standardNavBar()) { section { @@ -89,6 +95,12 @@ suspend fun ApplicationCall.viewCommentRoute(): Nothing { val comment = Comment.Table.get(commentId)!! + val currentNation = currentNation() + val submitter = nationCache.getNation(comment.submittedBy) + + if (submitter.isBanned && currentNation?.id != comment.submittedBy && currentNation?.id != OwnerNationId) + redirectWithError("/lore/${comment.submittedIn}", "The user who posted that comment is banned from commenting") + redirect("/lore/${comment.submittedIn}#comment-$commentId") } @@ -130,7 +142,7 @@ suspend fun ApplicationCall.deleteCommentPage(): HTML.() -> Unit { if (currNation.id != comment.submittedBy && currNation.id != OwnerNationId) throw ForbiddenException("Illegal attempt by ${currNation.id} to delete comment by ${comment.submittedBy}") - val commentDisplay = CommentRenderData(listOf(comment), mutableMapOf(currNation.id to currNation)).single() + val commentDisplay = CommentRenderData(listOf(comment), nationCache).single() return page("Confirm Deletion of Commment", standardNavBar()) { section { diff --git a/src/main/kotlin/info/mechyrdia/data/views_user.kt b/src/main/kotlin/info/mechyrdia/data/views_user.kt index d5838b1..beaf1f9 100644 --- a/src/main/kotlin/info/mechyrdia/data/views_user.kt +++ b/src/main/kotlin/info/mechyrdia/data/views_user.kt @@ -3,6 +3,7 @@ package info.mechyrdia.data import info.mechyrdia.OwnerNationId import info.mechyrdia.auth.createCsrfToken import info.mechyrdia.auth.verifyCsrfToken +import info.mechyrdia.lore.NationProfileSidebar import info.mechyrdia.lore.page import info.mechyrdia.lore.redirect import info.mechyrdia.lore.standardNavBar @@ -13,14 +14,14 @@ import org.litote.kmongo.setValue suspend fun ApplicationCall.userPage(): HTML.() -> Unit { val currNation = currentNation() - val viewingNation = NationData.get(Id(parameters["id"]!!)) + val viewingNation = nationCache.getNation(Id(parameters["id"]!!)) val comments = CommentRenderData( Comment.getCommentsBy(viewingNation.id).toList(), - mutableMapOf(viewingNation.id to viewingNation) + nationCache ) - return page(viewingNation.name, standardNavBar()) { + return page(viewingNation.name, standardNavBar(), NationProfileSidebar(viewingNation)) { section { a { id = "page-top" } h1 { +viewingNation.name } @@ -55,9 +56,10 @@ suspend fun ApplicationCall.adminBanUserRoute(): Nothing { verifyCsrfToken() - val bannedNation = NationData.get(Id(parameters["id"]!!)) + val bannedNation = nationCache.getNation(Id(parameters["id"]!!)) - NationData.Table.set(bannedNation.id, setValue(NationData::isBanned, true)) + if (!bannedNation.isBanned) + NationData.Table.set(bannedNation.id, setValue(NationData::isBanned, true)) redirect("/user/${bannedNation.id}") } @@ -69,9 +71,10 @@ suspend fun ApplicationCall.adminUnbanUserRoute(): Nothing { verifyCsrfToken() - val bannedNation = NationData.get(Id(parameters["id"]!!)) + val bannedNation = nationCache.getNation(Id(parameters["id"]!!)) - NationData.Table.set(bannedNation.id, setValue(NationData::isBanned, false)) + if (bannedNation.isBanned) + NationData.Table.set(bannedNation.id, setValue(NationData::isBanned, false)) redirect("/user/${bannedNation.id}") } diff --git a/src/main/kotlin/info/mechyrdia/lore/article_listing.kt b/src/main/kotlin/info/mechyrdia/lore/article_listing.kt index 025183c..a67db38 100644 --- a/src/main/kotlin/info/mechyrdia/lore/article_listing.kt +++ b/src/main/kotlin/info/mechyrdia/lore/article_listing.kt @@ -28,3 +28,13 @@ fun List.renderInto(list: UL, base: String? = null) { } } } + +fun String.toFriendlyIndexTitle() = split('/') + .joinToString(separator = " - ") { part -> + part.toFriendlyPageTitle() + } + +fun String.toFriendlyPageTitle() = split('-') + .joinToString(separator = " ") { word -> + word.lowercase().replaceFirstChar { it.titlecase() } + } diff --git a/src/main/kotlin/info/mechyrdia/lore/parser_reply.kt b/src/main/kotlin/info/mechyrdia/lore/parser_reply.kt index 16f2620..8dee964 100644 --- a/src/main/kotlin/info/mechyrdia/lore/parser_reply.kt +++ b/src/main/kotlin/info/mechyrdia/lore/parser_reply.kt @@ -16,7 +16,9 @@ class CommentRepliesBuilder { enum class TextParserReplyCounterTag(val type: TextParserTagType) { REPLY( TextParserTagType.Indirect { _, content, builder -> - builder.addReplyTag(Id(sanitizeLink(content))) + sanitizeId(content)?.let { id -> + builder.addReplyTag(Id(id)) + } "[reply]$content[/reply]" } ); diff --git a/src/main/kotlin/info/mechyrdia/lore/parser_tags.kt b/src/main/kotlin/info/mechyrdia/lore/parser_tags.kt index 3a1cbc1..cdd6f15 100644 --- a/src/main/kotlin/info/mechyrdia/lore/parser_tags.kt +++ b/src/main/kotlin/info/mechyrdia/lore/parser_tags.kt @@ -318,9 +318,9 @@ enum class TextParserCommentTags(val type: TextParserTagType) { REPLY( TextParserTagType.Indirect { _, content, _ -> - val id = sanitizeLink(content) - - ">>$id" + sanitizeId(content)?.let { id -> + ">>$id" + } ?: "[reply]$content[/reply]" } ), @@ -343,6 +343,9 @@ val NON_LINK_CHAR = Regex("[^#a-zA-Z\\d\\-._]") val DOT_CHARS = Regex("\\.+") fun sanitizeLink(html: String) = html.replace(NON_LINK_CHAR, "").replace(DOT_CHARS, ".") +val ID_REGEX = Regex("[A-IL-TVX-Z0-9]{24}") +fun sanitizeId(html: String) = ID_REGEX.matchEntire(html)?.value + fun getSizeParam(tagParam: String?): Pair = tagParam?.let { resolution -> val parts = resolution.split('x') parts.getOrNull(0)?.toIntOrNull() to parts.getOrNull(1)?.toIntOrNull() diff --git a/src/main/kotlin/info/mechyrdia/lore/preparser.kt b/src/main/kotlin/info/mechyrdia/lore/preparser.kt index 8cfe070..32b28ec 100644 --- a/src/main/kotlin/info/mechyrdia/lore/preparser.kt +++ b/src/main/kotlin/info/mechyrdia/lore/preparser.kt @@ -2,13 +2,11 @@ package info.mechyrdia.lore import com.samskivert.mustache.Escapers import com.samskivert.mustache.Mustache -import com.samskivert.mustache.Template import info.mechyrdia.Configuration import info.mechyrdia.JsonFileCodec import io.ktor.util.* import kotlinx.serialization.json.* import java.io.File -import java.security.MessageDigest @JvmInline value class JsonPath private constructor(private val pathElements: List) { @@ -76,8 +74,6 @@ object PreParser { .defaultValue("{{ MISSING VALUE \"{{name}}\" }}") .withLoader { File(Configuration.CurrentConfiguration.templateDir, "$it.tpl").bufferedReader() } - private val cache = mutableMapOf() - private fun convertJson(json: JsonElement, currentFile: File): Any? = when (json) { JsonNull -> null is JsonPrimitive -> if (json.isString) { @@ -98,13 +94,9 @@ object PreParser { convertJson(JsonFileCodec.parseToJsonElement(file.readText()), file) } - private val msgDigest = ThreadLocal.withInitial { MessageDigest.getInstance("SHA-256") } - fun preparse(name: String, content: String): String { return try { - val contentHash = hex(msgDigest.get().digest(content.toByteArray())) - val template = cache[contentHash] ?: compiler.compile(content) - + val template = compiler.compile(content) val context = loadJsonContext(name) template.execute(context) } catch (ex: RuntimeException) { diff --git a/src/main/kotlin/info/mechyrdia/lore/view_bar.kt b/src/main/kotlin/info/mechyrdia/lore/view_bar.kt index aba1c2e..4859c25 100644 --- a/src/main/kotlin/info/mechyrdia/lore/view_bar.kt +++ b/src/main/kotlin/info/mechyrdia/lore/view_bar.kt @@ -1,8 +1,7 @@ package info.mechyrdia.lore -import kotlinx.html.ASIDE -import kotlinx.html.TagConsumer -import kotlinx.html.div +import info.mechyrdia.data.NationData +import kotlinx.html.* abstract class Sidebar { protected abstract fun TagConsumer<*>.display() @@ -20,3 +19,13 @@ data class PageNavSidebar(val contents: List) : Sidebar() { } } } + +data class NationProfileSidebar(val nationData: NationData) : Sidebar() { + override fun TagConsumer<*>.display() { + img(src = nationData.flag, alt = "Flag of ${nationData.name}", classes = "flag-icon") + p { + style = "text-align:center" + +nationData.name + } + } +} diff --git a/src/main/kotlin/info/mechyrdia/lore/views_error.kt b/src/main/kotlin/info/mechyrdia/lore/views_error.kt index 6c6771d..2acb954 100644 --- a/src/main/kotlin/info/mechyrdia/lore/views_error.kt +++ b/src/main/kotlin/info/mechyrdia/lore/views_error.kt @@ -3,10 +3,7 @@ package info.mechyrdia.lore import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.request.* -import kotlinx.html.HTML -import kotlinx.html.a -import kotlinx.html.h1 -import kotlinx.html.p +import kotlinx.html.* suspend fun ApplicationCall.error400(): HTML.() -> Unit = page("400 Bad Request", standardNavBar()) { section { @@ -22,9 +19,16 @@ suspend fun ApplicationCall.error403(): HTML.() -> Unit = page("403 Forbidden", } } -suspend fun ApplicationCall.error403PageExpired(): HTML.() -> Unit = page("Page Expired", standardNavBar()) { +suspend fun ApplicationCall.error403PageExpired(formData: Parameters): HTML.() -> Unit = page("Page Expired", standardNavBar()) { section { h1 { +"Page Expired" } + formData["comment"]?.let { commentData -> + p { +"The comment you tried to submit had been preserved here:" } + textArea { + readonly = true + +commentData + } + } p { +"The page you were on has expired." request.header(HttpHeaders.Referrer)?.let { referrer -> @@ -39,7 +43,11 @@ suspend fun ApplicationCall.error403PageExpired(): HTML.() -> Unit = page("Page suspend fun ApplicationCall.error404(): HTML.() -> Unit = page("404 Not Found", standardNavBar()) { section { h1 { +"404 Not Found" } - p { +"Unfortunately, we could not find what you were looking for." } + p { + +"Unfortunately, we could not find what you were looking for. Would you like to " + a(href = "/") { +"return to the index page" } + +"?" + } } } diff --git a/src/main/kotlin/info/mechyrdia/lore/views_lore.kt b/src/main/kotlin/info/mechyrdia/lore/views_lore.kt index 9b633cf..6ca8c9a 100644 --- a/src/main/kotlin/info/mechyrdia/lore/views_lore.kt +++ b/src/main/kotlin/info/mechyrdia/lore/views_lore.kt @@ -16,20 +16,46 @@ suspend fun ApplicationCall.loreArticlePage(): HTML.() -> Unit { val pageFile = File(Configuration.CurrentConfiguration.articleDir).combineSafe(pagePath) val pageNode = pageFile.toArticleNode() + val (canCommentAs, comments) = coroutineScope { + val canCommentAs = async { currentNation() } + val comments = async { + CommentRenderData(Comment.getCommentsIn(pagePath).toList(), nationCache) + } + + canCommentAs.await() to comments.await() + } + if (pageFile.isDirectory) { val navbar = standardNavBar(pagePathParts.takeIf { it.isNotEmpty() }) - val title = pagePath.takeIf { it.isNotEmpty() } ?: "Mechyrdia Infobase" + val title = pagePath.takeIf { it.isNotEmpty() }?.toFriendlyIndexTitle() ?: "Mechyrdia Infobase" + + val sidebar = PageNavSidebar( + listOf( + NavLink("#page-top", title, aClasses = "left"), + NavLink("#comments", "Comments", aClasses = "left") + ) + ) - return page(title, navbar, null) { + return page(title, navbar, sidebar) { section { + a { id = "page-top" } h1 { +title } ul { pageNode.subNodes.renderInto(this, pagePath.takeIf { it.isNotEmpty() }) } } + section { + h2 { + a { id = "comments" } + +"Comments" + } + commentInput(pagePath, canCommentAs) + for (comment in comments) + commentBox(comment, canCommentAs?.id) + } } - } else { + } else if (pageFile.isFile) { val pageTemplate = pageFile.readText() val pageMarkup = PreParser.preparse(pagePath, pageTemplate) val pageHtml = TextParserState.parseText(pageMarkup, TextParserFormattingTag.asTags, Unit) @@ -39,15 +65,6 @@ suspend fun ApplicationCall.loreArticlePage(): HTML.() -> Unit { val pageNav = pageToC.toNavBar() + NavLink("#comments", "Comments", aClasses = "left") - val (canCommentAs, comments) = coroutineScope { - val canCommentAs = async { currentNation() } - val comments = async { - CommentRenderData(Comment.getCommentsIn(pagePath).toList()) - } - - canCommentAs.await() to comments.await() - } - val navbar = standardNavBar(pagePathParts) val sidebar = PageNavSidebar(pageNav) @@ -66,5 +83,37 @@ suspend fun ApplicationCall.loreArticlePage(): HTML.() -> Unit { commentBox(comment, canCommentAs?.id) } } + } else { + val title = pagePathParts.last().toFriendlyPageTitle() + + val navbar = standardNavBar(pagePathParts) + + val sidebar = PageNavSidebar( + listOf( + NavLink("#page-top", title, aClasses = "left"), + NavLink("#comments", "Comments", aClasses = "left") + ) + ) + + return page(title, navbar, sidebar) { + section { + a { id = "page-top" } + h1 { +title } + p { + +"This factbook does not exist. Would you like to " + a(href = "/") { +"return to the index page" } + +"?" + } + } + section { + h2 { + a { id = "comments" } + +"Comments" + } + commentInput(pagePath, canCommentAs) + for (comment in comments) + commentBox(comment, canCommentAs?.id) + } + } } } diff --git a/src/main/kotlin/info/mechyrdia/lore/views_prefs.kt b/src/main/kotlin/info/mechyrdia/lore/views_prefs.kt index cd03f1a..02558ed 100644 --- a/src/main/kotlin/info/mechyrdia/lore/views_prefs.kt +++ b/src/main/kotlin/info/mechyrdia/lore/views_prefs.kt @@ -13,7 +13,7 @@ suspend fun ApplicationCall.changeThemePage(): HTML.() -> Unit { else -> null } - return page("Client Preferences", standardNavBar(), null) { + return page("Client Preferences", standardNavBar()) { section { h1 { +"Client Preferences" } form(action = "/change-theme", method = FormMethod.post) { diff --git a/src/main/resources/static/style.css b/src/main/resources/static/style.css index 002e495..a57e786 100644 --- a/src/main/resources/static/style.css +++ b/src/main/resources/static/style.css @@ -505,6 +505,7 @@ main > section, main > nav.mobile, main > aside.mobile { aside.mobile img { margin: auto; display: block; + width: 50%; } @media only screen and (min-width: 8in) { @@ -599,6 +600,10 @@ aside.mobile img { max-height: calc(92vh - 2.5rem); } + aside.desktop img { + width: 100%; + } + aside.desktop div.list { max-height: calc(92vh - 7.5rem); overflow-y: auto; @@ -873,6 +878,12 @@ textarea.lang-tylan { font-family: 'Noto Sans Gothic', sans-serif; } +.flag-icon { + object-fit: cover; + aspect-ratio: 1; + border-radius: 50%; +} + .comment-input { border: 0.25em solid var(--comment-stroke); background-color: var(--comment-fill); @@ -893,10 +904,7 @@ textarea.lang-tylan { } .comment-box > .comment-author > .flag-icon { - object-fit: cover; width: 2em; - height: 2em; - border-radius: 1em; flex-grow: 0; flex-shrink: 0;