Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spicy Morenitta (Dashboard) and Web API #2462

Draft
wants to merge 10 commits into
base: cinnamon
Choose a base branch
from
8 changes: 8 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,12 @@ allprojects {
maven("https://oss.sonatype.org/content/repositories/snapshots/")
maven("https://repo.perfectdreams.net/")
}
}

// Workaround for https://kotlinlang.slack.com/archives/C0B8L3U69/p1633590092096600
// Remove this when Loritta updates to Kotlin 1.6.0+
rootProject.plugins.withType<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin> {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension>().apply {
resolution("@webpack-cli/serve", "1.5.2")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package net.perfectdreams.loritta.cinnamon.pudding.data.discord

import kotlinx.serialization.Serializable

@Serializable
class PartialDiscordGuild(
val id: ULong,
val name: String,
val icon: String?,
val owner: Boolean,
val permissions: String,
val features: List<String>
)
6 changes: 6 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ pluginManagement {
repositories {
gradlePluginPortal()
maven("https://repo.perfectdreams.net/")
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
}

rootProject.name = "cinnamon-parent"

include(":common")

// ===[ WEB ]===
include(":web:web-api-data")
include(":web:web-api")
include(":web:dashboard")

// ===[ PUDDING ]===
// The reason this is not named "common" is because IDEA was getting a bit confusing due to duplicated names
// (errors related to class not found)
Expand Down
33 changes: 33 additions & 0 deletions web/dashboard/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Add compose gradle plugin
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose") version "1.0.0-alpha4-build362"
}

// Add maven repositories
repositories {
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
google()
}

// Enable JS(IR) target and add dependencies
kotlin {
js(IR) {
browser()
binaries.executable()
}
sourceSets {
val jsMain by getting {
dependencies {
implementation(compose.web.core)
implementation(compose.runtime)
implementation(project(":web:web-api-data"))
implementation("io.ktor:ktor-client-js:1.6.5")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1")
implementation("com.ionspin.kotlin:bignum:0.3.3")
implementation("net.perfectdreams.i18nhelper.formatters:intl-messageformat-js:0.0.2-SNAPSHOT")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@

import io.ktor.client.*
import kotlinx.browser.document
import kotlinx.dom.addClass
import net.perfectdreams.loritta.spicymorenitta.dashboard.components.userdash.UserOverview
import net.perfectdreams.loritta.spicymorenitta.dashboard.screen.Screen
import net.perfectdreams.loritta.spicymorenitta.dashboard.utils.AppState
import net.perfectdreams.loritta.spicymorenitta.dashboard.utils.RoutingManager
import net.perfectdreams.loritta.spicymorenitta.dashboard.utils.State
import org.jetbrains.compose.web.renderComposable
import org.w3c.dom.HTMLDivElement

val http = HttpClient {}

class SpicyMorenitta {
val routingManager = RoutingManager(this)
val appState = AppState(this)
val spaLoadingWrapper by lazy { document.getElementById("spa-loading-wrapper") as HTMLDivElement? }

fun start() {
routingManager.switchToUserOverview()
appState.loadData()

renderComposable(rootElementId = "root") {
val sessionToken = appState.sessionToken
val language = appState.i18nContext
if (sessionToken is State.Success && language is State.Success) {
// Fade out the single page application loading wrapper...
spaLoadingWrapper?.addClass("loaded")

when (val screen = routingManager.screenState) {
is Screen.UserOverview -> UserOverview(this@SpicyMorenitta, screen)
Screen.Test -> TODO()
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package net.perfectdreams.loritta.spicymorenitta.dashboard

import SpicyMorenitta

fun main() {
val m = SpicyMorenitta()
m.start()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package net.perfectdreams.loritta.spicymorenitta.dashboard.components

import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.css.Position
import org.jetbrains.compose.web.css.em
import org.jetbrains.compose.web.css.height
import org.jetbrains.compose.web.css.position
import org.jetbrains.compose.web.css.width
import org.jetbrains.compose.web.dom.Div

@Composable
fun DiscordPartnerBadge() {
Div(attrs = {
style {
position(Position.Relative)
width(1.25.em)
height(1.25.em)
}
ref {
onDispose {}
}
}) {
Div(
attrs = {
ref {
// Flower Star
it.outerHTML = "<svg style=\"position: absolute; color: rgb(0, 167, 255); top: 50%; left: 50%; transform: translate(-50%,-50%);\" xmlns=\"http://www.w3.org/2000/svg\" aria-label=\"Verified\" class=\"flowerStar-1GeTsn\" aria-hidden=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 15.2\"><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"m16 7.6c0 .79-1.28 1.38-1.52 2.09s.44 2 0 2.59-1.84.35-2.46.8-.79 1.84-1.54 2.09-1.67-.8-2.47-.8-1.75 1-2.47.8-.92-1.64-1.54-2.09-2-.18-2.46-.8.23-1.84 0-2.59-1.54-1.3-1.54-2.09 1.28-1.38 1.52-2.09-.44-2 0-2.59 1.85-.35 2.48-.8.78-1.84 1.53-2.12 1.67.83 2.47.83 1.75-1 2.47-.8.91 1.64 1.53 2.09 2 .18 2.46.8-.23 1.84 0 2.59 1.54 1.3 1.54 2.09z\"></path></svg>"
onDispose {}
}
}
)

Div(
attrs = {
ref {
// Partner
it.outerHTML = "<svg style=\"position: absolute; top: 0; left: 0; color: white; top: 50%; left: 50%; transform: translate(-50%,-50%);\" xmlns=\"http://www.w3.org/2000/svg\" class=\"icon-1ihkOt\" aria-hidden=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><path d=\"M10.5906 6.39993L9.19223 7.29993C8.99246 7.39993 8.89258 7.39993 8.69281 7.29993C8.59293 7.19993 8.39317 7.09993 8.29328 6.99993C7.89375 6.89993 7.5941 6.99993 7.29445 7.19993L6.79504 7.49993L4.29797 9.19993C3.69867 9.49993 2.99949 9.39993 2.69984 8.79993C2.30031 8.29993 2.50008 7.59993 2.99949 7.19993L5.99598 5.19993C6.79504 4.69993 7.79387 4.49993 8.69281 4.69993C9.49188 4.89993 10.0912 5.29993 10.5906 5.89993C10.7904 6.09993 10.6905 6.29993 10.5906 6.39993Z\" fill=\"currentColor\"></path><path d=\"M13.4871 7.79985C13.4871 8.19985 13.2874 8.59985 12.9877 8.79985L9.89135 10.7999C9.29206 11.1999 8.69276 11.3999 7.99358 11.3999C7.69393 11.3999 7.49417 11.3999 7.19452 11.2999C6.39545 11.0999 5.79616 10.6999 5.29674 10.0999C5.19686 9.89985 5.29674 9.69985 5.39663 9.59985L6.79499 8.69985C6.89487 8.59985 7.09463 8.59985 7.19452 8.69985C7.39428 8.79985 7.59405 8.89985 7.69393 8.99985C8.09346 8.99985 8.39311 8.99985 8.69276 8.79985L9.39194 8.39985L11.3896 6.99985L11.6892 6.79985C12.1887 6.49985 12.9877 6.59985 13.2874 7.09985C13.4871 7.39985 13.4871 7.59985 13.4871 7.79985Z\" fill=\"currentColor\"></path></svg>"
onDispose {}
}
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package net.perfectdreams.loritta.spicymorenitta.dashboard.components

import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.css.Position
import org.jetbrains.compose.web.css.em
import org.jetbrains.compose.web.css.height
import org.jetbrains.compose.web.css.position
import org.jetbrains.compose.web.css.width
import org.jetbrains.compose.web.dom.Div

@Composable
fun DiscordVerifiedBadge() {
Div(attrs = {
style {
position(Position.Relative)
width(1.25.em)
height(1.25.em)
}
ref {
onDispose {}
}
}) {
Div(
attrs = {
ref {
// Flower Star
it.outerHTML = "<svg style=\"position: absolute; color: rgb(0, 167, 255); top: 50%; left: 50%; transform: translate(-50%,-50%);\" xmlns=\"http://www.w3.org/2000/svg\" aria-label=\"Verified\" class=\"flowerStar-1GeTsn\" aria-hidden=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 15.2\"><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"m16 7.6c0 .79-1.28 1.38-1.52 2.09s.44 2 0 2.59-1.84.35-2.46.8-.79 1.84-1.54 2.09-1.67-.8-2.47-.8-1.75 1-2.47.8-.92-1.64-1.54-2.09-2-.18-2.46-.8.23-1.84 0-2.59-1.54-1.3-1.54-2.09 1.28-1.38 1.52-2.09-.44-2 0-2.59 1.85-.35 2.48-.8.78-1.84 1.53-2.12 1.67.83 2.47.83 1.75-1 2.47-.8.91 1.64 1.53 2.09 2 .18 2.46.8-.23 1.84 0 2.59 1.54 1.3 1.54 2.09z\"></path></svg>"
onDispose {}
}
}
)

Div(
attrs = {
ref {
// Verified
it.outerHTML = "<svg style=\"position: absolute; top: 0; left: 0; color: white; top: 50%; left: 50%; transform: translate(-50%,-50%);\" xmlns=\"http://www.w3.org/2000/svg\" class=\"icon-1ihkOt\" aria-hidden=\"false\" width=\"16\" height=\"16\" viewBox=\"0 0 16 15.2\"><path d=\"M7.4,11.17,4,8.62,5,7.26l2,1.53L10.64,4l1.36,1Z\" fill=\"currentColor\"></path></svg>"
onDispose {}
}
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package net.perfectdreams.loritta.spicymorenitta.dashboard.components

import androidx.compose.runtime.Composable
import net.perfectdreams.loritta.cinnamon.pudding.data.discord.PartialDiscordGuild
import net.perfectdreams.loritta.spicymorenitta.dashboard.utils.Constants
import org.jetbrains.compose.web.css.AlignItems
import org.jetbrains.compose.web.css.DisplayStyle
import org.jetbrains.compose.web.css.alignItems
import org.jetbrains.compose.web.css.display
import org.jetbrains.compose.web.dom.A
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Img
import org.jetbrains.compose.web.dom.Span
import org.jetbrains.compose.web.dom.Text

@Composable
fun GuildOverviewCard(guildData: PartialDiscordGuild) {
A(href = Constants.LORITTA_WEBSITE_URL + "/guild/${guildData.id}/configure/", attrs = { classes("guild-overview-card") }) {
Div(attrs = { classes("icon-wrapper") }) {
// TODO: Add default icon if not present
val extension = if (guildData.icon?.startsWith("a_") == true) {
"gif"
} else "webp"

val discordIconUrl = "https://cdn.discordapp.com/icons/${guildData.id}/${guildData.icon}.$extension?size=128"
Img(src = discordIconUrl ?: "")
}

Div {
var hasBadges = false
Div(attrs = {
style {
display(DisplayStyle.LegacyInlineFlex)
alignItems(AlignItems.Center)
}
}) {
// Verified badge takes priority in the Discord Client
if (guildData.features.contains("VERIFIED")) {
hasBadges = true
DiscordVerifiedBadge()
} else if (guildData.features.contains("PARTNERED")) {
hasBadges = true
DiscordPartnerBadge()
}

Span {
if (hasBadges)
Span {
Text(" ")
}

Text(guildData.name)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package net.perfectdreams.loritta.spicymorenitta.dashboard.components

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import net.perfectdreams.loritta.spicymorenitta.dashboard.screen.Screen
import net.perfectdreams.loritta.spicymorenitta.dashboard.utils.State
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.css.AlignItems
import org.jetbrains.compose.web.css.DisplayStyle
import org.jetbrains.compose.web.css.FlexDirection
import org.jetbrains.compose.web.css.alignItems
import org.jetbrains.compose.web.css.color
import org.jetbrains.compose.web.css.display
import org.jetbrains.compose.web.css.em
import org.jetbrains.compose.web.css.flexDirection
import org.jetbrains.compose.web.css.height
import org.jetbrains.compose.web.css.rgb
import org.jetbrains.compose.web.css.width
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.H2
import org.jetbrains.compose.web.dom.Input
import org.jetbrains.compose.web.dom.Text

@Composable
fun GuildOverviewCards(screen: Screen.UserOverview) {
var filter by remember { mutableStateOf("") }

H2 {
Text("Servidores")

Input(InputType.Text) {
onInput {
filter = it.value
}
}
}

// TODO: Add warning if the user doesn't have any guilds ("Are you logged in into the correct account?")
// TODO: Add warning if filter doesn't match any server
when (val state = screen.model.guilds) {
is State.Success -> {
val guilds = state.value
val filteredGuilds = guilds.filter { it.name.contains(filter, true) }
.sortedBy { it.name }
if (guilds.isNotEmpty()) {
if (filteredGuilds.isNotEmpty()) {
GuildOverviewCardsGrid(filteredGuilds)
} else {
Text("Nenhum servidor é compatível com o filtro que você selecionou!")
}
} else {
Text("Você não está em nenhum servidor! Tem certeza que você logou na conta certa?")
}
}
is State.Loading -> {
Div(
attrs = {
style {
display(DisplayStyle.Flex)
flexDirection(FlexDirection.Column)
alignItems(AlignItems.Center)
}
}
) {
Div(
attrs = {
style {
width(5.em)
height(5.em)
color(rgb(0, 167, 255))
}
}
) {
LoadingIconTailSpin()
}
Text("Carregando alguma coisa... Espero que carregue logo né")
}
}
is State.Failure -> Text("Deu ruim!")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package net.perfectdreams.loritta.spicymorenitta.dashboard.components

import androidx.compose.runtime.Composable
import net.perfectdreams.loritta.cinnamon.pudding.data.discord.PartialDiscordGuild
import org.jetbrains.compose.web.dom.Div

@Composable
fun GuildOverviewCardsGrid(guilds: List<PartialDiscordGuild>) {
Div(attrs = { classes("guild-overview-cards-grid") }) {
for (guild in guilds)
GuildOverviewCard(guild)
}
}
Loading