Correctly pluralize page-view numbers AND implement factbook page caching
authorLanius Trolling <lanius@laniustrolling.dev>
Thu, 19 Oct 2023 15:18:08 +0000 (11:18 -0400)
committerLanius Trolling <lanius@laniustrolling.dev>
Thu, 19 Oct 2023 15:18:08 +0000 (11:18 -0400)
src/main/kotlin/info/mechyrdia/Factbooks.kt
src/main/kotlin/info/mechyrdia/auth/views_login.kt
src/main/kotlin/info/mechyrdia/data/views_comment.kt
src/main/kotlin/info/mechyrdia/data/visits.kt
src/main/kotlin/info/mechyrdia/lore/views_lore.kt
src/main/kotlin/info/mechyrdia/lore/views_prefs.kt
src/main/resources/static/init.js

index 93e5626133a4a86e88942f159ca55ca3322c105d..d98cbf3a6fa00f8181a28dc246c4d2ea64968b05 100644 (file)
@@ -49,20 +49,22 @@ fun Application.factbooks() {
        install(XForwardedHeaders)
        
        install(CachingHeaders) {
-               options { _, outgoingContent ->
+               options { call, outgoingContent ->
                        if (outgoingContent is JarFileContent)
                                CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 3600))
-                       else
-                               null
+                       else if (call.attributes.getOrNull(PageDoNotCacheAttributeKey) == true)
+                               CachingOptions(CacheControl.NoStore(null))
+                       else null
                }
        }
        
        install(ConditionalHeaders) {
-               version { _, outgoingContent ->
+               version { call, outgoingContent ->
                        if (outgoingContent is LocalFileContent)
                                listOf(LastModifiedVersion(outgoingContent.file.lastModified()))
-                       else
-                               emptyList()
+                       else call.attributes.getOrNull(FactbookLastModifiedAttributeKey)?.let { lastModified ->
+                               listOf(LastModifiedVersion(lastModified.toEpochMilli()))
+                       } ?: emptyList()
                }
        }
        
index cce96bfbcf87500b72a4e7f9f55bedcd1d274718..3cd4df130066348d4c40ea8b333b5dddfec3aa11 100644 (file)
@@ -11,51 +11,58 @@ import io.ktor.server.application.*
 import io.ktor.server.plugins.*
 import io.ktor.server.sessions.*
 import io.ktor.server.util.*
+import io.ktor.util.*
 import kotlinx.html.*
 import java.util.concurrent.ConcurrentHashMap
 
+val PageDoNotCacheAttributeKey = AttributeKey<Boolean>("Mechyrdia.PageDoNotCache")
+
 private val nsTokenMap = ConcurrentHashMap<String, String>()
 
-suspend fun ApplicationCall.loginPage(): HTML.() -> Unit = page("Log In With NationStates", standardNavBar()) {
-       val tokenKey = token()
-       val nsToken = token()
-       
-       nsTokenMap[tokenKey] = nsToken
+suspend fun ApplicationCall.loginPage(): HTML.() -> Unit {
+       attributes.put(PageDoNotCacheAttributeKey, true)
        
-       section {
-               h1 { +"Log In With NationStates" }
-               form(method = FormMethod.post, action = "/auth/login") {
-                       installCsrfToken(createCsrfToken())
-                       
-                       hiddenInput {
-                               name = "token"
-                               value = tokenKey
-                       }
-                       
-                       label {
-                               +"Nation Name"
-                               br
-                               textInput {
-                                       name = "nation"
-                                       placeholder = "Name of your nation without pretitle, e.g. Mechyrdia, Valentine Z, Reinkalistan, etc."
+       return page("Log In With NationStates", standardNavBar()) {
+               val tokenKey = token()
+               val nsToken = token()
+               
+               nsTokenMap[tokenKey] = nsToken
+               
+               section {
+                       h1 { +"Log In With NationStates" }
+                       form(method = FormMethod.post, action = "/auth/login") {
+                               installCsrfToken(createCsrfToken())
+                               
+                               hiddenInput {
+                                       name = "token"
+                                       value = tokenKey
                                }
-                       }
-                       p {
-                               style = "text-align:center"
-                               button(classes = "view-checksum") {
-                                       attributes["data-token"] = "mechyrdia_$nsToken"
-                                       +"View Your Checksum"
+                               
+                               label {
+                                       +"Nation Name"
+                                       br
+                                       textInput {
+                                               name = "nation"
+                                               placeholder = "Name of your nation without pretitle, e.g. Mechyrdia, Valentine Z, Reinkalistan, etc."
+                                       }
                                }
-                       }
-                       label {
-                               +"Verification Checksum"
-                               br
-                               textInput {
-                                       name = "checksum"
-                                       placeholder = "The random text checksum generated by NationStates for verification"
+                               p {
+                                       style = "text-align:center"
+                                       button(classes = "view-checksum") {
+                                               attributes["data-token"] = "mechyrdia_$nsToken"
+                                               +"View Your Checksum"
+                                       }
+                               }
+                               label {
+                                       +"Verification Checksum"
+                                       br
+                                       textInput {
+                                               name = "checksum"
+                                               placeholder = "The random text checksum generated by NationStates for verification"
+                                       }
                                }
+                               submitInput { value = "Log In" }
                        }
-                       submitInput { value = "Log In" }
                }
        }
 }
index 6c3710e9cfae6165d67076128b415a9d83f6c2c8..89bb037dcb249238bea6b9ce36439668f8a98809 100644 (file)
@@ -442,7 +442,7 @@ suspend fun ApplicationCall.commentHelpPage(): HTML.() -> Unit = page("Commentin
                pre { +tableDemoMarkup }
                unsafe { raw(tableDemoHtml) }
                p {
-                       +"The format goes as [td=(width)x(height)] or [th=(width)x(height)]. If one is omitted, then the format can be [td=(width)] or [td=x(height)]"
+                       +"The format goes as [td=(width)x(height)] or [th=(width)x(height)]. If one parameter is omitted (assumed to be 1), then the format can be [td=(width)] or [td=x(height)]"
                }
                table {
                        thead {
@@ -518,7 +518,7 @@ suspend fun ApplicationCall.commentHelpPage(): HTML.() -> Unit = page("Commentin
                                        td {
                                                +"Writes text in the Pokhwalish alphabet: "
                                                span(classes = "lang-pokhwal") {
-                                                       +"poKvalsqo jArgo"
+                                                       +PokhwalishAlphabet.pokhwalToFontAlphabet("pokhvalsqo jargo")
                                                }
                                        }
                                }
index 021cae857eb964c9ff4d3ee0ed36b0fb4cd7a7b1..1b7620fba9476da0607f86d4aee3cd48b83f9c23 100644 (file)
@@ -107,11 +107,13 @@ suspend fun ApplicationCall.processGuestbook(): PageVisitTotals {
        return totals
 }
 
+fun Long.pluralize(singular: String, plural: String = singular + "s") = if (this == 1L) singular else plural
+
 fun FlowContent.guestbook(totalsData: PageVisitTotals) {
        p {
                style = "font-size:0.8em"
                
-               +"This page has been visited ${totalsData.total} times by ${totalsData.totalUnique} unique visitors, most recently "
+               +"This page has been visited ${totalsData.total} ${totalsData.total.pluralize("time")} by ${totalsData.totalUnique} unique ${totalsData.totalUnique.pluralize("visitor")}, most recently "
                
                val mostRecent = totalsData.mostRecent
                if (mostRecent == null)
index 6b132f3151eaa4321d02a2f06e48067eb064ce27..2c3ecad84781474ace6790c760e232b258d0626b 100644 (file)
@@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.toList
 import kotlinx.html.*
 import kotlinx.serialization.Serializable
 import java.io.File
+import java.time.Instant
 
 @Serializable
 data class IntroMetaData(
@@ -22,6 +23,18 @@ data class IntroMetaData(
                get() = OpenGraphData(desc, image)
 }
 
+val FactbookLastModifiedAttributeKey = AttributeKey<Instant>("Mechyrdia.FactbookLastModified")
+
+private val File.lastSubFilesModified: Instant?
+       get() = if (isDirectory)
+               (listFiles()!!.mapNotNull {
+                       it.lastSubFilesModified
+               } + Instant.ofEpochMilli(lastModified())!!).max()
+       else null
+
+private val File.lastContentModified: Instant
+       get() = lastSubFilesModified ?: Instant.ofEpochMilli(lastModified())!!
+
 suspend fun ApplicationCall.loreIntroPage(): HTML.() -> Unit {
        val metaJsonFile = File(Configuration.CurrentConfiguration.articleDir).parentFile.combineSafe("introMeta.json")
        val metaData = JsonFileCodec.decodeFromString(IntroMetaData.serializer(), metaJsonFile.readText())
@@ -29,6 +42,14 @@ suspend fun ApplicationCall.loreIntroPage(): HTML.() -> Unit {
        val htmlFile = File(Configuration.CurrentConfiguration.articleDir).parentFile.combineSafe("intro.html")
        val fileHtml = htmlFile.readText()
        
+       attributes.put(
+               FactbookLastModifiedAttributeKey,
+               maxOf(
+                       Instant.ofEpochMilli(htmlFile.lastModified()),
+                       Instant.ofEpochMilli(metaJsonFile.lastModified())
+               )
+       )
+       
        return page(metaData.title, standardNavBar(), null, metaData.ogData) {
                section {
                        a { id = "page-top" }
@@ -54,6 +75,13 @@ suspend fun ApplicationCall.loreArticlePage(): HTML.() -> Unit {
                canCommentAs.await() to comments.await()
        }
        
+       if (pageFile.exists())
+               attributes.put(FactbookLastModifiedAttributeKey,
+                       (comments.map { comment ->
+                               comment.lastEdit ?: comment.submittedAt
+                       } + pageFile.lastContentModified).max()
+               )
+       
        if (pageFile.isDirectory) {
                val navbar = standardNavBar(pagePathParts.takeIf { it.isNotEmpty() })
                
index 44bcefd69c5209c8af2362b28cebba51569d73d4..6e8894fa7b0929d57e83654c81411e9dce3707bb 100644 (file)
@@ -1,5 +1,6 @@
 package info.mechyrdia.lore
 
+import info.mechyrdia.auth.PageDoNotCacheAttributeKey
 import info.mechyrdia.auth.createCsrfToken
 import info.mechyrdia.auth.installCsrfToken
 import info.mechyrdia.auth.verifyCsrfToken
@@ -7,6 +8,8 @@ import io.ktor.server.application.*
 import kotlinx.html.*
 
 suspend fun ApplicationCall.changeThemePage(): HTML.() -> Unit {
+       attributes.put(PageDoNotCacheAttributeKey, true)
+       
        val theme = when (request.cookies["FACTBOOK_THEME"]) {
                "light" -> "light"
                "dark" -> "dark"
index 4acac5189e4e74c98f3b47a54aaacf65ae7f6501..4e0a82bfda470cc1e620b2d1b230f92678b16411 100644 (file)
                        thirdCell.style.textAlign = "center";
                        const beginLink = thirdCell.appendChild(document.createElement("a"));
                        beginLink.href = "#";
-                       beginLink.append("Begin Quiz");
+                       beginLink.append("Begin Quiz (" + quiz.questions.length + " questions)");
                        beginLink.onclick = e => {
                                e.preventDefault();
                                quizFunctions.renderQuestion(0);
                        firstCell.style.textAlign = "center";
                        firstCell.style.fontSize = "1.5em";
                        firstCell.style.fontWeight = "bold";
+                       firstCell.append("Question " + (index + 1) + "/" + quiz.questions.length);
+                       firstCell.append(document.createElement("br"));
                        firstCell.append(question.asks.toString());
                        firstRow.appendChild(firstCell);
                        quizRoot.appendChild(firstRow);