diff --git a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/database/DatabasePlayerProfileEdit.kt b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/database/DatabasePlayerProfileEdit.kt index 0962c3e32..398ca897c 100644 --- a/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/database/DatabasePlayerProfileEdit.kt +++ b/webapp-service-layer/src/main/kotlin/io/elephantchess/servicelayer/dto/database/DatabasePlayerProfileEdit.kt @@ -8,5 +8,6 @@ data class DatabasePlayerProfileEdit( val profileText: String?, val sources: List, val editComment: String?, - val enabled: Boolean + val enabled: Boolean, + val versionTime: Long? = null, ) diff --git a/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/DatabasePageRenderer.kt b/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/DatabasePageRenderer.kt index c2aa0d79c..f892eb791 100644 --- a/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/DatabasePageRenderer.kt +++ b/webapp/src/main/kotlin/io/elephantchess/webapp/rendering/DatabasePageRenderer.kt @@ -10,11 +10,17 @@ import io.elephantchess.servicelayer.dto.database.* import io.elephantchess.utils.cropToFirstNWords import io.elephantchess.utils.formatWithChineseName import io.github.reactivecircus.cache4k.Cache +import java.time.Instant +import java.time.ZoneOffset.UTC +import java.time.format.DateTimeFormatter +import java.util.Locale import io.ktor.http.* +import kotlinx.html.b import kotlinx.html.a import kotlinx.html.li import kotlinx.html.meta import kotlinx.html.p +import kotlinx.html.span import kotlinx.html.style import kotlinx.html.unsafe import kotlin.time.Duration.Companion.hours @@ -41,7 +47,15 @@ class DatabasePageRenderer(private val htmlRenderer: HtmlRenderer) { hasOpeningData: Boolean, ): String { val description = edit.profileText - val editors = fetchEditorsUsername() + val contributors = fetchEditorsUsername() + .map { it.trim() } + .filter { it.isNotEmpty() } + .distinct() + .sorted() + val contributorsText = contributors.joinToString(", ") + val lastEditDate = edit.versionTime + ?.let { Instant.ofEpochMilli(it).atZone(UTC).toLocalDate() } + ?.format(DateTimeFormatter.ofPattern("d MMM yyyy", Locale.ENGLISH)) val playerNameEncodedResolver = SimpleValueTagResolver("player_name_encoded", databasePlayer.urlName) val playerIdResolver = SimpleValueTagResolver("player_id", databasePlayer.id) @@ -72,11 +86,38 @@ class DatabasePageRenderer(private val htmlRenderer: HtmlRenderer) { } val authorMeta = KtorHtmlBuilderTagResolver("author_meta") { - if (editors.isNotEmpty()) { - val names = editors.sortedBy { it.lowercase() }.joinToString(", ") + if (contributors.isNotEmpty()) { meta { name = "author" - content = names + content = contributorsText + } + } + } + + val profileMetaResolver = KtorHtmlBuilderTagResolver("profile_meta_info") { + if (contributors.isNotEmpty()) { + span { + b { +"Contributors:" } + +" " + contributors.forEachIndexed { index, username -> + if (index > 0) { + +", " + } + a(href = "/@/${username.encodeURLPath()}") { + +username + } + } + } + } + if (contributors.isNotEmpty() && lastEditDate != null) { + span(classes = "profile-meta-separator") { + +" ยท " + } + } + if (lastEditDate != null) { + span { + b { +"Last edit:" } + +" $lastEditDate" } } } @@ -112,6 +153,7 @@ class DatabasePageRenderer(private val htmlRenderer: HtmlRenderer) { sourcesResolver, styleResolver, authorMeta, + profileMetaResolver, noIndexMeta, openingsHeadResolver, openingsSectionResolver diff --git a/webapp/src/main/resources/public/css/database/database-player.css b/webapp/src/main/resources/public/css/database/database-player.css index 7256feed6..bd2411238 100644 --- a/webapp/src/main/resources/public/css/database/database-player.css +++ b/webapp/src/main/resources/public/css/database/database-player.css @@ -22,6 +22,11 @@ margin-bottom: 12px; } +#profile-meta-info { + margin-bottom: 15px; + font-size: 13px; +} + #player-profile-description .profile-sources { margin-top: 20px; padding: 15px; diff --git a/webapp/src/main/resources/templates/database/database_player.html b/webapp/src/main/resources/templates/database/database_player.html index 6e561b7e0..3aefa84bf 100644 --- a/webapp/src/main/resources/templates/database/database_player.html +++ b/webapp/src/main/resources/templates/database/database_player.html @@ -27,6 +27,7 @@

Sources

+
{{profile_meta_info}}
view current version view edit history diff --git a/webapp/src/test/kotlin/io/elephantchess/webapp/rendering/DatabasePageRendererTest.kt b/webapp/src/test/kotlin/io/elephantchess/webapp/rendering/DatabasePageRendererTest.kt new file mode 100644 index 000000000..d30de838b --- /dev/null +++ b/webapp/src/test/kotlin/io/elephantchess/webapp/rendering/DatabasePageRendererTest.kt @@ -0,0 +1,78 @@ +package io.elephantchess.webapp.rendering + +import io.elephantchess.htmlrenderer.HtmlRenderer +import io.elephantchess.htmlrenderer.TemplateRenderer +import io.elephantchess.servicelayer.dto.database.DatabasePlayer +import io.elephantchess.servicelayer.dto.database.DatabasePlayerProfileEdit +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class DatabasePageRendererTest { + + private fun pageRenderer(): DatabasePageRenderer { + val templateRenderer = object : TemplateRenderer(baseTagResolvers = emptyList(), disabledTemplates = emptyList()) {} + val htmlRenderer = HtmlRenderer(isMinificationEnabled = false, cdnFolder = null, webTemplateRenderer = templateRenderer) + return DatabasePageRenderer(htmlRenderer) + } + + @Test + fun `renderPlayerPage includes contributors and last edit date`() = runTest { + val output = pageRenderer().renderPlayerPage( + databasePlayer = DatabasePlayer("player-id", "John Doe", null, null), + requestedVersion = null, + edit = DatabasePlayerProfileEdit( + playerId = "player-id", + canonicalName = "John Doe", + chineseName = null, + gender = null, + profileText = "some text", + sources = emptyList(), + editComment = null, + enabled = true, + versionTime = 1704067200000 + ), + fetchEditorsUsername = { listOf("Bob", "Alice", "Bob", "