diff options
author | Joffrey Bion <joffrey.bion@gmail.com> | 2023-02-01 03:48:39 +0100 |
---|---|---|
committer | Joffrey Bion <joffrey.bion@gmail.com> | 2023-02-01 03:48:39 +0100 |
commit | d09c3e7128fbb8b9f1500153b12ef657dcb76694 (patch) | |
tree | 343a2ed29b989d1205490b38e30b43b68b0992ef /sw-ui | |
parent | Upgrade to BlueprintJS 4 (wrapper 7) (diff) | |
download | seven-wonders-d09c3e7128fbb8b9f1500153b12ef657dcb76694.tar.gz seven-wonders-d09c3e7128fbb8b9f1500153b12ef657dcb76694.tar.bz2 seven-wonders-d09c3e7128fbb8b9f1500153b12ef657dcb76694.zip |
Migrate to new Kotlin/React API and Emotion styles
Diffstat (limited to 'sw-ui')
33 files changed, 1909 insertions, 1918 deletions
diff --git a/sw-ui/build.gradle.kts b/sw-ui/build.gradle.kts index d1c4e6a8..6fb7d8b8 100644 --- a/sw-ui/build.gradle.kts +++ b/sw-ui/build.gradle.kts @@ -20,9 +20,9 @@ kotlin { implementation(libs.kotlin.wrappers.react.dom) implementation(libs.kotlin.wrappers.react.redux) implementation(libs.kotlin.wrappers.react.router.dom) - implementation(libs.kotlin.wrappers.styled.next) implementation(libs.kotlin.wrappers.blueprintjs.core) implementation(libs.kotlin.wrappers.blueprintjs.icons) + implementation(libs.kotlin.wrappers.emotion) } } test { diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt index 44faede8..0bd3400e 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/SevenWondersUi.kt @@ -5,35 +5,33 @@ import kotlinx.coroutines.* import org.luxons.sevenwonders.ui.components.* import org.luxons.sevenwonders.ui.redux.* import org.luxons.sevenwonders.ui.redux.sagas.* -import react.dom.* +import react.* +import react.dom.client.* import react.redux.* import redux.* -import web.dom.* import web.dom.document +import web.html.* fun main() { - window.onload = { - val rootElement = document.getElementById("root") - if (rootElement != null) { - initializeAndRender(rootElement) - } else { - console.error("Element with ID 'root' was not found, cannot bootstrap react app") - } + window.onload = { init() } +} + +private fun init() { + val rootElement = document.getElementById("root") + if (rootElement == null) { + console.error("Element with ID 'root' was not found, cannot bootstrap react app") + return } + renderRoot(rootElement) } -private fun initializeAndRender(rootElement: Element) { +private fun renderRoot(rootElement: HTMLElement) { val store = initRedux() - - // With the new API this might look something like: - // createRoot(rootElement).render(FC<Props> { .. }.create()) - // See: https://github.com/karakum-team/kotlin-mui-showcase/blob/main/src/main/kotlin/team/karakum/App.kt - @Suppress("DEPRECATION") - render(rootElement) { - provider(store) { - application() - } + val connectedApp = Provider.create { + this.store = store + Application() } + createRoot(rootElement).render(connectedApp) } @OptIn(DelicateCoroutinesApi::class) diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/Application.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/Application.kt index 51e7e78f..9c210538 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/Application.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/Application.kt @@ -1,40 +1,43 @@ package org.luxons.sevenwonders.ui.components -import org.luxons.sevenwonders.ui.components.errors.errorDialog -import org.luxons.sevenwonders.ui.components.game.gameScene -import org.luxons.sevenwonders.ui.components.gameBrowser.gameBrowser -import org.luxons.sevenwonders.ui.components.home.home -import org.luxons.sevenwonders.ui.components.lobby.lobby -import org.luxons.sevenwonders.ui.router.SwRoute -import react.Props -import react.RBuilder -import react.RElementBuilder -import react.createElement -import react.router.Navigate -import react.router.Route -import react.router.Routes -import react.router.RoutesProps +import org.luxons.sevenwonders.ui.components.errors.* +import org.luxons.sevenwonders.ui.components.game.* +import org.luxons.sevenwonders.ui.components.gameBrowser.* +import org.luxons.sevenwonders.ui.components.home.* +import org.luxons.sevenwonders.ui.components.lobby.* +import org.luxons.sevenwonders.ui.router.* +import react.* +import react.router.* import react.router.dom.* -fun RBuilder.application() = HashRouter { - errorDialog() - Routes { - route(SwRoute.GAME_BROWSER.path) { gameBrowser() } - route(SwRoute.GAME.path) { gameScene() } - route(SwRoute.LOBBY.path) { lobby() } - route(SwRoute.HOME.path) { home() } - route("*") { - Navigate { - attrs.to = "/" - attrs.replace = true +val Application = VFC("Application") { + HashRouter { + ErrorDialog() + Routes { + Route { + path = SwRoute.GAME_BROWSER.path + element = GameBrowser.create() + } + Route { + path = SwRoute.GAME.path + element = GameScene.create() + } + Route { + path = SwRoute.LOBBY.path + element = Lobby.create() + } + Route { + path = SwRoute.HOME.path + element = Home.create() + } + Route { + path = "*" + element = Navigate.create { + to = "/" + replace = true + } } } } } -private fun RElementBuilder<RoutesProps>.route(path: String, render: RBuilder.() -> Unit) { - Route { - attrs.path = path - attrs.element = createElement<Props>(render) - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/GlobalStyles.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/GlobalStyles.kt index 9ba5951a..bd12537e 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/GlobalStyles.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/GlobalStyles.kt @@ -1,19 +1,20 @@ package org.luxons.sevenwonders.ui.components -import kotlinx.css.* -import kotlinx.css.properties.* -import styled.StyleSheet +import csstype.* +import emotion.css.* +import org.luxons.sevenwonders.ui.utils.* -object GlobalStyles : StyleSheet("GlobalStyles", isStatic = true) { + +object GlobalStyles { val preGameWidth = 60.rem - val zeusBackground by css { - background = "url('images/backgrounds/zeus-temple.jpg') center no-repeat" - backgroundSize = "cover" + val zeusBackground = ClassName { + background = "url('images/backgrounds/zeus-temple.jpg') center no-repeat".unsafeCast<Background>() + backgroundSize = BackgroundSize.cover } - val fullscreen by css { + val fullscreen = ClassName { position = Position.fixed top = 0.px left = 0.px @@ -22,30 +23,26 @@ object GlobalStyles : StyleSheet("GlobalStyles", isStatic = true) { overflow = Overflow.hidden } - val papyrusBackground by css { - background = "url('images/backgrounds/papyrus.jpg')" - backgroundSize = "cover" + val papyrusBackground = ClassName { + background = "url('images/backgrounds/papyrus.jpg')".unsafeCast<Background>() + backgroundSize = BackgroundSize.cover } - val fixedCenter by css { - position = Position.fixed - +centerLeftTopTransform + val centerLeftTopTransform = ClassName { + left = 50.pct + top = 50.pct + transform = translate((-50).pct, (-50).pct) } - val centerInPositionedParent by css { - position = Position.absolute - +centerLeftTopTransform + val fixedCenter = ClassName(centerLeftTopTransform) { + position = Position.fixed } - val centerLeftTopTransform by css { - left = 50.pct - top = 50.pct - transform { - translate((-50).pct, (-50).pct) - } + val centerInPositionedParent = ClassName(centerLeftTopTransform) { + position = Position.absolute } - val noPadding by css { - padding(all = 0.px) + val noPadding = ClassName { + padding = Padding(all = 0.px) } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/errors/ErrorDialog.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/errors/ErrorDialog.kt index 5ff56055..5399f60d 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/errors/ErrorDialog.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/errors/ErrorDialog.kt @@ -6,44 +6,46 @@ import kotlinx.browser.* import org.luxons.sevenwonders.ui.redux.* import org.luxons.sevenwonders.ui.router.* import react.* -import react.dom.p -import styled.* +import react.dom.html.ReactHTML.p +import react.redux.* +import redux.* -external interface ErrorDialogStateProps : PropsWithChildren { - var errorMessage: String? +val ErrorDialog = VFC { + val dispatch = useDispatch<RAction, WrapperAction>() + + ErrorDialogPresenter { + errorMessage = useSwSelector { it.fatalError } + goHome = { dispatch(Navigate(SwRoute.HOME)) } + } } -external interface ErrorDialogDispatchProps : PropsWithChildren { +private external interface ErrorDialogProps : Props { + var errorMessage: String? var goHome: () -> Unit } -external interface ErrorDialogProps : ErrorDialogDispatchProps, ErrorDialogStateProps +private val ErrorDialogPresenter = FC<ErrorDialogProps>("ErrorDialogPresenter") { props -> + val errorMessage = props.errorMessage + BpDialog { + isOpen = errorMessage != null + titleText = "Oops!" + icon = BpIcon.create { + icon = IconNames.ERROR + intent = Intent.DANGER + } + onClose = { goHomeAndRefresh() } -class ErrorDialogPresenter(props: ErrorDialogProps) : RComponent<ErrorDialogProps, State>(props) { - override fun RBuilder.render() { - val errorMessage = props.errorMessage - bpDialog( - isOpen = errorMessage != null, - title = "Oops!", - icon = IconNames.ERROR, - iconIntent = Intent.DANGER, - onClose = { goHomeAndRefresh() } - ) { - styledDiv { - css { - classes.add(Classes.DIALOG_BODY) - } - p { - +(errorMessage ?: "fatal error") - } + BpDialogBody { + p { + +(errorMessage ?: "fatal error") } - styledDiv { - css { - classes.add(Classes.DIALOG_FOOTER) - } - bpButton(icon = IconNames.LOG_OUT, onClick = { goHomeAndRefresh() }) { - +"HOME" - } + } + BpDialogFooter { + BpButton { + icon = IconNames.LOG_OUT + onClick = { goHomeAndRefresh() } + + +"HOME" } } } @@ -53,15 +55,3 @@ private fun goHomeAndRefresh() { // we don't use a redux action here because we actually want to redirect and refresh the page window.location.href = SwRoute.HOME.path } - -fun RBuilder.errorDialog() = errorDialog {} - -private val errorDialog = connectStateAndDispatch<ErrorDialogStateProps, ErrorDialogDispatchProps, ErrorDialogProps>( - clazz = ErrorDialogPresenter::class, - mapStateToProps = { state, _ -> - errorMessage = state.fatalError - }, - mapDispatchToProps = { dispatch, _ -> - goHome = { dispatch(Navigate(SwRoute.HOME)) } - }, -) diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Board.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Board.kt index c3dc6460..9ca56efa 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Board.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Board.kt @@ -1,37 +1,34 @@ package org.luxons.sevenwonders.ui.components.game -import kotlinx.css.* -import kotlinx.css.properties.* -import kotlinx.html.DIV -import kotlinx.html.HTMLTag -import kotlinx.html.IMG -import kotlinx.html.title -import org.luxons.sevenwonders.model.boards.Board -import org.luxons.sevenwonders.model.boards.Military -import org.luxons.sevenwonders.model.cards.TableCard -import org.luxons.sevenwonders.model.wonders.ApiWonder -import org.luxons.sevenwonders.model.wonders.ApiWonderStage -import react.RBuilder -import react.dom.* -import styled.StyledDOMBuilder -import styled.css -import styled.styledDiv -import styled.styledImg +import csstype.* +import emotion.react.* +import org.luxons.sevenwonders.model.boards.* +import org.luxons.sevenwonders.model.cards.* +import org.luxons.sevenwonders.model.wonders.* +import react.* +import react.dom.html.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.img +import web.html.* // card offsets in % of their size when displayed in columns private const val xOffset = 20 private const val yOffset = 21 -fun RBuilder.boardComponent(board: Board, block: StyledDOMBuilder<DIV>.() -> Unit = {}) { - styledDiv { - block() - tableCards(cardColumns = board.playedCards) - wonderComponent(wonder = board.wonder, military = board.military) +external interface BoardComponentProps : PropsWithClassName { + var board: Board +} + +val BoardComponent = FC<BoardComponentProps>("Board") { props -> + div { + className = props.className + tableCards(cardColumns = props.board.playedCards) + wonderComponent(wonder = props.board.wonder, military = props.board.military) } } -private fun RBuilder.tableCards(cardColumns: List<List<TableCard>>) { - styledDiv { +private fun ChildrenBuilder.tableCards(cardColumns: List<List<TableCard>>) { + div { css { display = Display.flex justifyContent = JustifyContent.spaceAround @@ -39,101 +36,109 @@ private fun RBuilder.tableCards(cardColumns: List<List<TableCard>>) { width = 100.pct } cardColumns.forEach { cards -> - tableCardColumn(cards = cards) { - attrs { - key = cards.first().color.toString() - } + TableCardColumn { + this.key = cards.first().color.toString() + this.cards = cards } } } } -private fun RBuilder.tableCardColumn(cards: List<TableCard>, block: StyledDOMBuilder<DIV>.() -> Unit = {}) { - styledDiv { +private external interface TableCardColumnProps : PropsWithClassName { + var cards: List<TableCard> +} + +private val TableCardColumn = FC<TableCardColumnProps>("TableCardColumn") { props -> + div { css { height = 100.pct width = 13.pct marginRight = 4.pct position = Position.relative } - block() - cards.forEachIndexed { index, card -> - tableCard(card = card, indexInColumn = index) { - attrs { key = card.name } + props.cards.forEachIndexed { index, card -> + TableCard { + this.card = card + this.indexInColumn = index + this.key = card.name } } } } -private fun RBuilder.tableCard(card: TableCard, indexInColumn: Int, block: StyledDOMBuilder<IMG>.() -> Unit = {}) { - val highlightColor = if (card.playedDuringLastMove) Color.gold else null - cardImage(card = card, highlightColor = highlightColor) { +private external interface TableCardProps : PropsWithClassName { + var card: TableCard + var indexInColumn: Int +} + +private val TableCard = FC<TableCardProps>("TableCard") { props -> + val highlightColor = if (props.card.playedDuringLastMove) NamedColor.gold else null + CardImage { + this.card = props.card + this.highlightColor = highlightColor + css { position = Position.absolute - zIndex = indexInColumn + 2 // go above the board and the built wonder cards - transform { - translate( - tx = (indexInColumn * xOffset).pct, - ty = (indexInColumn * yOffset).pct, - ) - } + zIndex = integer(props.indexInColumn + 2) // go above the board and the built wonder cards + transform = translate( + tx = (props.indexInColumn * xOffset).pct, + ty = (props.indexInColumn * yOffset).pct, + ) maxWidth = 100.pct maxHeight = 70.pct hover { - zIndex = 1000 + zIndex = integer(1000) maxWidth = 110.pct maxHeight = 75.pct hoverHighlightStyle() } } - block() } } -private fun RBuilder.wonderComponent(wonder: ApiWonder, military: Military) { - styledDiv { +private fun ChildrenBuilder.wonderComponent(wonder: ApiWonder, military: Military) { + div { css { position = Position.relative width = 100.pct height = 40.pct } - styledDiv { + div { css { position = Position.absolute left = 50.pct top = 0.px - transform { translateX((-50).pct) } + transform = translatex((-50).pct) height = 100.pct maxWidth = 95.pct // same as wonder // bring to the foreground on hover - hover { zIndex = 1000 } + hover { zIndex = integer(1000) } } - styledImg(src = "/images/wonders/${wonder.image}") { + img { + src = "/images/wonders/${wonder.image}" + title = wonder.name + alt = "Wonder ${wonder.name}" + css { - classes.add("wonder-image") - declarations["border-radius"] = "0.5%/1.5%" - boxShadow(color = Color.black, offsetX = 0.2.rem, offsetY = 0.2.rem, blurRadius = 0.5.rem) + borderRadius = "0.5%/1.5%".unsafeCast<BorderRadius>() + boxShadow = BoxShadow(color = NamedColor.black, offsetX = 0.2.rem, offsetY = 0.2.rem, blurRadius = 0.5.rem) maxHeight = 100.pct maxWidth = 100.pct - zIndex = 1 // go above the built wonder cards, but below the table cards + zIndex = integer(1) // go above the built wonder cards, but below the table cards hover { hoverHighlightStyle() } } - attrs { - this.title = wonder.name - this.alt = "Wonder ${wonder.name}" - } } - styledDiv { + div { css { position = Position.absolute top = 20.pct right = (-80).px display = Display.flex flexDirection = FlexDirection.column - alignItems = Align.start + alignItems = AlignItems.start } victoryPoints(military.victoryPoints) { css { @@ -147,7 +152,8 @@ private fun RBuilder.wonderComponent(wonder: ApiWonder, military: Military) { } } wonder.stages.forEachIndexed { index, stage -> - wonderStageElement(stage) { + WonderStageElement { + this.stage = stage css { wonderCardStyle(index, wonder.stages.size) } @@ -157,15 +163,15 @@ private fun RBuilder.wonderComponent(wonder: ApiWonder, military: Military) { } } -private fun RBuilder.victoryPoints(points: Int, block: StyledDOMBuilder<DIV>.() -> Unit = {}) { +private fun ChildrenBuilder.victoryPoints(points: Int, block: HTMLAttributes<HTMLDivElement>.() -> Unit = {}) { boardToken("military/victory1", points, block) } -private fun RBuilder.defeatTokenCount(nbDefeatTokens: Int, block: StyledDOMBuilder<DIV>.() -> Unit = {}) { +private fun ChildrenBuilder.defeatTokenCount(nbDefeatTokens: Int, block: HTMLAttributes<HTMLDivElement>.() -> Unit = {}) { boardToken("military/defeat1", nbDefeatTokens, block) } -private fun RBuilder.boardToken(tokenName: String, count: Int, block: StyledDOMBuilder<DIV>.() -> Unit) { +private fun ChildrenBuilder.boardToken(tokenName: String, count: Int, block: HTMLAttributes<HTMLDivElement>.() -> Unit) { tokenWithCount( tokenName = tokenName, count = count, @@ -173,34 +179,40 @@ private fun RBuilder.boardToken(tokenName: String, count: Int, block: StyledDOMB brightText = true, ) { css { - filter = "drop-shadow(0.2rem 0.2rem 0.5rem black)" + filter = dropShadow(0.2.rem, 0.2.rem, 0.5.rem, NamedColor.black) height = 15.pct } block() } } -private fun RBuilder.wonderStageElement(stage: ApiWonderStage, block: StyledDOMBuilder<HTMLTag>.() -> Unit) { - val back = stage.cardBack +private external interface WonderStageElementProps : PropsWithClassName { + var stage: ApiWonderStage +} + +private val WonderStageElement = FC<WonderStageElementProps>("WonderStageElement") { props -> + val back = props.stage.cardBack if (back != null) { - val highlightColor = if (stage.builtDuringLastMove) Color.gold else null - cardBackImage(cardBack = back, highlightColor = highlightColor) { - block() + val highlightColor = if (props.stage.builtDuringLastMove) NamedColor.gold else null + CardBackImage { + this.cardBack = back + this.highlightColor = highlightColor + this.className = props.className } } else { - cardPlaceholderImage { - block() + CardPlaceholderImage { + this.className = props.className } } } -private fun CssBuilder.wonderCardStyle(stageIndex: Int, nbStages: Int) { +private fun PropertiesBuilder.wonderCardStyle(stageIndex: Int, nbStages: Int) { position = Position.absolute top = 60.pct // makes the cards stick out of the bottom of the wonder left = stagePositionPercent(stageIndex, nbStages).pct maxWidth = 24.pct // ratio of card width to wonder width maxHeight = 90.pct // ratio of card height to wonder height - zIndex = -1 // below wonder (somehow 0 is not sufficient) + zIndex = integer(-1) // below wonder (somehow 0 is not sufficient) } private fun stagePositionPercent(stageIndex: Int, nbStages: Int): Double = when (nbStages) { @@ -209,6 +221,6 @@ private fun stagePositionPercent(stageIndex: Int, nbStages: Int): Double = when else -> 7.9 + stageIndex * 30.0 } -private fun CssBuilder.hoverHighlightStyle() { - highlightStyle(Color.paleGoldenrod) +private fun PropertiesBuilder.hoverHighlightStyle() { + highlightStyle(NamedColor.palegoldenrod) } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/BoardSummary.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/BoardSummary.kt index ec7ea464..76487db5 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/BoardSummary.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/BoardSummary.kt @@ -1,109 +1,134 @@ package org.luxons.sevenwonders.ui.components.game -import blueprintjs.core.PopoverPosition -import blueprintjs.core.bpDivider -import blueprintjs.core.bpPopover -import kotlinx.css.* -import kotlinx.html.* -import org.luxons.sevenwonders.model.api.PlayerDTO -import org.luxons.sevenwonders.model.boards.Board -import org.luxons.sevenwonders.ui.components.gameBrowser.playerInfo +import blueprintjs.core.* +import csstype.* +import emotion.css.* +import emotion.react.* +import org.luxons.sevenwonders.model.api.* +import org.luxons.sevenwonders.model.boards.* +import org.luxons.sevenwonders.ui.components.gameBrowser.* +import org.luxons.sevenwonders.ui.utils.* import react.* -import styled.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.hr enum class BoardSummarySide( val tokenCountPosition: TokenCountPosition, - val alignment: Align, + val alignment: AlignItems, val popoverPosition: PopoverPosition, ) { - LEFT(TokenCountPosition.RIGHT, Align.flexStart, PopoverPosition.RIGHT), - TOP(TokenCountPosition.OVER, Align.flexStart, PopoverPosition.BOTTOM), - RIGHT(TokenCountPosition.LEFT, Align.flexEnd, PopoverPosition.LEFT), - BOTTOM(TokenCountPosition.OVER, Align.flexStart, PopoverPosition.TOP), + LEFT(TokenCountPosition.RIGHT, AlignItems.flexStart, PopoverPosition.RIGHT), + TOP(TokenCountPosition.OVER, AlignItems.flexStart, PopoverPosition.BOTTOM), + RIGHT(TokenCountPosition.LEFT, AlignItems.flexEnd, PopoverPosition.LEFT), + BOTTOM(TokenCountPosition.OVER, AlignItems.flexStart, PopoverPosition.TOP), } -fun RBuilder.boardSummaryWithPopover( - player: PlayerDTO, - board: Board, - boardSummarySide: BoardSummarySide, - block: StyledDOMBuilder<DIV>.() -> Unit = {}, -) { - val popoverClass = GameStyles.getClassName { it::fullBoardPreviewPopover } - bpPopover( - content = createFullBoardPreview(board), - position = boardSummarySide.popoverPosition, - popoverClassName = popoverClass, - ) { - boardSummary( - player = player, - board = board, - side = boardSummarySide, - block = block, - ) - } +external interface BoardSummaryWithPopoverProps : PropsWithClassName { + var player: PlayerDTO + var board: Board + var side: BoardSummarySide } -private fun createFullBoardPreview(board: Board): ReactElement<*> = buildElement { - boardComponent(board = board) { - css { - +GameStyles.fullBoardPreview +val BoardSummaryWithPopover = FC<BoardSummaryWithPopoverProps>("BoardSummaryWithPopover") { props -> + BpPopover { + content = BoardComponent.create { + className = GameStyles.fullBoardPreview + board = props.board + } + position = props.side.popoverPosition + interactionKind = PopoverInteractionKind.HOVER + popoverClassName = ClassName { + val bgColor = GameStyles.sandBgColor.withAlpha(0.7) + backgroundColor = bgColor + borderRadius = 0.5.rem + padding = Padding(all = 0.5.rem) + + children(".bp4-popover-content") { + background = None.none // overrides default white background + } + descendants(".bp4-popover-arrow-fill") { + set(Variable("fill"), bgColor.toString()) // overrides default white arrow + } + descendants(".bp4-popover-arrow::before") { + // The popover arrow is implemented with a simple square rotated 45 degrees (like a rhombus). + // Since we use a semi-transparent background, we can see the box shadow of the rest of the arrow through + // the popover, and thus we see the square. This boxShadow(transparent) is to avoid that. + boxShadow = BoxShadow(0.px, 0.px, 0.px, 0.px, NamedColor.transparent) + } + }.toString() + + BoardSummary { + this.className = props.className + this.player = props.player + this.board = props.board + this.side = props.side } } } -fun RBuilder.boardSummary( - player: PlayerDTO, - board: Board, - side: BoardSummarySide, - showPreparationStatus: Boolean = true, - block: StyledDOMBuilder<DIV>.() -> Unit = {}, -) { - styledDiv { - css { +external interface BoardSummaryProps : PropsWithClassName { + var player: PlayerDTO + var board: Board + var side: BoardSummarySide + var showPreparationStatus: Boolean? +} + +val BoardSummary = FC<BoardSummaryProps>("BoardSummary") { props -> + div { + css(props.className) { display = Display.flex flexDirection = FlexDirection.column - alignItems = side.alignment - padding(all = 0.5.rem) - backgroundColor = Color.paleGoldenrod.withAlpha(0.5) - zIndex = 50 // above table cards + alignItems = props.side.alignment + padding = Padding(all = 0.5.rem) + backgroundColor = NamedColor.palegoldenrod.withAlpha(0.5) + zIndex = integer(50) // above table cards hover { - backgroundColor = Color.paleGoldenrod + backgroundColor = NamedColor.palegoldenrod } } - topBar(player, side, showPreparationStatus) - styledHr { + val showPreparationStatus = props.showPreparationStatus ?: true + topBar(props.player, props.side, showPreparationStatus) + hr { css { - margin(vertical = 0.5.rem) + margin = Margin(vertical = 0.5.rem, horizontal = 0.rem) width = 100.pct } } - bottomBar(side, board, player, showPreparationStatus) - block() + bottomBar(props.side, props.board, props.player, showPreparationStatus) } } -private fun RBuilder.topBar(player: PlayerDTO, side: BoardSummarySide, showPreparationStatus: Boolean) { +private fun ChildrenBuilder.topBar(player: PlayerDTO, side: BoardSummarySide, showPreparationStatus: Boolean) { val playerIconSize = 25 if (showPreparationStatus && side == BoardSummarySide.TOP) { - styledDiv { + div { css { display = Display.flex flexDirection = FlexDirection.row justifyContent = JustifyContent.spaceBetween width = 100.pct } - playerInfo(player, iconSize = playerIconSize) - playerPreparedCard(player) + PlayerInfo { + this.player = player + this.iconSize = playerIconSize + } + PlayerPreparedCard { + this.playerDisplayName = player.displayName + this.username = player.username + } } } else { - playerInfo(player, iconSize = playerIconSize) + PlayerInfo { + this.player = player + this.iconSize = playerIconSize + } } } -private fun RBuilder.bottomBar(side: BoardSummarySide, board: Board, player: PlayerDTO, showPreparationStatus: Boolean) { - styledDiv { +private fun ChildrenBuilder.bottomBar(side: BoardSummarySide, board: Board, player: PlayerDTO, showPreparationStatus: Boolean) { + div { css { display = Display.flex flexDirection = if (side == BoardSummarySide.TOP || side == BoardSummarySide.BOTTOM) FlexDirection.row else FlexDirection.column @@ -114,26 +139,29 @@ private fun RBuilder.bottomBar(side: BoardSummarySide, board: Board, player: Pla } val tokenSize = 2.rem generalCounts(board, tokenSize, side.tokenCountPosition) - bpDivider() + BpDivider() scienceTokens(board, tokenSize, side.tokenCountPosition) if (showPreparationStatus && side != BoardSummarySide.TOP) { - bpDivider() - styledDiv { + BpDivider() + div { css { width = 100.pct - alignItems = Align.center + alignItems = AlignItems.center display = Display.flex flexDirection = FlexDirection.column } - playerPreparedCard(player) + PlayerPreparedCard { + this.playerDisplayName = player.displayName + this.username = player.username + } } } } } -private fun StyledDOMBuilder<DIV>.generalCounts( +private fun ChildrenBuilder.generalCounts( board: Board, - tokenSize: LinearDimension, + tokenSize: Length, countPosition: TokenCountPosition, ) { goldIndicator(amount = board.gold, imgSize = tokenSize, amountPosition = countPosition) @@ -153,9 +181,9 @@ private fun StyledDOMBuilder<DIV>.generalCounts( ) } -private fun RBuilder.scienceTokens( +private fun ChildrenBuilder.scienceTokens( board: Board, - tokenSize: LinearDimension, + tokenSize: Length, sciencePosition: TokenCountPosition, ) { tokenWithCount( diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/CardImage.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/CardImage.kt index 409c4dac..093384a2 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/CardImage.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/CardImage.kt @@ -1,77 +1,72 @@ package org.luxons.sevenwonders.ui.components.game -import kotlinx.css.* -import kotlinx.css.properties.* -import kotlinx.html.IMG -import kotlinx.html.title -import org.luxons.sevenwonders.model.cards.Card -import org.luxons.sevenwonders.model.cards.CardBack -import react.RBuilder -import react.dom.attrs -import styled.StyledDOMBuilder -import styled.css -import styled.styledImg +import csstype.* +import csstype.Color +import emotion.react.* +import org.luxons.sevenwonders.model.cards.* +import react.* +import react.dom.html.ReactHTML.img -fun RBuilder.cardImage( - card: Card, - faceDown: Boolean = false, - highlightColor: Color? = null, - block: StyledDOMBuilder<IMG>.() -> Unit = {}, -) { - if (faceDown) { - cardBackImage(card.back, highlightColor, block) - return - } - styledImg(src = "/images/cards/${card.image}") { - css { - cardImageStyle(highlightColor) +external interface CardImageProps : PropsWithClassName { + var card: Card + var faceDown: Boolean? + var highlightColor: Color? +} + +val CardImage = FC<CardImageProps>("CardImage") { props -> + if (props.faceDown == true) { + CardBackImage { + cardBack = props.card.back + highlightColor = props.highlightColor } - attrs { - title = card.name - alt = "Card ${card.name}" + } else { + img { + src = "/images/cards/${props.card.image}" + title = props.card.name + alt = "Card ${props.card.name}" + + css(props.className) { + cardImageStyle(props.highlightColor) + } } - block() } } -fun RBuilder.cardBackImage( - cardBack: CardBack, - highlightColor: Color? = null, - block: StyledDOMBuilder<IMG>.() -> Unit = {}, -) { - styledImg(src = "/images/cards/back/${cardBack.image}") { - css { - cardImageStyle(highlightColor) - } - attrs { - alt = "Card back (${cardBack.image})" +external interface CardBackImageProps : PropsWithClassName { + var cardBack: CardBack + var highlightColor: Color? +} + +val CardBackImage = FC<CardBackImageProps>("CardBackImage") { props -> + img { + src = "/images/cards/back/${props.cardBack.image}" + alt = "Card back (${props.cardBack.image})" + css(props.className) { + cardImageStyle(props.highlightColor) } - block() } } -fun RBuilder.cardPlaceholderImage(block: StyledDOMBuilder<IMG>.() -> Unit = {}) { - styledImg(src = "/images/cards/back/placeholder.png") { - css { - opacity = 0.20 +val CardPlaceholderImage = FC<PropsWithClassName>("CardPlaceholderImage") { props -> + img { + src = "/images/cards/back/placeholder.png" + alt = "Card placeholder" + css(props.className) { + opacity = number(0.20) borderRadius = 5.pct } - attrs { - alt = "Card placeholder" - } - block() } } -private fun CssBuilder.cardImageStyle(highlightColor: Color?) { +private fun PropertiesBuilder.cardImageStyle(highlightColor: Color?) { borderRadius = 5.pct - boxShadow(offsetX = 2.px, offsetY = 2.px, blurRadius = 5.px, color = Color.black) + boxShadow = BoxShadow(offsetX = 2.px, offsetY = 2.px, blurRadius = 5.px, color = NamedColor.black) highlightStyle(highlightColor) } -internal fun CssBuilder.highlightStyle(highlightColor: Color?) { +internal fun PropertiesBuilder.highlightStyle(highlightColor: Color?) { if (highlightColor != null) { - boxShadow( + boxShadow = BoxShadow( offsetX = 0.px, offsetY = 0.px, blurRadius = 1.rem, diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt index cec41d6a..843caaf7 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameScene.kt @@ -1,309 +1,309 @@ package org.luxons.sevenwonders.ui.components.game import blueprintjs.core.* -import blueprintjs.icons.IconNames -import kotlinx.css.* -import kotlinx.css.Position -import kotlinx.css.properties.transform -import kotlinx.css.properties.translate -import org.luxons.sevenwonders.client.GameState -import org.luxons.sevenwonders.client.getBoard -import org.luxons.sevenwonders.client.getNonNeighbourBoards -import org.luxons.sevenwonders.client.getOwnBoard -import org.luxons.sevenwonders.model.MoveType -import org.luxons.sevenwonders.model.PlayerMove -import org.luxons.sevenwonders.model.TurnAction -import org.luxons.sevenwonders.model.api.PlayerDTO -import org.luxons.sevenwonders.model.boards.Board -import org.luxons.sevenwonders.model.boards.RelativeBoardPosition -import org.luxons.sevenwonders.model.cards.HandCard -import org.luxons.sevenwonders.model.resources.ResourceTransactionOptions -import org.luxons.sevenwonders.ui.components.GlobalStyles +import blueprintjs.icons.* +import csstype.* +import csstype.Position +import emotion.react.* +import org.luxons.sevenwonders.client.* +import org.luxons.sevenwonders.model.* +import org.luxons.sevenwonders.model.api.* +import org.luxons.sevenwonders.model.boards.* +import org.luxons.sevenwonders.model.cards.* +import org.luxons.sevenwonders.model.resources.* +import org.luxons.sevenwonders.ui.components.* import org.luxons.sevenwonders.ui.redux.* import org.luxons.sevenwonders.ui.utils.* import react.* -import styled.css -import styled.styledDiv +import react.dom.html.ReactHTML.div -external interface GameSceneStateProps : PropsWithChildren { +external interface GameSceneProps : Props { var currentPlayer: PlayerDTO? var players: List<PlayerDTO> - var game: GameState? + var game: GameState var preparedMove: PlayerMove? var preparedCard: HandCard? -} - -external interface GameSceneDispatchProps : PropsWithChildren { var sayReady: () -> Unit var prepareMove: (move: PlayerMove) -> Unit var unprepareMove: () -> Unit var leaveGame: () -> Unit } -interface GameSceneProps : GameSceneStateProps, GameSceneDispatchProps - -data class GameSceneState( - var transactionSelector: TransactionSelectorState? -) : State - data class TransactionSelectorState( val moveType: MoveType, val card: HandCard, val transactionsOptions: ResourceTransactionOptions, ) -private class GameScene(props: GameSceneProps) : RComponent<GameSceneProps, GameSceneState>(props) { +val GameScene = VFC("GameScene") { - override fun GameSceneState.init() { - transactionSelector = null - } + val player = useSwSelector { it.currentPlayer } + val gameState = useSwSelector { it.gameState } + val dispatch = useSwDispatch() - override fun RBuilder.render() { - styledDiv { - css { - +GlobalStyles.papyrusBackground - +GlobalStyles.fullscreen + div { + css(GlobalStyles.papyrusBackground, GlobalStyles.fullscreen) {} + + if (gameState == null) { + BpNonIdealState { + icon = IconNames.ERROR + titleText = "Error: no game data" } - val game = props.game - if (game == null) { - bpNonIdealState(icon = IconNames.ERROR, title = "Error: no game data") - } else { - boardScene(game) + } else { + GameScenePresenter { + currentPlayer = player + players = gameState.players + game = gameState + preparedMove = gameState.currentPreparedMove + preparedCard = gameState.currentPreparedCard + + prepareMove = { move -> dispatch(RequestPrepareMove(move)) } + unprepareMove = { dispatch(RequestUnprepareMove()) } + sayReady = { dispatch(RequestSayReady()) } + leaveGame = { dispatch(RequestLeaveGame()) } } } } +} - private fun RBuilder.boardScene(game: GameState) { - val board = game.getOwnBoard() - styledDiv { +private val GameScenePresenter = FC<GameSceneProps>("GameScenePresenter") { props -> + var transactionSelectorState by useState<TransactionSelectorState>() + + val game = props.game + val board = game.getOwnBoard() + div { + val maybeRed = GameStyles.pulsatingRed.takeIf { game.everyoneIsWaitingForMe() } + css(maybeRed) { + height = 100.pct + } + val action = game.action + if (action is TurnAction.WatchScore) { + scoreTableOverlay(action.scoreBoard, props.players, props.leaveGame) + } + actionInfo(game.action.message) + BoardComponent { + this.board = board css { + padding = Padding(vertical = 7.rem, horizontal = 7.rem) // to fit the action info message & board summaries + width = 100.pct height = 100.pct - if (everyoneIsWaitingForMe()) { - +GameStyles.pulsatingRed - } } - val action = game.action - if (action is TurnAction.WatchScore) { - scoreTableOverlay(action.scoreBoard, props.players, props.leaveGame) + } + transactionsSelectorDialog( + state = transactionSelectorState, + neighbours = playerNeighbours(props.currentPlayer, props.players), + prepareMove = { move -> + props.prepareMove(move) + transactionSelectorState = null + }, + cancelTransactionSelection = { transactionSelectorState = null }, + ) + boardSummaries(game) + handRotationIndicator(game.handRotationDirection) + handCards( + game = game, + prepareMove = props.prepareMove, + startTransactionsSelection = { + transactionSelectorState = it } - actionInfo(game.action.message) - boardComponent(board = board) { - css { - padding(all = 7.rem) // to fit the action info message & board summaries - width = 100.pct - height = 100.pct + ) + val card = props.preparedCard + val move = props.preparedMove + if (card != null && move != null) { + BpOverlay { + isOpen = true + onClose = { props.unprepareMove() } + + preparedMove(card, move, props.unprepareMove) { + css(GlobalStyles.fixedCenter) {} } } - transactionsSelectorDialog( - state = state.transactionSelector, - neighbours = playerNeighbours(), - prepareMove = ::prepareMoveAndCloseTransactions, - cancelTransactionSelection = ::resetTransactionSelector, - ) - boardSummaries(game) - handRotationIndicator(game.handRotationDirection) - handCards(game, props.prepareMove, ::startTransactionSelection) - val card = props.preparedCard - val move = props.preparedMove - if (card != null && move != null) { - preparedMove(card, move) - } - if (game.action is TurnAction.SayReady) { - sayReadyButton() + } + if (game.action is TurnAction.SayReady) { + SayReadyButton { + currentPlayer = props.currentPlayer + players = props.players + sayReady = props.sayReady } } } +} - private fun prepareMoveAndCloseTransactions(move: PlayerMove) { - props.prepareMove(move) - setState { transactionSelector = null } +private fun GameState.everyoneIsWaitingForMe(): Boolean { + val onlyMeInTheGame = players.count { it.isHuman } == 1 + if (onlyMeInTheGame || currentPreparedMove != null) { + return false } + return preparedCardsByUsername.values.count { it != null } == players.size - 1 +} - private fun startTransactionSelection(selectorState: TransactionSelectorState) { - setState { transactionSelector = selectorState } - } +private fun playerNeighbours(currentPlayer: PlayerDTO?, players: List<PlayerDTO>): Pair<PlayerDTO, PlayerDTO> { + val me = currentPlayer?.username ?: error("we shouldn't be trying to display this if there is no player") + val size = players.size + val myIndex = players.indexOfFirst { it.username == me } + return players[(myIndex - 1 + size) % size] to players[(myIndex + 1) % size] +} - private fun resetTransactionSelector() { - setState { transactionSelector = null } - } +private fun ChildrenBuilder.actionInfo(message: String) { + div { + css(ClassName(Classes.DARK)) { + position = Position.fixed + top = 0.pct + left = 0.pct + margin = Margin(vertical = 0.4.rem, horizontal = 0.4.rem) + maxWidth = 25.pct // leave space for 4 board summaries when there are 7 players + } + BpCard { + elevation = Elevation.TWO + css { + padding = Padding(all = 0.px) + } - private fun everyoneIsWaitingForMe(): Boolean { - val onlyMeInTheGame = props.players.count { it.isHuman } == 1 - if (onlyMeInTheGame || props.preparedMove != null) { - return false + BpCallout { + intent = Intent.PRIMARY + icon = IconNames.INFO_SIGN + +message + } } - val gameState = props.game ?: return false - return gameState.preparedCardsByUsername.values.count { it != null } == props.players.size - 1 } +} - private fun playerNeighbours(): Pair<PlayerDTO, PlayerDTO> { - val me = props.currentPlayer?.username ?: error("we shouldn't be trying to display this if there is no player") - val players = props.players - val size = players.size - val myIndex = players.indexOfFirst { it.username == me } - return players[(myIndex - 1 + size) % size] to players[(myIndex + 1) % size] +private fun ChildrenBuilder.boardSummaries(game: GameState) { + val leftBoard = game.getBoard(RelativeBoardPosition.LEFT) + val rightBoard = game.getBoard(RelativeBoardPosition.RIGHT) + val topBoards = game.getNonNeighbourBoards().reversed() + selfBoardSummary(game.getOwnBoard(), game.players) + leftPlayerBoardSummary(leftBoard, game.players) + rightPlayerBoardSummary(rightBoard, game.players) + if (topBoards.isNotEmpty()) { + topPlayerBoardsSummaries(topBoards, game.players) } +} + +private fun ChildrenBuilder.leftPlayerBoardSummary(board: Board, players: List<PlayerDTO>) { + div { + css { + position = Position.absolute + left = 0.px + bottom = 40.pct + } + BoardSummaryWithPopover { + this.player = players[board.playerIndex] + this.board = board + this.side = BoardSummarySide.LEFT - private fun RBuilder.actionInfo(message: String) { - styledDiv { css { - classes.add(Classes.DARK) - position = Position.fixed - top = 0.pct - left = 0.pct - margin(all = 0.4.rem) - maxWidth = 25.pct // leave space for 4 board summaries when there are 7 players - } - bpCard(elevation = Elevation.TWO) { - attrs { - this.className = GlobalStyles.getTypedClassName { it::noPadding } - } - bpCallout(intent = Intent.PRIMARY, icon = IconNames.INFO_SIGN) { +message } + borderTopRightRadius = 0.4.rem + borderBottomRightRadius = 0.4.rem } } } +} - private fun RBuilder.boardSummaries(game: GameState) { - val leftBoard = game.getBoard(RelativeBoardPosition.LEFT) - val rightBoard = game.getBoard(RelativeBoardPosition.RIGHT) - val topBoards = game.getNonNeighbourBoards().reversed() - selfBoardSummary(game.getOwnBoard()) - leftPlayerBoardSummary(leftBoard) - rightPlayerBoardSummary(rightBoard) - if (topBoards.isNotEmpty()) { - topPlayerBoardsSummaries(topBoards) +private fun ChildrenBuilder.rightPlayerBoardSummary(board: Board, players: List<PlayerDTO>) { + div { + css { + position = Position.absolute + right = 0.px + bottom = 40.pct } - } + BoardSummaryWithPopover { + this.player = players[board.playerIndex] + this.board = board + this.side = BoardSummarySide.RIGHT - private fun RBuilder.leftPlayerBoardSummary(board: Board) { - styledDiv { css { - position = Position.absolute - left = 0.px - bottom = 40.pct - } - boardSummaryWithPopover(props.players[board.playerIndex], board, BoardSummarySide.LEFT) { - css { - borderTopRightRadius = 0.4.rem - borderBottomRightRadius = 0.4.rem - } + borderTopLeftRadius = 0.4.rem + borderBottomLeftRadius = 0.4.rem } } } +} + +private fun ChildrenBuilder.topPlayerBoardsSummaries(boards: List<Board>, players: List<PlayerDTO>) { + div { + css { + position = Position.absolute + top = 0.px + left = 50.pct + transform = translate((-50).pct) + display = Display.flex + flexDirection = FlexDirection.row + } + boards.forEach { board -> + BoardSummaryWithPopover { + this.player = players[board.playerIndex] + this.board = board + this.side = BoardSummarySide.TOP - private fun RBuilder.rightPlayerBoardSummary(board: Board) { - styledDiv { - css { - position = Position.absolute - right = 0.px - bottom = 40.pct - } - boardSummaryWithPopover(props.players[board.playerIndex], board, BoardSummarySide.RIGHT) { css { - borderTopLeftRadius = 0.4.rem borderBottomLeftRadius = 0.4.rem + borderBottomRightRadius = 0.4.rem + margin = Margin(vertical = 0.rem, horizontal = 2.rem) } } } } +} - private fun RBuilder.topPlayerBoardsSummaries(boards: List<Board>) { - styledDiv { - css { - position = Position.absolute - top = 0.px - left = 50.pct - transform { translate((-50).pct) } - display = Display.flex - flexDirection = FlexDirection.row - } - boards.forEach { board -> - boardSummaryWithPopover(props.players[board.playerIndex], board, BoardSummarySide.TOP) { - css { - borderBottomLeftRadius = 0.4.rem - borderBottomRightRadius = 0.4.rem - margin(horizontal = 2.rem) - } - } - } +private fun ChildrenBuilder.selfBoardSummary(board: Board, players: List<PlayerDTO>) { + div { + css { + position = Position.absolute + bottom = 0.px + left = 0.px } - } + BoardSummary { + this.player = players[board.playerIndex] + this.board = board + this.side = BoardSummarySide.BOTTOM + this.showPreparationStatus = false - private fun RBuilder.selfBoardSummary(board: Board) { - styledDiv { css { - position = Position.absolute - bottom = 0.px - left = 0.px - } - boardSummary( - player = props.players[board.playerIndex], - board = board, BoardSummarySide.BOTTOM, - showPreparationStatus = false, - ) { - css { - borderTopLeftRadius = 0.4.rem - borderTopRightRadius = 0.4.rem - margin(horizontal = 2.rem) - } + borderTopLeftRadius = 0.4.rem + borderTopRightRadius = 0.4.rem + margin = Margin(vertical = 0.rem, horizontal = 2.rem) } } } +} - private fun RBuilder.preparedMove(card: HandCard, move: PlayerMove) { - bpOverlay(isOpen = true, onClose = props.unprepareMove) { - preparedMove(card, move, props.unprepareMove) { - css { +GlobalStyles.fixedCenter } - } +private external interface SayReadyButtonProps : Props { + var currentPlayer: PlayerDTO? + var players: List<PlayerDTO> + var sayReady: () -> Unit +} + +private val SayReadyButton = FC<SayReadyButtonProps>("SayReadyButton") { props -> + val isReady = props.currentPlayer?.isReady == true + val intent = if (isReady) Intent.SUCCESS else Intent.PRIMARY + div { + css { + position = Position.absolute + bottom = 6.rem + left = 50.pct + transform = translate(tx = (-50).pct) + zIndex = integer(2) // go above the wonder (1) and wonder-upgrade cards (0) } - } + BpButtonGroup { + BpButton { + this.large = true + this.disabled = isReady + this.intent = intent + this.icon = if (isReady) IconNames.TICK_CIRCLE else IconNames.PLAY + this.onClick = { props.sayReady() } - private fun RBuilder.sayReadyButton() { - val isReady = props.currentPlayer?.isReady == true - val intent = if (isReady) Intent.SUCCESS else Intent.PRIMARY - styledDiv { - css { - position = Position.absolute - bottom = 6.rem - left = 50.pct - transform { translate(tx = (-50).pct) } - zIndex = 2 // go above the wonder (1) and wonder-upgrade cards (0) + +"READY" } - bpButtonGroup { - bpButton( - large = true, - disabled = isReady, - intent = intent, - icon = if (isReady) IconNames.TICK_CIRCLE else IconNames.PLAY, - onClick = { props.sayReady() }, - ) { - +"READY" - } - // not really a button, but nice for style - bpButton(large = true, icon = IconNames.PEOPLE, disabled = isReady, intent = intent) { - +"${props.players.count { it.isReady }}/${props.players.size}" - } + // not really a button, but nice for style + BpButton { + this.large = true + this.icon = IconNames.PEOPLE + this.disabled = isReady + this.intent = intent + + +"${props.players.count { it.isReady }}/${props.players.size}" } } } } - -fun RBuilder.gameScene() = gameScene {} - -private val gameScene: ComponentClass<GameSceneProps> = - connectStateAndDispatch<GameSceneStateProps, GameSceneDispatchProps, GameSceneProps>( - clazz = GameScene::class, - mapDispatchToProps = { dispatch, _ -> - prepareMove = { move -> dispatch(RequestPrepareMove(move)) } - unprepareMove = { dispatch(RequestUnprepareMove()) } - sayReady = { dispatch(RequestSayReady()) } - leaveGame = { dispatch(RequestLeaveGame()) } - }, - mapStateToProps = { state, _ -> - currentPlayer = state.currentPlayer - players = state.gameState?.players ?: emptyList() - game = state.gameState - preparedMove = state.gameState?.currentPreparedMove - preparedCard = state.gameState?.currentPreparedCard - }, - ) diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameStyles.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameStyles.kt index 86d2d101..bccee3f1 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameStyles.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/GameStyles.kt @@ -1,100 +1,86 @@ package org.luxons.sevenwonders.ui.components.game -import kotlinx.css.* -import kotlinx.css.properties.* -import styled.StyleSheet -import styled.animation +import csstype.* +import emotion.css.* +import org.luxons.sevenwonders.ui.utils.* -object GameStyles : StyleSheet("GameStyles", isStatic = true) { +object GameStyles { - val totalScore by css { + val totalScore = ClassName { fontWeight = FontWeight.bold } - val civilScore by scoreTagColorCss(Color("#2a73c9")) - val scienceScore by scoreTagColorCss(Color("#0f9960")) - val militaryScore by scoreTagColorCss(Color("#d03232")) - val tradeScore by scoreTagColorCss(Color("#e2c11b")) - val guildScore by scoreTagColorCss(Color("#663399")) - val wonderScore by scoreTagColorCss(Color.darkCyan) - val goldScore by scoreTagColorCss(Color.goldenrod) + val civilScore = scoreTagColorCss(Color("#2a73c9")) + val scienceScore = scoreTagColorCss(Color("#0f9960")) + val militaryScore = scoreTagColorCss(Color("#d03232")) + val tradeScore = scoreTagColorCss(Color("#e2c11b")) + val guildScore = scoreTagColorCss(Color("#663399")) + val wonderScore = scoreTagColorCss(NamedColor.darkcyan) + val goldScore = scoreTagColorCss(NamedColor.goldenrod) - private val sandBgColor = Color.paleGoldenrod + val sandBgColor = NamedColor.palegoldenrod - val fullBoardPreviewPopover by css { - val bgColor = sandBgColor.withAlpha(0.7) - backgroundColor = bgColor - borderRadius = 0.5.rem - padding(all = 0.5.rem) - children(".bp4-popover-content") { - background = "none" // overrides default white background - } - descendants(".bp4-popover-arrow-fill") { - put("fill", bgColor.toString()) // overrides default white arrow - } - descendants(".bp4-popover-arrow::before") { - // The popover arrow is implemented with a simple square rotated 45 degrees (like a rhombus). - // Since we use a semi-transparent background, we can see the box shadow of the rest of the arrow through - // the popover, and thus we see the square. This boxShadow(transparent) is to avoid that. - boxShadow(Color.transparent) - } - } - - val fullBoardPreview by css { + val fullBoardPreview = ClassName { width = 40.vw height = 50.vh } - val dimmedCard by css { - filter = "brightness(60%) grayscale(50%)" + val dimmedCard = ClassName { + filter = brightness(60.pct) + grayscale(50.pct) } - val transactionsSelector by css { + val transactionsSelector = ClassName { backgroundColor = sandBgColor width = 40.rem // default is 500px, we want to fit players on the side children(".bp4-dialog-header") { - background = "none" // overrides default white background + background = None.none // overrides default white background } } - val bestPrice by css { + val bestPrice = ClassName { fontWeight = FontWeight.bold color = rgb(50, 120, 50) - transform { - rotate((-20).deg) - } + transform = rotate((-20).deg) } - val discardMoveText by css { + val discardMoveText = ClassName { display = Display.flex - alignItems = Align.center + alignItems = AlignItems.center height = 3.rem fontSize = 2.rem - color = Color.goldenrod + color = NamedColor.goldenrod fontWeight = FontWeight.bold - borderTop(0.2.rem, BorderStyle.solid, Color.goldenrod) - borderBottom(0.2.rem, BorderStyle.solid, Color.goldenrod) + borderTop = Border(0.2.rem, LineStyle.solid, NamedColor.goldenrod) + borderBottom = Border(0.2.rem, LineStyle.solid, NamedColor.goldenrod) } - val scoreBoard by css { + val scoreBoard = ClassName { backgroundColor = sandBgColor } - private fun scoreTagColorCss(color: Color) = css { + private fun scoreTagColorCss(color: Color) = ClassName { backgroundColor = color } - val pulsatingRed by css { - animation( + val pulsatingRed = ClassName { + animation = Animation( + name = keyframes { + to { + boxShadow = BoxShadow( + inset = BoxShadowInset.inset, + offsetX = 0.px, + offsetY = 0.px, + blurRadius = 20.px, + spreadRadius = 8.px, + color = NamedColor.red, + ) + } + }, duration = 2.s, - iterationCount = IterationCount.infinite, - direction = AnimationDirection.alternate, - ) { - to { - boxShadowInset(color = Color.red, blurRadius = 20.px, spreadRadius = 8.px) - } - } + ) + animationIterationCount = AnimationIterationCount.infinite + animationDirection = AnimationDirection.alternate } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt index da17c987..a136465d 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Hand.kt @@ -1,26 +1,38 @@ package org.luxons.sevenwonders.ui.components.game import blueprintjs.core.* -import blueprintjs.icons.IconNames -import kotlinx.css.* -import kotlinx.css.Position -import kotlinx.css.properties.* -import kotlinx.html.DIV -import org.luxons.sevenwonders.client.GameState -import org.luxons.sevenwonders.client.getOwnBoard +import blueprintjs.icons.* +import csstype.* +import csstype.Position +import emotion.react.* +import org.luxons.sevenwonders.client.* import org.luxons.sevenwonders.model.* -import org.luxons.sevenwonders.model.boards.Board -import org.luxons.sevenwonders.model.cards.CardPlayability -import org.luxons.sevenwonders.model.cards.HandCard -import org.luxons.sevenwonders.model.resources.ResourceTransactionOptions -import org.luxons.sevenwonders.model.wonders.WonderBuildability +import org.luxons.sevenwonders.model.boards.* +import org.luxons.sevenwonders.model.cards.* +import org.luxons.sevenwonders.model.resources.* +import org.luxons.sevenwonders.model.wonders.* +import org.luxons.sevenwonders.ui.utils.* import react.* -import react.dom.attrs -import styled.StyledDOMBuilder -import styled.css -import styled.styledDiv -import web.html.* -import kotlin.math.absoluteValue +import react.dom.html.ReactHTML.div +import kotlin.math.* + +fun ChildrenBuilder.handCards( + game: GameState, + prepareMove: (PlayerMove) -> Unit, + startTransactionsSelection: (TransactionSelectorState) -> Unit, +) { + HandCards { + this.action = game.action + this.ownBoard = game.getOwnBoard() + this.preparedMove = game.currentPreparedMove + this.prepareMove = { moveType: MoveType, card: HandCard, transactionOptions: ResourceTransactionOptions -> + when (transactionOptions.size) { + 1 -> prepareMove(PlayerMove(moveType, card.name, transactionOptions.single())) + else -> startTransactionsSelection(TransactionSelectorState(moveType, card, transactionOptions)) + } + } + } +} private enum class HandAction( val buttonTitle: String, @@ -33,145 +45,154 @@ private enum class HandAction( COPY_GUILD("Copy this guild card", MoveType.COPY_GUILD, "duplicate") } -external interface HandProps : PropsWithChildren { +external interface HandCardsProps : Props { var action: TurnAction var ownBoard: Board var preparedMove: PlayerMove? - var prepareMove: (PlayerMove) -> Unit - var startTransactionsSelection: (TransactionSelectorState) -> Unit + var prepareMove: (MoveType, HandCard, ResourceTransactionOptions) -> Unit } -class HandComponent(props: HandProps) : RComponent<HandProps, State>(props) { - - override fun RBuilder.render() { - val hand = props.action.cardsToPlay() ?: return - styledDiv { - css { - handStyle() - } - hand.filter { it.name != props.preparedMove?.cardName }.forEachIndexed { index, c -> - handCard(card = c) { - attrs { - key = index.toString() - } - } +private val HandCards = FC<HandCardsProps>("HandCards") { props -> + val hand = props.action.cardsToPlay() ?: return@FC + div { + css { + handStyle() + } + hand.filter { it.name != props.preparedMove?.cardName }.forEachIndexed { index, c -> + HandCard { + card = c + action = props.action + ownBoard = props.ownBoard + prepareMove = props.prepareMove + key = index.toString() } } } +} - private fun TurnAction.cardsToPlay(): List<HandCard>? = when (this) { - is TurnAction.PlayFromHand -> hand - is TurnAction.PlayFromDiscarded -> discardedCards - is TurnAction.PickNeighbourGuild -> neighbourGuildCards - is TurnAction.SayReady, - is TurnAction.Wait, - is TurnAction.WatchScore -> null - } +private fun TurnAction.cardsToPlay(): List<HandCard>? = when (this) { + is TurnAction.PlayFromHand -> hand + is TurnAction.PlayFromDiscarded -> discardedCards + is TurnAction.PickNeighbourGuild -> neighbourGuildCards + is TurnAction.SayReady, + is TurnAction.Wait, + is TurnAction.WatchScore -> null +} - private fun RBuilder.handCard( - card: HandCard, - block: StyledDOMBuilder<DIV>.() -> Unit, - ) { - styledDiv { +private external interface HandCardProps : Props { + var card: HandCard + var action: TurnAction + var ownBoard: Board + var prepareMove: (MoveType, HandCard, ResourceTransactionOptions) -> Unit +} + +private val HandCard = FC<HandCardProps>("HandCard") { props -> + div { + css(ClassName("hand-card")) { + alignItems = AlignItems.flexEnd + display = Display.grid + margin = Margin(all = 0.2.rem) + } + CardImage { css { - handCardStyle() - } - block() - cardImage(card) { - css { - val isPlayable = card.playability.isPlayable || props.ownBoard.canPlayAnyCardForFree - handCardImgStyle(isPlayable) - } + val isPlayable = props.card.playability.isPlayable || props.ownBoard.canPlayAnyCardForFree + handCardImgStyle(isPlayable) } - actionButtons(card) + this.card = props.card } + actionButtons(props.card, props.action, props.ownBoard, props.prepareMove) } +} - private fun RBuilder.actionButtons(card: HandCard) { - styledDiv { - css { - justifyContent = JustifyContent.center - alignItems = Align.flexEnd - display = Display.none - gridRow = GridRow("1") - gridColumn = GridColumn("1") +private fun ChildrenBuilder.actionButtons( + card: HandCard, + action: TurnAction, + ownBoard: Board, + prepareMove: (MoveType, HandCard, ResourceTransactionOptions) -> Unit, +) { + div { + css { + justifyContent = JustifyContent.center + alignItems = AlignItems.flexEnd + display = None.none + gridRow = integer(1) + gridColumn = integer(1) - ancestorHover(".hand-card") { - display = Display.flex - } + ancestorHover(".hand-card") { + display = Display.flex } - bpButtonGroup { - val action = props.action - when (action) { - is TurnAction.PlayFromHand -> { - playCardButton(card, HandAction.PLAY) - if (props.ownBoard.canPlayAnyCardForFree) { - playCardButton(card.copy(playability = CardPlayability.SPECIAL_FREE), HandAction.PLAY_FREE) - } + } + BpButtonGroup { + when (action) { + is TurnAction.PlayFromHand -> { + playCardButton(card, HandAction.PLAY, prepareMove) + if (ownBoard.canPlayAnyCardForFree) { + playCardButton(card.copy(playability = CardPlayability.SPECIAL_FREE), HandAction.PLAY_FREE, prepareMove) } - is TurnAction.PlayFromDiscarded -> playCardButton(card, HandAction.PLAY_FREE_DISCARDED) - is TurnAction.PickNeighbourGuild -> playCardButton(card, HandAction.COPY_GUILD) - is TurnAction.SayReady, - is TurnAction.Wait, - is TurnAction.WatchScore -> error("unsupported action in hand card: $action") - } - - if (action.allowsBuildingWonder()) { - upgradeWonderButton(card) - } - if (action.allowsDiscarding()) { - discardButton(card) } + is TurnAction.PlayFromDiscarded -> playCardButton(card, HandAction.PLAY_FREE_DISCARDED, prepareMove) + is TurnAction.PickNeighbourGuild -> playCardButton(card, HandAction.COPY_GUILD, prepareMove) + is TurnAction.SayReady, + is TurnAction.Wait, + is TurnAction.WatchScore -> error("unsupported action in hand card: $action") } - } - } - private fun RElementBuilder<ButtonGroupProps>.playCardButton(card: HandCard, handAction: HandAction) { - bpButton( - title = "${handAction.buttonTitle} (${cardPlayabilityInfo(card.playability)})", - large = true, - intent = Intent.SUCCESS, - disabled = !card.playability.isPlayable, - onClick = { prepareMove(handAction.moveType, card, card.playability.transactionOptions) }, - ) { - bpIcon(handAction.icon) - if (card.playability.isPlayable && !card.playability.isFree) { - priceInfo(card.playability.minPrice) + if (action.allowsBuildingWonder()) { + upgradeWonderButton(card, ownBoard.wonder.buildability, prepareMove) + } + if (action.allowsDiscarding()) { + discardButton(card, prepareMove) } } } +} - private fun RElementBuilder<ButtonGroupProps>.upgradeWonderButton(card: HandCard) { - val wonderBuildability = props.ownBoard.wonder.buildability - bpButton( - title = "UPGRADE WONDER (${wonderBuildabilityInfo(wonderBuildability)})", - large = true, - intent = Intent.PRIMARY, - disabled = !wonderBuildability.isBuildable, - onClick = { prepareMove(MoveType.UPGRADE_WONDER, card, wonderBuildability.transactionsOptions) }, - ) { - bpIcon("key-shift") - if (wonderBuildability.isBuildable && !wonderBuildability.isFree) { - priceInfo(wonderBuildability.minPrice) - } +private fun ChildrenBuilder.playCardButton( + card: HandCard, + handAction: HandAction, + prepareMove: (MoveType, HandCard, ResourceTransactionOptions) -> Unit, +) { + BpButton { + title = "${handAction.buttonTitle} (${cardPlayabilityInfo(card.playability)})" + large = true + intent = Intent.SUCCESS + disabled = !card.playability.isPlayable + onClick = { prepareMove(handAction.moveType, card, card.playability.transactionOptions) } + + BpIcon { icon = handAction.icon } + + if (card.playability.isPlayable && !card.playability.isFree) { + priceInfo(card.playability.minPrice) } } +} - private fun prepareMove(moveType: MoveType, card: HandCard, transactionOptions: ResourceTransactionOptions) { - when (transactionOptions.size) { - 1 -> props.prepareMove(PlayerMove(moveType, card.name, transactionOptions.single())) - else -> props.startTransactionsSelection(TransactionSelectorState(moveType, card, transactionOptions)) +private fun ChildrenBuilder.upgradeWonderButton( + card: HandCard, + wonderBuildability: WonderBuildability, + prepareMove: (MoveType, HandCard, ResourceTransactionOptions) -> Unit, +) { + BpButton { + title = "UPGRADE WONDER (${wonderBuildabilityInfo(wonderBuildability)})" + large = true + intent = Intent.PRIMARY + disabled = !wonderBuildability.isBuildable + onClick = { prepareMove(MoveType.UPGRADE_WONDER, card, wonderBuildability.transactionsOptions) } + + BpIcon { icon = IconNames.KEY_SHIFT } + if (wonderBuildability.isBuildable && !wonderBuildability.isFree) { + priceInfo(wonderBuildability.minPrice) } } +} - private fun RElementBuilder<ButtonGroupProps>.discardButton(card: HandCard) { - bpButton( - title = "DISCARD (+3 coins)", // TODO remove hardcoded value - large = true, - intent = Intent.DANGER, - icon = IconNames.CROSS, - onClick = { props.prepareMove(PlayerMove(MoveType.DISCARD, card.name)) }, - ) +private fun ChildrenBuilder.discardButton(card: HandCard, prepareMove: (MoveType, HandCard, ResourceTransactionOptions) -> Unit) { + BpButton { + title = "DISCARD (+3 coins)" // TODO remove hardcoded value + large = true + intent = Intent.DANGER + icon = IconNames.CROSS + onClick = { prepareMove(MoveType.DISCARD, card, emptyList()) } } } @@ -196,15 +217,14 @@ private fun pricePrefix(amount: Int) = when { else -> "" } -private fun RElementBuilder<ButtonProps<HTMLButtonElement>>.priceInfo(amount: Int) { - val size = 1.rem +private fun ChildrenBuilder.priceInfo(amount: Int) { goldIndicator( amount = amount, amountPosition = TokenCountPosition.OVER, - imgSize = size, + imgSize = 1.rem, customCountStyle = { - fontFamily = "sans-serif" - fontSize = size * 0.8 + fontFamily = FontFamily.sansSerif + fontSize = 0.8.rem }, ) { css { @@ -215,68 +235,41 @@ private fun RElementBuilder<ButtonProps<HTMLButtonElement>>.priceInfo(amount: In } } -private fun CssBuilder.handStyle() { - alignItems = Align.center +private fun PropertiesBuilder.handStyle() { + alignItems = AlignItems.center bottom = 0.px display = Display.flex height = 345.px left = 50.pct maxHeight = 25.vw position = Position.absolute - transform { - translate(tx = (-50).pct, ty = 65.pct) - } - transition(duration = 0.5.s) - zIndex = 30 + transform = translate(tx = (-50).pct, ty = 65.pct) + transition = Transition(TransitionProperty.all, duration = 0.5.s, timingFunction = TransitionTimingFunction.ease) + zIndex = integer(30) hover { bottom = 1.rem - zIndex = 60 - transform { - translate(tx = (-50).pct, ty = 0.pct) - } + zIndex = integer(60) + transform = translate(tx = (-50).pct, ty = 0.pct) } } -private fun CssBuilder.handCardStyle() { - classes.add("hand-card") - alignItems = Align.flexEnd - display = Display.grid - margin(all = 0.2.rem) -} - -private fun CssBuilder.handCardImgStyle(isPlayable: Boolean) { - gridRow = GridRow("1") - gridColumn = GridColumn("1") +private fun PropertiesBuilder.handCardImgStyle(isPlayable: Boolean) { + gridRow = integer(1) + gridColumn = integer(1) maxWidth = 13.vw maxHeight = 60.vh - transition(duration = 0.1.s) + transition = Transition(TransitionProperty.all, duration = 0.1.s, timingFunction = TransitionTimingFunction.ease) width = 11.rem ancestorHover(".hand-card") { - boxShadow(offsetX = 0.px, offsetY = 10.px, blurRadius = 40.px, color = Color.black) + boxShadow = BoxShadow(offsetX = 0.px, offsetY = 10.px, blurRadius = 40.px, color = NamedColor.black) width = 14.rem maxWidth = 15.vw maxHeight = 90.vh } if (!isPlayable) { - filter = "grayscale(50%) contrast(50%)" - } -} - -fun RBuilder.handCards( - game: GameState, - prepareMove: (PlayerMove) -> Unit, - startTransactionsSelection: (TransactionSelectorState) -> Unit, -) { - child(HandComponent::class) { - attrs { - this.action = game.action - this.ownBoard = game.getOwnBoard() - this.preparedMove = game.currentPreparedMove - this.prepareMove = prepareMove - this.startTransactionsSelection = startTransactionsSelection - } + filter = grayscale(50.pct) + contrast(50.pct) } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/HandRotationIndicator.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/HandRotationIndicator.kt index 4761ac13..9e6874d3 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/HandRotationIndicator.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/HandRotationIndicator.kt @@ -1,44 +1,51 @@ package org.luxons.sevenwonders.ui.components.game -import blueprintjs.core.bpIcon -import kotlinx.css.* -import kotlinx.html.title -import org.luxons.sevenwonders.model.cards.HandRotationDirection -import react.RBuilder -import react.dom.attrs -import styled.css -import styled.styledDiv -import styled.styledImg +import blueprintjs.core.* +import blueprintjs.icons.* +import csstype.* +import csstype.Position +import emotion.react.* +import org.luxons.sevenwonders.model.cards.* +import react.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.img -fun RBuilder.handRotationIndicator(direction: HandRotationDirection) { - styledDiv { +fun ChildrenBuilder.handRotationIndicator(direction: HandRotationDirection) { + div { css { position = Position.absolute display = Display.flex - alignItems = Align.center + alignItems = AlignItems.center bottom = 25.vh } - attrs { - title = "Your hand will be passed to the player on your $direction after playing this card." - } + + title = "Your hand will be passed to the player on your $direction after playing this card." + val sideDistance = 2.rem when (direction) { HandRotationDirection.LEFT -> { css { left = sideDistance } - bpIcon("arrow-left", size = 25) + BpIcon { + icon = IconNames.ARROW_LEFT + size = 25 + } handCardsImg() } HandRotationDirection.RIGHT -> { css { right = sideDistance } handCardsImg() - bpIcon("arrow-right", size = 25) + BpIcon { + icon = IconNames.ARROW_RIGHT + size = 25 + } } } } } -private fun RBuilder.handCardsImg() { - styledImg(src = "images/hand-cards5.png") { +private fun ChildrenBuilder.handCardsImg() { + img { + src = "images/hand-cards5.png" css { width = 4.rem } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PlayerPreparedCard.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PlayerPreparedCard.kt deleted file mode 100644 index b42d3b81..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PlayerPreparedCard.kt +++ /dev/null @@ -1,82 +0,0 @@ -package org.luxons.sevenwonders.ui.components.game - -import kotlinx.css.* -import kotlinx.css.properties.* -import kotlinx.html.title -import org.luxons.sevenwonders.model.api.PlayerDTO -import org.luxons.sevenwonders.model.cards.CardBack -import org.luxons.sevenwonders.ui.redux.connectStateWithOwnProps -import react.* -import react.dom.attrs -import styled.animation -import styled.css -import styled.styledDiv -import styled.styledImg - -external interface PlayerPreparedCardProps : PropsWithChildren { - var playerDisplayName: String - var cardBack: CardBack? -} - -external interface PlayerPreparedCardContainerProps : PropsWithChildren { - var playerDisplayName: String - var username: String -} - -fun RBuilder.playerPreparedCard(player: PlayerDTO) = playerPreparedCard { - attrs { - this.playerDisplayName = player.displayName - this.username = player.username - } -} - -private class PlayerPreparedCard(props: PlayerPreparedCardProps) : RComponent<PlayerPreparedCardProps, State>(props) { - - override fun RBuilder.render() { - val cardBack = props.cardBack - val sideSize = 30.px - styledDiv { - css { - width = sideSize - height = sideSize - } - attrs { - title = if (cardBack == null) { - "${props.playerDisplayName} is still thinking…" - } else { - "${props.playerDisplayName} is ready to play this turn" - } - } - if (cardBack != null) { - cardBackImage(cardBack) { - css { - maxHeight = sideSize - } - } - } else { - styledImg(src = "images/gear-50.png") { - css { - maxHeight = sideSize - animation( - duration = 1.5.s, - iterationCount = IterationCount.infinite, - timing = cubicBezier(0.2, 0.9, 0.7, 1.3) - ) { - to { - transform { rotate(360.deg) } - } - } - } - } - } - } - } -} - -private val playerPreparedCard = connectStateWithOwnProps( - clazz = PlayerPreparedCard::class, - mapStateToProps = { state, ownProps: PlayerPreparedCardContainerProps -> - playerDisplayName = ownProps.playerDisplayName - cardBack = state.gameState?.preparedCardsByUsername?.get(ownProps.username) - }, -) diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PlayerPreparedCardPresenter.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PlayerPreparedCardPresenter.kt new file mode 100644 index 00000000..26678309 --- /dev/null +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PlayerPreparedCardPresenter.kt @@ -0,0 +1,79 @@ +package org.luxons.sevenwonders.ui.components.game + +import csstype.* +import emotion.css.* +import emotion.react.* +import org.luxons.sevenwonders.model.cards.* +import org.luxons.sevenwonders.ui.redux.* +import org.luxons.sevenwonders.ui.utils.* +import react.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.img + +external interface PlayerPreparedCardProps : Props { + var playerDisplayName: String + var username: String +} + +val PlayerPreparedCard = FC<PlayerPreparedCardProps>("PlayerPreparedCard") { props -> + val cardBack = useSwSelector { it.gameState?.preparedCardsByUsername?.get(props.username) } + + PlayerPreparedCardPresenter { + this.playerDisplayName = props.playerDisplayName + this.cardBack = cardBack + } +} + +external interface PlayerPreparedCardPresenterProps : Props { + var playerDisplayName: String + var cardBack: CardBack? +} + +private val PlayerPreparedCardPresenter = FC<PlayerPreparedCardPresenterProps>("PlayerPreparedCardPresenter") { props -> + val cardBack = props.cardBack + val sideSize = 30.px + div { + css { + width = sideSize + height = sideSize + } + title = if (cardBack == null) { + "${props.playerDisplayName} is still thinking…" + } else { + "${props.playerDisplayName} is ready to play this turn" + } + + if (cardBack != null) { + CardBackImage { + this.cardBack = cardBack + css { + maxHeight = sideSize + } + } + } else { + RotatingGear { + css { + maxHeight = sideSize + } + } + } + } +} + +private val RotatingGear = FC<PropsWithClassName> { props -> + img { + src = "images/gear-50.png" + css(props.className) { + animation = Animation( + name = keyframes { + to { + transform = rotate(360.deg) + } + }, + duration = 1.5.s, + timingFunction = cubicBezier(0.2, 0.9, 0.7, 1.3), + ) + animationIterationCount = AnimationIterationCount.infinite + } + } +} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PreparedMove.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PreparedMove.kt index a56bbc00..b03505d6 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PreparedMove.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/PreparedMove.kt @@ -1,32 +1,31 @@ package org.luxons.sevenwonders.ui.components.game -import blueprintjs.core.Intent -import blueprintjs.core.bpButton -import blueprintjs.icons.IconNames -import kotlinx.css.* -import kotlinx.css.properties.* -import kotlinx.html.DIV -import org.luxons.sevenwonders.model.MoveType -import org.luxons.sevenwonders.model.PlayerMove -import org.luxons.sevenwonders.model.cards.HandCard -import org.luxons.sevenwonders.ui.components.GlobalStyles -import react.RBuilder -import styled.StyledDOMBuilder -import styled.css -import styled.styledDiv -import styled.styledImg +import blueprintjs.core.* +import blueprintjs.icons.* +import csstype.* +import csstype.Position +import emotion.react.* +import org.luxons.sevenwonders.model.* +import org.luxons.sevenwonders.model.cards.* +import org.luxons.sevenwonders.ui.components.* +import react.* +import react.dom.html.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.img +import web.html.* -fun RBuilder.preparedMove( +fun ChildrenBuilder.preparedMove( card: HandCard, move: PlayerMove, unprepareMove: () -> Unit, - block: StyledDOMBuilder<DIV>.() -> Unit, + block: HTMLAttributes<HTMLDivElement>.() -> Unit, ) { - styledDiv { + div { block() - cardImage(card) { + CardImage { + this.card = card if (move.type == MoveType.DISCARD || move.type == MoveType.UPGRADE_WONDER) { - css { +GameStyles.dimmedCard } + this.className = GameStyles.dimmedCard } } if (move.type == MoveType.DISCARD) { @@ -35,43 +34,39 @@ fun RBuilder.preparedMove( if (move.type == MoveType.UPGRADE_WONDER) { upgradeWonderSymbol() } - styledDiv { + div { css { position = Position.absolute top = 0.px right = 0.px } - bpButton( - icon = IconNames.CROSS, - title = "Cancel prepared move", - small = true, - intent = Intent.DANGER, - onClick = { unprepareMove() }, - ) + BpButton { + icon = IconNames.CROSS + title = "Cancel prepared move" + small = true + intent = Intent.DANGER + onClick = { unprepareMove() } + } } } } -private fun StyledDOMBuilder<DIV>.discardText() { - styledDiv { - css { - +GlobalStyles.centerInPositionedParent - +GameStyles.discardMoveText - } +private fun ChildrenBuilder.discardText() { + div { + css(GlobalStyles.centerInPositionedParent, GameStyles.discardMoveText) {} +"DISCARD" } } -private fun StyledDOMBuilder<DIV>.upgradeWonderSymbol() { - styledImg(src = "/images/wonder-upgrade-bright.png") { +private fun ChildrenBuilder.upgradeWonderSymbol() { + img { + src = "/images/wonder-upgrade-bright.png" css { width = 8.rem position = Position.absolute left = 10.pct top = 50.pct - transform { - translate((-50).pct, (-50).pct) - } + transform = translate((-50).pct, (-50).pct) } } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/ScoreTable.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/ScoreTable.kt index 01bf139b..c9e530ce 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/ScoreTable.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/ScoreTable.kt @@ -1,52 +1,55 @@ package org.luxons.sevenwonders.ui.components.game import blueprintjs.core.* +import blueprintjs.icons.* import csstype.* -import kotlinx.css.* -import kotlinx.css.Display -import kotlinx.css.FlexDirection -import kotlinx.css.TextAlign -import kotlinx.css.VerticalAlign -import kotlinx.css.px -import kotlinx.css.rem -import kotlinx.html.TD -import kotlinx.html.TH -import org.luxons.sevenwonders.model.api.PlayerDTO -import org.luxons.sevenwonders.model.score.ScoreBoard -import org.luxons.sevenwonders.model.score.ScoreCategory -import org.luxons.sevenwonders.ui.components.GlobalStyles +import emotion.react.* +import org.luxons.sevenwonders.model.api.* +import org.luxons.sevenwonders.model.score.* +import org.luxons.sevenwonders.ui.components.* import org.luxons.sevenwonders.ui.utils.* -import react.RBuilder -import react.dom.* -import styled.* - -fun RBuilder.scoreTableOverlay(scoreBoard: ScoreBoard, players: List<PlayerDTO>, leaveGame: () -> Unit) { - bpOverlay(isOpen = true) { - bpCard { - attrs { - val fixedCenterClass = GlobalStyles.getClassName { it::fixedCenter } - val scoreBoardClass = GameStyles.getClassName { it::scoreBoard } - className = ClassName("$fixedCenterClass $scoreBoardClass") - } - styledDiv { - css { +import react.* +import react.dom.html.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.h1 +import react.dom.html.ReactHTML.sup +import react.dom.html.ReactHTML.tbody +import react.dom.html.ReactHTML.td +import react.dom.html.ReactHTML.th +import react.dom.html.ReactHTML.thead +import react.dom.html.ReactHTML.tr + +fun ChildrenBuilder.scoreTableOverlay(scoreBoard: ScoreBoard, players: List<PlayerDTO>, leaveGame: () -> Unit) { + BpOverlay { + isOpen = true + + BpCard { + css(GlobalStyles.fixedCenter, GameStyles.scoreBoard) {} + + div { + // FIXME this doesn't look right, the scoreBoard class is applied at both levels + css(GameStyles.scoreBoard) { // loads the styles so that they can be picked up by bpCard display = Display.flex flexDirection = FlexDirection.column - alignItems = Align.center - +GameStyles.scoreBoard // loads the styles so that they can be picked up by bpCard + alignItems = AlignItems.center } - styledH1 { + h1 { css { marginTop = 0.px } +"Score Board" } scoreTable(scoreBoard, players) - styledDiv { + div { css { marginTop = 1.rem } - bpButton(intent = Intent.WARNING, rightIcon = "log-out", large = true, onClick = { leaveGame() }) { + BpButton { + intent = Intent.WARNING + rightIcon = "log-out" + large = true + onClick = { leaveGame() } + +"LEAVE" } } @@ -55,18 +58,32 @@ fun RBuilder.scoreTableOverlay(scoreBoard: ScoreBoard, players: List<PlayerDTO>, } } -private fun RBuilder.scoreTable(scoreBoard: ScoreBoard, players: List<PlayerDTO>) { - bpHtmlTable(bordered = false, interactive = true) { +private fun ChildrenBuilder.scoreTable(scoreBoard: ScoreBoard, players: List<PlayerDTO>) { + BpHTMLTable { + bordered = false + interactive = true + thead { tr { - centeredTh { +"Rank" } - centeredTh { - attrs { colSpan = "2" } + th { + fullCenterInlineStyle() + +"Rank" + } + th { + fullCenterInlineStyle() + colSpan = 2 + +"Player" } - centeredTh { +"Score" } + th { + fullCenterInlineStyle() + +"Score" + } ScoreCategory.values().forEach { - centeredTh { +it.title } + th { + fullCenterInlineStyle() + +it.title + } } } } @@ -74,28 +91,44 @@ private fun RBuilder.scoreTable(scoreBoard: ScoreBoard, players: List<PlayerDTO> scoreBoard.scores.forEachIndexed { index, score -> val player = players[score.playerIndex] tr { - centeredTd { ordinal(scoreBoard.ranks[index]) } - centeredTd { bpIcon(player.icon?.name ?: "user", size = 25) } - styledTd { + td { + fullCenterInlineStyle() + ordinal(scoreBoard.ranks[index]) + } + td { + fullCenterInlineStyle() + BpIcon { + icon = player.icon?.name ?: IconNames.USER + size = 25 + } + } + td { inlineStyles { verticalAlign = VerticalAlign.middle } +player.displayName } - centeredTd { - bpTag(large = true, round = true, minimal = true) { - attrs { - this.className = GameStyles.getTypedClassName { it::totalScore } - } + td { + fullCenterInlineStyle() + BpTag { + large = true + round = true + minimal = true + className = GameStyles.totalScore + +"${score.totalPoints}" } } ScoreCategory.values().forEach { cat -> - centeredTd { - bpTag(large = true, round = true, icon = cat.icon, fill = true) { - attrs { - this.className = classNameForCategory(cat) - } + td { + fullCenterInlineStyle() + BpTag { + large = true + round = true + fill = true + icon = cat.icon + className = classNameForCategory(cat) + +"${score.pointsByCategory[cat]}" } } @@ -106,7 +139,7 @@ private fun RBuilder.scoreTable(scoreBoard: ScoreBoard, players: List<PlayerDTO> } } -private fun RBuilder.ordinal(value: Int) { +private fun ChildrenBuilder.ordinal(value: Int) { +"$value" sup { +value.ordinalIndicator() } } @@ -118,49 +151,33 @@ private fun Int.ordinalIndicator() = when { else -> "th" } -private fun RBuilder.centeredTh(block: RDOMBuilder<TH>.() -> Unit) { - th { - // inline styles necessary to overcome blueprintJS overrides - inlineStyles { - textAlign = TextAlign.center - verticalAlign = VerticalAlign.middle - } - block() +private fun HTMLAttributes<*>.fullCenterInlineStyle() { + // inline styles necessary to overcome blueprintJS overrides + inlineStyles { + textAlign = TextAlign.center + verticalAlign = VerticalAlign.middle } } -private fun RBuilder.centeredTd(block: RDOMBuilder<TD>.() -> Unit) { - td { - // inline styles necessary to overcome blueprintJS overrides - inlineStyles { - textAlign = TextAlign.center - verticalAlign = VerticalAlign.middle - } - block() - } -} - -private fun classNameForCategory(cat: ScoreCategory): ClassName = GameStyles.getTypedClassName { - when (cat) { - ScoreCategory.CIVIL -> it::civilScore - ScoreCategory.SCIENCE -> it::scienceScore - ScoreCategory.MILITARY -> it::militaryScore - ScoreCategory.TRADE -> it::tradeScore - ScoreCategory.GUILD -> it::guildScore - ScoreCategory.WONDER -> it::wonderScore - ScoreCategory.GOLD -> it::goldScore - } +private fun classNameForCategory(cat: ScoreCategory): ClassName = when (cat) { + ScoreCategory.CIVIL -> GameStyles.civilScore + ScoreCategory.SCIENCE -> GameStyles.scienceScore + ScoreCategory.MILITARY -> GameStyles.militaryScore + ScoreCategory.TRADE -> GameStyles.tradeScore + ScoreCategory.GUILD -> GameStyles.guildScore + ScoreCategory.WONDER -> GameStyles.wonderScore + ScoreCategory.GOLD -> GameStyles.goldScore } private val ScoreCategory.icon: String get() = when (this) { - ScoreCategory.CIVIL -> "office" - ScoreCategory.SCIENCE -> "lab-test" - ScoreCategory.MILITARY -> "cut" - ScoreCategory.TRADE -> "swap-horizontal" - ScoreCategory.GUILD -> "clean" // stars - ScoreCategory.WONDER -> "symbol-triangle-up" - ScoreCategory.GOLD -> "dollar" + ScoreCategory.CIVIL -> IconNames.OFFICE + ScoreCategory.SCIENCE -> IconNames.LAB_TEST + ScoreCategory.MILITARY -> IconNames.CUT + ScoreCategory.TRADE -> IconNames.SWAP_HORIZONTAL + ScoreCategory.GUILD -> IconNames.CLEAN // stars + ScoreCategory.WONDER -> IconNames.SYMBOL_TRIANGLE_UP + ScoreCategory.GOLD -> IconNames.DOLLAR } // Potentially useful emojis: diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Tokens.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Tokens.kt index da5fc5ed..c7942f59 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Tokens.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/Tokens.kt @@ -1,14 +1,15 @@ package org.luxons.sevenwonders.ui.components.game -import kotlinx.css.* -import kotlinx.html.DIV -import kotlinx.html.IMG -import kotlinx.html.title -import org.luxons.sevenwonders.model.resources.ResourceType -import org.luxons.sevenwonders.ui.components.GlobalStyles -import react.RBuilder -import react.dom.attrs -import styled.* +import csstype.* +import emotion.react.* +import org.luxons.sevenwonders.model.resources.* +import org.luxons.sevenwonders.ui.components.* +import react.* +import react.dom.html.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.img +import react.dom.html.ReactHTML.span +import web.html.* private fun getResourceTokenName(resourceType: ResourceType) = "resources/${resourceType.toString().lowercase()}" @@ -20,12 +21,12 @@ enum class TokenCountPosition { OVER, } -fun RBuilder.goldIndicator( +fun ChildrenBuilder.goldIndicator( amount: Int, amountPosition: TokenCountPosition = TokenCountPosition.OVER, - imgSize: LinearDimension = 3.rem, - customCountStyle: CssBuilder.() -> Unit = {}, - block: StyledDOMBuilder<DIV>.() -> Unit = {}, + imgSize: Length = 3.rem, + customCountStyle: PropertiesBuilder.() -> Unit = {}, + block: HTMLAttributes<HTMLDivElement>.() -> Unit = {}, ) { tokenWithCount( tokenName = "coin", @@ -38,54 +39,39 @@ fun RBuilder.goldIndicator( ) } -fun RBuilder.resourceWithCount( +fun ChildrenBuilder.resourceImage( resourceType: ResourceType, - count: Int, title: String = resourceType.toString(), - imgSize: LinearDimension? = null, - countPosition: TokenCountPosition = TokenCountPosition.RIGHT, - brightText: Boolean = false, - customCountStyle: CssBuilder.() -> Unit = {}, - block: StyledDOMBuilder<DIV>.() -> Unit = {}, + size: Length?, ) { - tokenWithCount( - tokenName = getResourceTokenName(resourceType), - count = count, - title = title, - imgSize = imgSize, - countPosition = countPosition, - brightText = brightText, - customCountStyle = customCountStyle, - block = block - ) -} - -fun RBuilder.resourceImage( - resourceType: ResourceType, - title: String = resourceType.toString(), - size: LinearDimension?, - block: StyledDOMBuilder<IMG>.() -> Unit = {}, -) { - tokenImage(getResourceTokenName(resourceType), title, size, block) + TokenImage { + this.tokenName = getResourceTokenName(resourceType) + this.title = title + this.size = size + } } -fun RBuilder.tokenWithCount( +fun ChildrenBuilder.tokenWithCount( tokenName: String, count: Int, title: String = tokenName, - imgSize: LinearDimension? = null, + imgSize: Length? = null, countPosition: TokenCountPosition = TokenCountPosition.RIGHT, brightText: Boolean = false, - customCountStyle: CssBuilder.() -> Unit = {}, - block: StyledDOMBuilder<DIV>.() -> Unit = {}, + customCountStyle: PropertiesBuilder.() -> Unit = {}, + block: HTMLAttributes<HTMLDivElement>.() -> Unit = {}, ) { - styledDiv { + div { block() - val tokenCountSize = if (imgSize != null) imgSize * 0.6 else 1.5.rem + val tokenCountSize = if (imgSize != null) 0.6 * imgSize else 1.5.rem when (countPosition) { TokenCountPosition.RIGHT -> { - tokenImage(tokenName, title = title, size = imgSize) - styledSpan { + TokenImage { + this.tokenName = tokenName + this.title = title + this.size = imgSize + } + span { css { tokenCountStyle(tokenCountSize, brightText, customCountStyle) marginLeft = 0.2.rem @@ -93,27 +79,36 @@ fun RBuilder.tokenWithCount( +"× $count" } } + TokenCountPosition.LEFT -> { - styledSpan { + span { css { tokenCountStyle(tokenCountSize, brightText, customCountStyle) marginRight = 0.2.rem } +"$count ×" } - tokenImage(tokenName, title = title, size = imgSize) + TokenImage { + this.tokenName = tokenName + this.title = title + this.size = imgSize + } } + TokenCountPosition.OVER -> { - styledDiv { + div { css { position = Position.relative // if container becomes large, this one stays small so that children stay on top of each other - width = LinearDimension.fitContent + width = Length.fitContent } - tokenImage(tokenName, title = title, size = imgSize) - styledSpan { - css { - +GlobalStyles.centerInPositionedParent + TokenImage { + this.tokenName = tokenName + this.title = title + this.size = imgSize + } + span { + css(GlobalStyles.centerInPositionedParent) { tokenCountStyle(tokenCountSize, brightText, customCountStyle) } +"$count" @@ -124,36 +119,36 @@ fun RBuilder.tokenWithCount( } } -fun RBuilder.tokenImage( - tokenName: String, - title: String = tokenName, - size: LinearDimension?, - block: StyledDOMBuilder<IMG>.() -> Unit = {}, -) { - styledImg(src = getTokenImagePath(tokenName)) { +external interface TokenImageProps : Props { + var tokenName: String + var title: String? + var size: Length? +} + +val TokenImage = FC<TokenImageProps> { props -> + img { + src = getTokenImagePath(props.tokenName) + title = props.title ?: props.tokenName + alt = props.tokenName + css { - height = size ?: 100.pct - if (size != null) { - width = size + height = props.size ?: 100.pct + if (props.size != null) { + width = props.size } verticalAlign = VerticalAlign.middle } - attrs { - this.title = title - this.alt = tokenName - } - block() } } -private fun CssBuilder.tokenCountStyle( - size: LinearDimension, +private fun PropertiesBuilder.tokenCountStyle( + size: Length, brightText: Boolean, - customStyle: CssBuilder.() -> Unit = {}, + customStyle: PropertiesBuilder.() -> Unit = {}, ) { - fontFamily = "Acme" + fontFamily = string("Acme") fontSize = size verticalAlign = VerticalAlign.middle - color = if (brightText) Color.white else Color.black + color = if (brightText) NamedColor.white else NamedColor.black customStyle() } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/TransactionsSelector.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/TransactionsSelector.kt index 66bc44d3..966a40eb 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/TransactionsSelector.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/game/TransactionsSelector.kt @@ -1,64 +1,65 @@ package org.luxons.sevenwonders.ui.components.game import blueprintjs.core.* -import kotlinx.css.* -import kotlinx.html.DIV -import kotlinx.html.TBODY -import kotlinx.html.TD -import kotlinx.html.classes -import kotlinx.html.js.onClickFunction -import org.luxons.sevenwonders.model.PlayerMove -import org.luxons.sevenwonders.model.api.PlayerDTO +import blueprintjs.icons.* +import csstype.* +import emotion.react.* +import org.luxons.sevenwonders.model.* +import org.luxons.sevenwonders.model.api.* import org.luxons.sevenwonders.model.resources.* import org.luxons.sevenwonders.model.resources.Provider -import org.luxons.sevenwonders.ui.components.gameBrowser.playerInfo +import org.luxons.sevenwonders.ui.components.gameBrowser.* import org.luxons.sevenwonders.ui.utils.* import react.* -import react.dom.* -import styled.* +import react.dom.html.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.p +import react.dom.html.ReactHTML.tbody +import react.dom.html.ReactHTML.td +import react.dom.html.ReactHTML.tr +import web.html.* -fun RBuilder.transactionsSelectorDialog( +fun ChildrenBuilder.transactionsSelectorDialog( state: TransactionSelectorState?, neighbours: Pair<PlayerDTO, PlayerDTO>, prepareMove: (PlayerMove) -> Unit, cancelTransactionSelection: () -> Unit, ) { - bpDialog( - isOpen = state != null, - title = "Trading time!", - canEscapeKeyClose = true, - canOutsideClickClose = true, - isCloseButtonShown = true, - onClose = cancelTransactionSelection, - ) { - attrs { - className = GameStyles.getTypedClassName { it::transactionsSelector } - } - div { - attrs { - classes += Classes.DIALOG_BODY - } + BpDialog { + isOpen = state != null + titleText = "Trading time!" + canEscapeKeyClose = true + canOutsideClickClose = true + isCloseButtonShown = true + onClose = { cancelTransactionSelection() } + + className = GameStyles.transactionsSelector + + BpDialogBody { p { +"You don't have enough resources to perform this move, but you can buy them from neighbours. " +"Please pick an option:" } if (state != null) { // should always be true when the dialog is rendered - styledDiv { + div { css { - margin(all = LinearDimension.auto) + margin = Margin(all = Auto.auto) display = Display.flex - alignItems = Align.center + alignItems = AlignItems.center } neighbour(neighbours.first) - styledDiv { + div { css { - flex(Flex.GROW) - margin(horizontal = 0.5.rem) + flexGrow = number(1.0) + margin = Margin(vertical = 0.rem, horizontal = 0.5.rem) display = Display.flex flexDirection = FlexDirection.column - alignItems = Align.center + alignItems = AlignItems.center + } + OptionsTable { + this.state = state + this.prepareMove = prepareMove } - optionsTable(state, prepareMove) } neighbour(neighbours.second) } @@ -67,28 +68,21 @@ fun RBuilder.transactionsSelectorDialog( } } -private fun StyledDOMBuilder<DIV>.neighbour(player: PlayerDTO) { - styledDiv { +private fun ChildrenBuilder.neighbour(player: PlayerDTO) { + div { css { width = 12.rem // center the icon display = Display.flex flexDirection = FlexDirection.column - alignItems = Align.center + alignItems = AlignItems.center } - playerInfo(player, iconSize = 40, orientation = FlexDirection.column, ellipsize = false) - } -} - -private fun RBuilder.optionsTable( - state: TransactionSelectorState, - prepareMove: (PlayerMove) -> Unit, -) { - child(optionsTable) { - attrs { - this.state = state - this.prepareMove = prepareMove + PlayerInfo { + this.player = player + this.iconSize = 40 + this.orientation = FlexDirection.column + this.ellipsize = false } } } @@ -98,7 +92,7 @@ private external interface OptionsTableProps : PropsWithChildren { var prepareMove: (PlayerMove) -> Unit } -private val optionsTable = fc<OptionsTableProps> { props -> +private val OptionsTable = FC<OptionsTableProps> { props -> val state = props.state val prepareMove = props.prepareMove @@ -107,7 +101,8 @@ private val optionsTable = fc<OptionsTableProps> { props -> val bestPrice = state.transactionsOptions.bestPrice val (cheapestOptions, otherOptions) = state.transactionsOptions.partition { it.totalPrice == bestPrice } - bpHtmlTable(interactive = true) { + BpHTMLTable { + interactive = true tbody { cheapestOptions.forEach { transactions -> transactionsOptionRow( @@ -130,84 +125,86 @@ private val optionsTable = fc<OptionsTableProps> { props -> if (otherOptions.isNotEmpty()) { val icon = if (expanded) "chevron-up" else "chevron-down" val text = if (expanded) "Hide expensive options" else "Show more expensive options" - bpButton( - minimal = true, - small = true, - icon = icon, - rightIcon = icon, - onClick = { expanded = !expanded }, - ) { + BpButton { + this.minimal = true + this.small = true + this.icon = icon + this.rightIcon = icon + this.onClick = { expanded = !expanded } + +text } } } -private fun RDOMBuilder<TBODY>.transactionsOptionRow( +private fun ChildrenBuilder.transactionsOptionRow( transactions: PricedResourceTransactions, showBestPriceIndicator: Boolean, onClick: () -> Unit, ) { - styledTr { + tr { css { cursor = Cursor.pointer - alignItems = Align.center - } - attrs { - onClickFunction = { onClick() } + alignItems = AlignItems.center } + this.onClick = { onClick() } // there should be at most one of each val leftTr = transactions.firstOrNull { it.provider == Provider.LEFT_PLAYER } val rightTr = transactions.firstOrNull { it.provider == Provider.RIGHT_PLAYER } - styledTd { + td { transactionCellCss() - styledDiv { - css { opacity = if (leftTr == null) 0.5 else 1 } + div { + css { opacity = number(if (leftTr == null) 0.5 else 1.0) } transactionCellInnerCss() - bpIcon(name = "caret-left", size = IconSize.LARGE) + BpIcon { + icon = IconNames.CARET_LEFT + size = IconSize.LARGE + } goldIndicator(leftTr?.totalPrice ?: 0, imgSize = 2.5.rem) } } - styledTd { + td { transactionCellCss() if (leftTr != null) { resourceList(leftTr.resources) } } - styledTd { + td { transactionCellCss() css { width = 1.5.rem } if (showBestPriceIndicator) { bestPriceIndicator() } } - styledTd { + td { transactionCellCss() if (rightTr != null) { resourceList(rightTr.resources) } } - styledTd { + td { transactionCellCss() - styledDiv { - css { opacity = if (rightTr == null) 0.5 else 1 } + div { + css { opacity = number(if (rightTr == null) 0.5 else 1.0) } transactionCellInnerCss() goldIndicator(rightTr?.totalPrice ?: 0, imgSize = 2.5.rem) - bpIcon(name = "caret-right", size = IconSize.LARGE) + BpIcon { + icon = IconNames.CARET_RIGHT + size = IconSize.LARGE + } } } } } -private fun StyledDOMBuilder<TD>.bestPriceIndicator() { - styledDiv { - css { - +GameStyles.bestPrice - } +private fun ChildrenBuilder.bestPriceIndicator() { + div { + css(GameStyles.bestPrice){} +"Best\nprice!" } } -private fun StyledDOMBuilder<TD>.transactionCellCss() { +private fun HTMLAttributes<HTMLTableCellElement>.transactionCellCss() { // we need inline styles to win over BlueprintJS's styles (which are more specific than .class) inlineStyles { verticalAlign = VerticalAlign.middle @@ -215,15 +212,15 @@ private fun StyledDOMBuilder<TD>.transactionCellCss() { } } -private fun StyledDOMBuilder<DIV>.transactionCellInnerCss() { +private fun HTMLAttributes<HTMLDivElement>.transactionCellInnerCss() { css { display = Display.flex flexDirection = FlexDirection.row - alignItems = Align.center + alignItems = AlignItems.center } } -private fun RBuilder.resourceList(countedResources: List<CountedResource>) { +private fun ChildrenBuilder.resourceList(countedResources: List<CountedResource>) { val resources = countedResources.toRepeatedTypesList() // The biggest card is the Palace and requires 7 resources (1 of each). @@ -231,35 +228,35 @@ private fun RBuilder.resourceList(countedResources: List<CountedResource>) { // Therefore, 3 by row seems decent. When there are 4 items, it's visually better to have a 2x2 matrix, though. val rows = resources.chunked(if (resources.size == 4) 2 else 3) - val imgSize = 1.5.rem - styledDiv { + val imgSize = 1.5 + div { css { display = Display.flex flexDirection = FlexDirection.column - alignItems = Align.center + alignItems = AlignItems.center justifyContent = JustifyContent.center - flex(Flex.GROW) + flexGrow = number(1.0) // this ensures stable dimensions, no matter how many resources (up to 2x3 matrix) - width = imgSize * 3 - height = imgSize * 2 + width = (imgSize * 3).rem + height = (imgSize * 2).rem } rows.forEach { row -> - styledDiv { + div { resourceRowCss() row.forEach { - resourceImage(it, size = imgSize) + resourceImage(it, size = imgSize.rem) } } } } } -private fun StyledDOMBuilder<DIV>.resourceRowCss() { +private fun HTMLAttributes<HTMLDivElement>.resourceRowCss() { css { display = Display.flex flexDirection = FlexDirection.row - alignItems = Align.center - margin(horizontal = LinearDimension.auto) + alignItems = AlignItems.center + margin = Margin(vertical = 0.px, horizontal = Auto.auto) } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/CreateGameForm.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/CreateGameForm.kt index 0d148744..ae79125b 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/CreateGameForm.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/CreateGameForm.kt @@ -2,67 +2,48 @@ package org.luxons.sevenwonders.ui.components.gameBrowser import blueprintjs.core.* import blueprintjs.icons.* -import kotlinx.css.* -import kotlinx.html.js.* +import csstype.* +import emotion.react.* import org.luxons.sevenwonders.ui.redux.* import react.* -import react.dom.* -import styled.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.form -private external interface CreateGameFormProps : PropsWithChildren { - var createGame: (String) -> Unit -} - -private external interface CreateGameFormState : State { - var gameName: String -} - -private class CreateGameForm(props: CreateGameFormProps) : RComponent<CreateGameFormProps, CreateGameFormState>(props) { +val CreateGameForm = VFC { + var gameName by useState("") - override fun CreateGameFormState.init(props: CreateGameFormProps) { - gameName = "" - } + val dispatch = useSwDispatch() + val createGame = { dispatch(RequestCreateGame(gameName)) } - override fun RBuilder.render() { - styledDiv { - css { - display = Display.flex - flexDirection = FlexDirection.row - justifyContent = JustifyContent.spaceBetween + div { + css { + display = Display.flex + flexDirection = FlexDirection.row + justifyContent = JustifyContent.spaceBetween + } + form { + onSubmit = { e -> + e.preventDefault() + createGame() } - form { - attrs { - onSubmitFunction = { e -> - e.preventDefault() + + BpInputGroup { + large = true + placeholder = "Game name" + onChange = { e -> + val input = e.currentTarget + gameName = input.value + } + rightElement = BpButton.create { + minimal = true + intent = Intent.PRIMARY + icon = IconNames.ADD + onClick = { e -> + e.preventDefault() // prevents refreshing the page when pressing Enter createGame() } } - - bpInputGroup( - large = true, - placeholder = "Game name", - onChange = { e -> - val input = e.currentTarget - state.gameName = input.value - }, - rightElement = createGameButton(), - ) } } } - - private fun createGameButton() = buildElement { - bpButton(minimal = true, intent = Intent.PRIMARY, icon = IconNames.ADD, onClick = { e -> - e.preventDefault() // prevents refreshing the page when pressing Enter - createGame() - }) - } - - private fun createGame() { - props.createGame(state.gameName) - } -} - -val createGameForm: ComponentClass<PropsWithChildren> = connectDispatch(CreateGameForm::class) { dispatch, _ -> - createGame = { name -> dispatch(RequestCreateGame(name)) } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowser.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowser.kt index 579e6cb2..16b0965d 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowser.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowser.kt @@ -1,51 +1,69 @@ package org.luxons.sevenwonders.ui.components.gameBrowser import blueprintjs.core.* -import kotlinx.css.* -import kotlinx.html.* -import org.luxons.sevenwonders.ui.components.GlobalStyles +import csstype.* +import emotion.react.* +import org.luxons.sevenwonders.ui.components.* +import org.luxons.sevenwonders.ui.redux.* import org.luxons.sevenwonders.ui.utils.* import react.* -import react.dom.* -import styled.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.h1 +import react.dom.html.ReactHTML.h2 -fun RBuilder.gameBrowser() = styledDiv { - css { - +GlobalStyles.fullscreen - +GlobalStyles.zeusBackground - padding(all = 1.rem) - } - styledDiv { - attrs { - classes += Classes.DARK - } - css { - margin(horizontal = LinearDimension.auto) - maxWidth = GlobalStyles.preGameWidth +val GameBrowser = VFC { + div { + css(GlobalStyles.fullscreen, GlobalStyles.zeusBackground) { + padding = Padding(all = 1.rem) } - styledDiv { - css { - display = Display.flex - justifyContent = JustifyContent.spaceBetween + div { + css(ClassName(Classes.DARK)) { + margin = Margin(vertical = 0.px, horizontal = Auto.auto) + maxWidth = GlobalStyles.preGameWidth } - h1 { +"Games" } - currentPlayerInfo() - } + div { + css { + display = Display.flex + justifyContent = JustifyContent.spaceBetween + } + h1 { +"Games" } + CurrentPlayerInfo() + } + + BpCard { + css { + marginBottom = 1.rem + } - bpCard(className = GameBrowserStyles.getTypedClassName { it::createGameCard }) { - styledH2 { - css { +GameBrowserStyles.cardTitle } - +"Create a Game" + h2 { + css { + marginTop = 0.px + } + +"Create a Game" + } + CreateGameForm() } - createGameForm {} - } - bpCard { - styledH2 { - css { +GameBrowserStyles.cardTitle } - +"Join a Game" + BpCard { + h2 { + css { + marginTop = 0.px + } + +"Join a Game" + } + GameList() } - gameList() } } } + +val CurrentPlayerInfo = VFC { + val connectedPlayer = useSwSelector { it.connectedPlayer } + PlayerInfo { + player = connectedPlayer + iconSize = 30 + showUsername = true + orientation = FlexDirection.row + ellipsize = false + } +} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowserStyles.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowserStyles.kt deleted file mode 100644 index 611991c2..00000000 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameBrowserStyles.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.luxons.sevenwonders.ui.components.gameBrowser - -import kotlinx.css.* -import styled.StyleSheet - -object GameBrowserStyles : StyleSheet("GameBrowserStyles", isStatic = true) { - - val cardTitle by css { - marginTop = 0.px - } - - val createGameCard by css { - marginBottom = 1.rem - } - - val gameTable by css { - width = 100.pct - } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameList.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameList.kt index a1f47045..2437d9e0 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameList.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/GameList.kt @@ -1,41 +1,50 @@ package org.luxons.sevenwonders.ui.components.gameBrowser import blueprintjs.core.* -import blueprintjs.icons.IconNames +import blueprintjs.icons.* import csstype.* -import kotlinx.css.* -import kotlinx.css.Display -import kotlinx.css.FlexDirection -import kotlinx.css.JustifyContent -import kotlinx.css.TextAlign -import kotlinx.css.VerticalAlign -import kotlinx.css.rem -import kotlinx.html.classes -import kotlinx.html.title -import org.luxons.sevenwonders.model.api.ConnectedPlayer -import org.luxons.sevenwonders.model.api.LobbyDTO +import emotion.react.* +import org.luxons.sevenwonders.model.api.* import org.luxons.sevenwonders.model.api.State -import org.luxons.sevenwonders.ui.redux.RequestJoinGame -import org.luxons.sevenwonders.ui.redux.connectStateAndDispatch +import org.luxons.sevenwonders.ui.redux.* +import org.luxons.sevenwonders.ui.utils.* import react.* -import react.dom.* -import styled.* +import react.dom.html.ReactHTML.col +import react.dom.html.ReactHTML.colgroup +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.span +import react.dom.html.ReactHTML.tbody +import react.dom.html.ReactHTML.td +import react.dom.html.ReactHTML.th +import react.dom.html.ReactHTML.thead +import react.dom.html.ReactHTML.tr import react.State as RState -external interface GameListStateProps : PropsWithChildren { +external interface GameListStateProps : Props { var connectedPlayer: ConnectedPlayer var games: List<LobbyDTO> } -external interface GameListDispatchProps : PropsWithChildren { +external interface GameListDispatchProps : Props { var joinGame: (Long) -> Unit } external interface GameListProps : GameListStateProps, GameListDispatchProps -class GameListPresenter(props: GameListProps) : RComponent<GameListProps, RState>(props) { +val GameList = connectStateAndDispatch<GameListStateProps, GameListDispatchProps, GameListProps>( + clazz = GameListPresenter::class, + mapStateToProps = { state, _ -> + connectedPlayer = state.connectedPlayer ?: error("there should be a connected player") + games = state.games + }, + mapDispatchToProps = { dispatch, _ -> + joinGame = { gameId -> dispatch(RequestJoinGame(gameId = gameId)) } + }, +) + +private class GameListPresenter(props: GameListProps) : Component<GameListProps, RState>(props) { - override fun RBuilder.render() { + override fun render() = Fragment.create { if (props.games.isEmpty()) { noGamesInfo() } else { @@ -43,16 +52,13 @@ class GameListPresenter(props: GameListProps) : RComponent<GameListProps, RState } } - private fun RBuilder.noGamesInfo() { - bpNonIdealState( - icon = IconNames.GEOSEARCH, - title = "No games to join", - ) { - styledDiv { - attrs { - classes += Classes.RUNNING_TEXT - } - css { + private fun ChildrenBuilder.noGamesInfo() { + BpNonIdealState { + icon = IconNames.GEOSEARCH + titleText = "No games to join" + + div { + css(ClassName(Classes.RUNNING_TEXT)) { maxWidth = 35.rem } +"Nobody seems to be playing at the moment. " @@ -61,11 +67,12 @@ class GameListPresenter(props: GameListProps) : RComponent<GameListProps, RState } } - private fun RBuilder.gamesTable() { - bpHtmlTable { - attrs { - className = ClassName(GameBrowserStyles.getClassName { it::gameTable }) + private fun ChildrenBuilder.gamesTable() { + BpHTMLTable { + css { + width = 100.pct } + columnWidthsSpec() thead { gameListHeaderRow() @@ -78,26 +85,26 @@ class GameListPresenter(props: GameListProps) : RComponent<GameListProps, RState } } - private fun RElementBuilder<HTMLTableProps>.columnWidthsSpec() { + private fun ChildrenBuilder.columnWidthsSpec() { colgroup { - styledCol { + col { css { width = 40.rem } } - styledCol { + col { css { width = 5.rem textAlign = TextAlign.center } } - styledCol { + col { css { width = 5.rem textAlign = TextAlign.center // use inline style on th instead to overcome blueprint style } } - styledCol { + col { css { width = 3.rem textAlign = TextAlign.center @@ -106,7 +113,7 @@ class GameListPresenter(props: GameListProps) : RComponent<GameListProps, RState } } - private fun RBuilder.gameListHeaderRow() = tr { + private fun ChildrenBuilder.gameListHeaderRow() = tr { th { +"Name" } @@ -124,10 +131,8 @@ class GameListPresenter(props: GameListProps) : RComponent<GameListProps, RState } } - private fun RBuilder.gameListItemRow(lobby: LobbyDTO) = styledTr { - attrs { - key = lobby.id.toString() - } + private fun ChildrenBuilder.gameListItemRow(lobby: LobbyDTO) = tr { + key = lobby.id.toString() // inline styles necessary to overcome BlueprintJS's verticalAlign=top td { inlineStyles { gameTableCellStyle() } @@ -150,37 +155,41 @@ class GameListPresenter(props: GameListProps) : RComponent<GameListProps, RState } } - private fun StyledElement.gameTableHeaderCellStyle() { + private fun PropertiesBuilder.gameTableHeaderCellStyle() { textAlign = TextAlign.center } - private fun StyledElement.gameTableCellStyle() { + private fun PropertiesBuilder.gameTableCellStyle() { verticalAlign = VerticalAlign.middle } - private fun RBuilder.gameStatus(state: State) { + private fun ChildrenBuilder.gameStatus(state: State) { val intent = when (state) { State.LOBBY -> Intent.SUCCESS State.PLAYING -> Intent.WARNING State.FINISHED -> Intent.DANGER } - bpTag(minimal = true, intent = intent) { + BpTag { + this.minimal = true + this.intent = intent + +state.toString() } } - private fun RBuilder.playerCount(nPlayers: Int) { - styledDiv { + private fun ChildrenBuilder.playerCount(nPlayers: Int) { + div { css { display = Display.flex flexDirection = FlexDirection.row justifyContent = JustifyContent.center } - attrs { - title = "Number of players" + title = "Number of players" + BpIcon { + icon = IconNames.PEOPLE + title = null } - bpIcon(name = "people", title = null) - styledSpan { + span { css { marginLeft = 0.3.rem } @@ -189,28 +198,15 @@ class GameListPresenter(props: GameListProps) : RComponent<GameListProps, RState } } - private fun RBuilder.joinButton(lobby: LobbyDTO) { + private fun ChildrenBuilder.joinButton(lobby: LobbyDTO) { val joinability = lobby.joinability(props.connectedPlayer.displayName) - bpButton( - minimal = true, - large = true, - title = joinability.tooltip, - icon = "arrow-right", - disabled = !joinability.canDo, - onClick = { props.joinGame(lobby.id) }, - ) + BpButton { + minimal = true + large = true + title = joinability.tooltip + icon = "arrow-right" + disabled = !joinability.canDo + onClick = { props.joinGame(lobby.id) } + } } } - -fun RBuilder.gameList() = gameList {} - -private val gameList = connectStateAndDispatch<GameListStateProps, GameListDispatchProps, GameListProps>( - clazz = GameListPresenter::class, - mapStateToProps = { state, _ -> - connectedPlayer = state.connectedPlayer ?: error("there should be a connected player") - games = state.games - }, - mapDispatchToProps = { dispatch, _ -> - joinGame = { gameId -> dispatch(RequestJoinGame(gameId = gameId)) } - }, -) diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/PlayerInfo.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/PlayerInfo.kt index aa1033a0..cae25ba5 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/PlayerInfo.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/gameBrowser/PlayerInfo.kt @@ -1,51 +1,52 @@ package org.luxons.sevenwonders.ui.components.gameBrowser -import blueprintjs.core.bpIcon -import kotlinx.css.* -import kotlinx.html.title -import org.luxons.sevenwonders.model.api.BasicPlayerInfo -import org.luxons.sevenwonders.model.api.PlayerDTO -import org.luxons.sevenwonders.ui.redux.connectState +import blueprintjs.core.* +import csstype.* +import emotion.react.* +import org.luxons.sevenwonders.model.api.* import react.* -import react.dom.attrs -import styled.css -import styled.styledDiv -import styled.styledSpan +import react.State +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.span external interface PlayerInfoProps : PropsWithChildren { var player: BasicPlayerInfo? - var showUsername: Boolean - var iconSize: Int - var orientation: FlexDirection - var ellipsize: Boolean + var showUsername: Boolean? + var iconSize: Int? + var orientation: FlexDirection? + var ellipsize: Boolean? } -class PlayerInfoPresenter(props: PlayerInfoProps) : RComponent<PlayerInfoProps, State>(props) { +val PlayerInfo = PlayerInfoPresenter::class.react - override fun RBuilder.render() { - styledDiv { - css { - display = Display.flex - alignItems = Align.center - flexDirection = props.orientation +private class PlayerInfoPresenter(props: PlayerInfoProps) : Component<PlayerInfoProps, State>(props) { + + override fun render() = div.create { + val orientation = props.orientation ?: FlexDirection.row + css { + display = Display.flex + alignItems = AlignItems.center + flexDirection = orientation + } + props.player?.let { + BpIcon { + icon = it.icon?.name ?: "user" + size = props.iconSize ?: 30 } - props.player?.let { - bpIcon(name = it.icon?.name ?: "user", size = props.iconSize) - if (props.showUsername) { - playerNameWithUsername(it.displayName, it.username) { - iconSeparationMargin() - } - } else { - playerName(it.displayName) { - iconSeparationMargin() - } + if (props.showUsername == true) { + playerNameWithUsername(it.displayName, it.username) { + iconSeparationMargin(orientation) + } + } else { + playerName(it.displayName) { + iconSeparationMargin(orientation) } } } } - private fun RBuilder.playerName(displayName: String, style: CssBuilder.() -> Unit = {}) { - styledSpan { + private fun ChildrenBuilder.playerName(displayName: String, style: PropertiesBuilder.() -> Unit = {}) { + span { css { fontSize = 1.rem if (props.orientation == FlexDirection.column) { @@ -55,10 +56,9 @@ class PlayerInfoPresenter(props: PlayerInfoProps) : RComponent<PlayerInfoProps, } // TODO replace by BlueprintJS's Text elements (built-in ellipsize based on width) val maxDisplayNameLength = 15 - if (props.ellipsize && displayName.length > maxDisplayNameLength) { - attrs { - title = displayName - } + val ellipsize = props.ellipsize ?: true + if (ellipsize && displayName.length > maxDisplayNameLength) { + title = displayName +displayName.ellipsize(maxDisplayNameLength) } else { +displayName @@ -68,33 +68,33 @@ class PlayerInfoPresenter(props: PlayerInfoProps) : RComponent<PlayerInfoProps, private fun String.ellipsize(maxLength: Int) = take(maxLength - 1) + "…" - private fun CssBuilder.iconSeparationMargin() { + private fun PropertiesBuilder.iconSeparationMargin(orientation: FlexDirection) { val margin = 0.4.rem - when (props.orientation) { + when (orientation) { FlexDirection.row -> marginLeft = margin FlexDirection.column -> marginTop = margin FlexDirection.rowReverse -> marginRight = margin FlexDirection.columnReverse -> marginBottom = margin - else -> error("Unsupported orientation '${props.orientation}' for player info component") + else -> error("Unsupported orientation '$orientation' for player info component") } } - private fun RBuilder.playerNameWithUsername( + private fun ChildrenBuilder.playerNameWithUsername( displayName: String, username: String, - style: CssBuilder.() -> Unit = {} + style: PropertiesBuilder.() -> Unit = {} ) { - styledDiv { + div { css { display = Display.flex flexDirection = FlexDirection.column style() } playerName(displayName) - styledSpan { + span { css { marginTop = 0.1.rem - color = Color.lightGray + color = NamedColor.lightgray fontSize = 0.8.rem } +"($username)" @@ -102,32 +102,3 @@ class PlayerInfoPresenter(props: PlayerInfoProps) : RComponent<PlayerInfoProps, } } } - -fun RBuilder.playerInfo( - player: PlayerDTO, - showUsername: Boolean = false, - iconSize: Int = 30, - orientation: FlexDirection = FlexDirection.row, - ellipsize: Boolean = true, -) = child(PlayerInfoPresenter::class) { - attrs { - this.player = player - this.showUsername = showUsername - this.iconSize = iconSize - this.orientation = orientation - this.ellipsize = ellipsize - } -} - -fun RBuilder.currentPlayerInfo() = playerInfo {} - -private val playerInfo = connectState( - clazz = PlayerInfoPresenter::class, - mapStateToProps = { state, _ -> - player = state.connectedPlayer - iconSize = 30 - showUsername = true - orientation = FlexDirection.row - ellipsize = false - }, -) diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt index 27a2057c..8de9f53e 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/ChooseNameForm.kt @@ -2,93 +2,89 @@ package org.luxons.sevenwonders.ui.components.home import blueprintjs.core.* import blueprintjs.icons.* -import kotlinx.css.* -import kotlinx.html.js.* +import csstype.* +import emotion.react.* import org.luxons.sevenwonders.ui.redux.* +import org.luxons.sevenwonders.ui.utils.* import react.* -import styled.* +import react.dom.events.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.form +import web.html.* -private external interface ChooseNameFormProps : PropsWithChildren { - var chooseUsername: (String) -> Unit +val ChooseNameForm = VFC { + val dispatch = useSwDispatch() + ChooseNameFormPresenter { + chooseUsername = { name -> dispatch(RequestChooseName(name)) } + } } -private external interface ChooseNameFormState : State { - var username: String +private external interface ChooseNameFormPresenterProps : PropsWithChildren { + var chooseUsername: (String) -> Unit } -private class ChooseNameForm(props: ChooseNameFormProps) : RComponent<ChooseNameFormProps, ChooseNameFormState>(props) { - - override fun ChooseNameFormState.init(props: ChooseNameFormProps) { - username = "" - } +private val ChooseNameFormPresenter = FC<ChooseNameFormPresenterProps> { props -> + var usernameState by useState("") - override fun RBuilder.render() { - styledForm { - css { - display = Display.flex - flexDirection = FlexDirection.row + form { + css { + display = Display.flex + flexDirection = FlexDirection.row + } + onSubmit = { e -> + e.preventDefault() + props.chooseUsername(usernameState) + } + RandomNameButton { + onClick = { usernameState = randomGreekName() } + } + spacer() + BpInputGroup { + large = true + placeholder = "Username" + rightElement = SubmitButton.create { + onClick = { e -> + e.preventDefault() + props.chooseUsername(usernameState) + } } - attrs.onSubmitFunction = { e -> - e.preventDefault() - chooseUsername() + value = usernameState + onChange = { e -> + val input = e.currentTarget + usernameState = input.value } - randomNameButton() - spacer() - bpInputGroup( - large = true, - placeholder = "Username", - rightElement = submitButton(), - value = state.username, - onChange = { e -> - val input = e.currentTarget - setState { - username = input.value - } - }, - ) } } +} - private fun submitButton(): ReactElement<*> = buildElement { - bpButton( - minimal = true, - icon = IconNames.ARROW_RIGHT, - intent = Intent.PRIMARY, - onClick = { e -> - e.preventDefault() - chooseUsername() - }, - ) - } - - private fun RBuilder.randomNameButton() { - bpButton( - title = "Generate random name", - large = true, - icon = IconNames.RANDOM, - intent = Intent.PRIMARY, - onClick = { fillRandomUsername() }, - ) - } +private external interface SpecificButtonProps : Props { + var onClick: MouseEventHandler<HTMLElement>? +} - private fun fillRandomUsername() { - setState { username = randomGreekName() } +private val SubmitButton = FC<SpecificButtonProps> { props -> + BpButton { + minimal = true + icon = IconNames.ARROW_RIGHT + intent = Intent.PRIMARY + onClick = props.onClick } +} - private fun chooseUsername() { - props.chooseUsername(state.username) +private val RandomNameButton = FC<SpecificButtonProps> { props -> + BpButton { + title = "Generate random name" + large = true + icon = IconNames.RANDOM + intent = Intent.PRIMARY + onClick = props.onClick } +} - // TODO this is so bad I'm dying inside - private fun RBuilder.spacer() { - styledDiv { - css { - margin(2.px) - } +// TODO this is so bad I'm dying inside +private fun ChildrenBuilder.spacer() { + div { + css { + margin = Margin(all = 2.px) } } } - -val chooseNameForm: ComponentClass<PropsWithChildren> = connectDispatch(ChooseNameForm::class) { dispatch, _ -> - chooseUsername = { name -> dispatch(RequestChooseName(name)) } -} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/Home.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/Home.kt index 4b209979..b821b23d 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/Home.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/Home.kt @@ -1,21 +1,22 @@ package org.luxons.sevenwonders.ui.components.home -import org.luxons.sevenwonders.ui.components.GlobalStyles -import react.RBuilder -import react.dom.* -import styled.css -import styled.styledDiv +import emotion.react.* +import org.luxons.sevenwonders.ui.components.* +import react.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.img private const val LOGO = "images/logo-7-wonders.png" -fun RBuilder.home() = styledDiv { - css { - +GlobalStyles.fullscreen - +GlobalStyles.zeusBackground - +HomeStyles.centerChildren - } +val Home = VFC("Home") { + div { + css(GlobalStyles.fullscreen, GlobalStyles.zeusBackground, HomeStyles.centerChildren) {} - img(src = LOGO, alt = "Seven Wonders") {} + img { + src = LOGO + alt = "Seven Wonders" + } - chooseNameForm {} + ChooseNameForm() + } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/HomeStyles.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/HomeStyles.kt index 10037b36..fa0a83ad 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/HomeStyles.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/home/HomeStyles.kt @@ -1,14 +1,14 @@ package org.luxons.sevenwonders.ui.components.home -import kotlinx.css.* -import styled.StyleSheet +import csstype.* +import emotion.css.* -object HomeStyles : StyleSheet("HomeStyles", isStatic = true) { +object HomeStyles { - val centerChildren by css { + val centerChildren = ClassName { display = Display.flex flexDirection = FlexDirection.column - alignItems = Align.center + alignItems = AlignItems.center justifyContent = JustifyContent.center } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt index 7435ffb0..83f6aa7b 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Lobby.kt @@ -3,31 +3,51 @@ package org.luxons.sevenwonders.ui.components.lobby import blueprintjs.core.* import blueprintjs.icons.* import csstype.* -import kotlinx.css.* -import kotlinx.css.Display -import kotlinx.css.JustifyContent -import kotlinx.css.Position -import kotlinx.css.pct -import kotlinx.css.properties.* -import kotlinx.css.px -import kotlinx.css.rem +import csstype.Position +import emotion.react.* import org.luxons.sevenwonders.model.api.* import org.luxons.sevenwonders.model.wonders.* -import org.luxons.sevenwonders.ui.components.GlobalStyles +import org.luxons.sevenwonders.ui.components.* import org.luxons.sevenwonders.ui.redux.* +import org.luxons.sevenwonders.ui.utils.* import react.* -import react.State -import react.dom.* -import styled.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.h1 +import react.dom.html.ReactHTML.h2 +import react.dom.html.ReactHTML.h3 +import react.dom.html.ReactHTML.h4 private val BOT_NAMES = listOf("Wall-E", "B-Max", "Sonny", "T-800", "HAL", "GLaDOS", "R2-D2", "Bender", "AWESOM-O") -external interface LobbyStateProps : PropsWithChildren { - var currentGame: LobbyDTO? - var currentPlayer: PlayerDTO? +val Lobby = VFC(displayName = "Lobby") { + val lobby = useSwSelector { it.currentLobby } + val player = useSwSelector { it.currentPlayer } + + val dispatch = useSwDispatch() + + if (lobby == null || player == null) { + BpNonIdealState { + icon = IconNames.ERROR + titleText = "Error: no current game" + } + } else { + LobbyPresenter { + currentGame = lobby + currentPlayer = player + + startGame = { dispatch(RequestStartGame()) } + addBot = { name -> dispatch(RequestAddBot(name)) } + leaveLobby = { dispatch(RequestLeaveLobby()) } + disbandLobby = { dispatch(RequestDisbandLobby()) } + reorderPlayers = { orderedPlayers -> dispatch(RequestReorderPlayers(orderedPlayers)) } + reassignWonders = { wonders -> dispatch(RequestReassignWonders(wonders)) } + } + } } -external interface LobbyDispatchProps : PropsWithChildren { +private external interface LobbyPresenterProps : Props { + var currentGame: LobbyDTO + var currentPlayer: PlayerDTO var startGame: () -> Unit var addBot: (displayName: String) -> Unit var leaveLobby: () -> Unit @@ -36,235 +56,217 @@ external interface LobbyDispatchProps : PropsWithChildren { var reassignWonders: (wonders: List<AssignedWonder>) -> Unit } -interface LobbyProps : LobbyDispatchProps, LobbyStateProps - -class LobbyPresenter(props: LobbyProps) : RComponent<LobbyProps, State>(props) { - - override fun RBuilder.render() { - val currentGame = props.currentGame - val currentPlayer = props.currentPlayer - if (currentGame == null || currentPlayer == null) { - bpNonIdealState(icon = IconNames.ERROR, title = "Error: no current game") - return +private val LobbyPresenter = FC<LobbyPresenterProps> { props -> + div { + css(GlobalStyles.fullscreen, GlobalStyles.zeusBackground) { + padding = Padding(all = 1.rem) } - styledDiv { - css { - +GlobalStyles.fullscreen - +GlobalStyles.zeusBackground - padding(all = 1.rem) + div { + css(ClassName(Classes.DARK), LobbyStyles.contentContainer) { + margin = Margin(vertical = 0.rem, horizontal = Auto.auto) + maxWidth = GlobalStyles.preGameWidth } - styledDiv { + h1 { +"${props.currentGame.name} — Lobby" } + + radialPlayerList(props.currentGame.players, props.currentPlayer) { css { - classes.add(Classes.DARK) - +LobbyStyles.contentContainer - } - h1 { +"${currentGame.name} — Lobby" } - - radialPlayerList(currentGame.players, currentPlayer) { - css { - // to make players more readable on the background - background = "radial-gradient(closest-side, black 20%, transparent)" - // make it bigger so the background covers more ground - width = 40.rem - height = 40.rem - } + // to make players more readable on the background + background = "radial-gradient(closest-side, black 20%, transparent)".unsafeCast<Gradient>() + // make it bigger so the background covers more ground + width = 40.rem + height = 40.rem } - actionButtons(currentPlayer, currentGame) + } + actionButtons(props.currentPlayer, props.currentGame, props.startGame, props.leaveLobby, props.disbandLobby, props.addBot) - if (currentPlayer.isGameOwner) { - setupPanel(currentGame) - } + if (props.currentPlayer.isGameOwner) { + setupPanel(props.currentGame, props.reorderPlayers, props.reassignWonders) } } } +} + +private fun ChildrenBuilder.actionButtons( + currentPlayer: PlayerDTO, + currentGame: LobbyDTO, + startGame: () -> Unit, + leaveLobby: () -> Unit, + disbandLobby: () -> Unit, + addBot: (String) -> Unit, +) { + div { + css { + position = Position.absolute + bottom = 2.rem + left = 50.pct + transform = translate((-50).pct) - private fun RBuilder.actionButtons(currentPlayer: PlayerDTO, currentGame: LobbyDTO) { - styledDiv { - css { - position = Position.absolute - bottom = 2.rem - left = 50.pct - transform { translate((-50).pct) } - - width = 70.pct - display = Display.flex - justifyContent = JustifyContent.spaceAround + width = 70.pct + display = Display.flex + justifyContent = JustifyContent.spaceAround + } + if (currentPlayer.isGameOwner) { + BpButtonGroup { + leaveButton(leaveLobby) + disbandButton(disbandLobby) } - if (currentPlayer.isGameOwner) { - bpButtonGroup { - leaveButton() - disbandButton() - } - bpButtonGroup { - addBotButton(currentGame) - startButton(currentGame, currentPlayer) - } - } else { - leaveButton() + BpButtonGroup { + addBotButton(currentGame, addBot) + startButton(currentGame.startability(currentPlayer.username), startGame) } + } else { + leaveButton(leaveLobby) } } +} - private fun RBuilder.startButton(currentGame: LobbyDTO, currentPlayer: PlayerDTO) { - val startability = currentGame.startability(currentPlayer.username) - bpButton( - large = true, - intent = Intent.PRIMARY, - icon = IconNames.PLAY, - title = startability.tooltip, - disabled = !startability.canDo, - onClick = { props.startGame() }, - ) { - +"START" - } +private fun ChildrenBuilder.startButton(startability: Actionability, startGame: () -> Unit) { + BpButton { + large = true + intent = Intent.PRIMARY + icon = IconNames.PLAY + title = startability.tooltip + disabled = !startability.canDo + onClick = { startGame() } + + +"START" } +} - private fun RBuilder.setupPanel(currentGame: LobbyDTO) { - styledDiv { - css { - +LobbyStyles.setupPanel - } - bpCard(Elevation.TWO, className = ClassName(Classes.DARK)) { - styledH2 { - css { - margin(top = 0.px) - } - +"Game setup" - } - bpDivider() - h3 { - +"Players" - } - reorderPlayersButton(currentGame) - h3 { - +"Wonders" +private fun ChildrenBuilder.setupPanel( + currentGame: LobbyDTO, + reorderPlayers: (usernames: List<String>) -> Unit, + reassignWonders: (wonders: List<AssignedWonder>) -> Unit, +) { + div { + className = LobbyStyles.setupPanel + + BpCard { + elevation = Elevation.TWO + className = ClassName(Classes.DARK) + + h2 { + css { + marginTop = 0.px } - randomizeWondersButton(currentGame) - wonderSideSelectionGroup(currentGame) + +"Game setup" + } + BpDivider() + h3 { + +"Players" + } + reorderPlayersButton(currentGame, reorderPlayers) + h3 { + +"Wonders" + } + WonderSettingsGroup { + this.currentGame = currentGame + this.reassignWonders = reassignWonders } } } +} - private fun RBuilder.addBotButton(currentGame: LobbyDTO) { - bpButton( - large = true, - icon = IconNames.PLUS, - rightIcon = IconNames.DESKTOP, - intent = Intent.PRIMARY, - title = if (currentGame.maxPlayersReached) "Max players reached" else "Add a bot to this game", - disabled = currentGame.maxPlayersReached, - onClick = { addBot(currentGame) }, - ) +private fun ChildrenBuilder.addBotButton(currentGame: LobbyDTO, addBot: (String) -> Unit) { + BpButton { + large = true + icon = IconNames.PLUS + rightIcon = IconNames.DESKTOP + intent = Intent.PRIMARY + title = if (currentGame.maxPlayersReached) "Max players reached" else "Add a bot to this game" + disabled = currentGame.maxPlayersReached + onClick = { addBot(randomBotNameUnusedIn(currentGame)) } } +} - private fun addBot(currentGame: LobbyDTO) { - val availableBotNames = BOT_NAMES.filter { name -> - currentGame.players.all { it.displayName != name } - } - props.addBot(availableBotNames.random()) +private fun randomBotNameUnusedIn(currentGame: LobbyDTO): String { + val availableBotNames = BOT_NAMES.filter { name -> + currentGame.players.none { it.displayName == name } } + return availableBotNames.random() +} - private fun RBuilder.reorderPlayersButton(currentGame: LobbyDTO) { - bpButton( - icon = IconNames.RANDOM, - rightIcon = IconNames.PEOPLE, - title = "Re-order players randomly", - onClick = { reorderPlayers(currentGame) }, - ) { - +"Reorder players" - } - } +private fun ChildrenBuilder.reorderPlayersButton(currentGame: LobbyDTO, reorderPlayers: (usernames: List<String>) -> Unit) { + BpButton { + icon = IconNames.RANDOM + rightIcon = IconNames.PEOPLE + title = "Re-order players randomly" + onClick = { reorderPlayers(currentGame.players.map { it.username }.shuffled()) } - private fun reorderPlayers(currentGame: LobbyDTO) { - props.reorderPlayers(currentGame.players.map { it.username }.shuffled()) + +"Reorder players" } +} - private fun RBuilder.randomizeWondersButton(currentGame: LobbyDTO) { - bpButton( - icon = IconNames.RANDOM, - title = "Re-assign wonders to players randomly", - onClick = { randomizeWonders(currentGame) }, - ) { - +"Randomize wonders" - } +private external interface WonderSettingsGroupProps : Props { + var currentGame: LobbyDTO + var reassignWonders: (List<AssignedWonder>) -> Unit +} + +private val WonderSettingsGroup = FC<WonderSettingsGroupProps> { props -> + val reassignWonders = props.reassignWonders + + BpButton { + icon = IconNames.RANDOM + title = "Re-assign wonders to players randomly" + onClick = { reassignWonders(randomWonderAssignments(props.currentGame)) } + + +"Randomize wonders" + } + h4 { + +"Select wonder sides:" } + BpButtonGroup { + BpButton { + icon = IconNames.RANDOM + title = "Re-roll wonder sides randomly" + onClick = { reassignWonders(assignedWondersWithRandomSides(props.currentGame)) } + } + BpButton { + title = "Choose side A for everyone" + onClick = { reassignWonders(assignedWondersWithForcedSide(props.currentGame, WonderSide.A)) } - private fun RBuilder.wonderSideSelectionGroup(currentGame: LobbyDTO) { - h4 { - +"Select wonder sides:" + +"A" } - bpButtonGroup { - bpButton( - icon = IconNames.RANDOM, - title = "Re-roll wonder sides randomly", - onClick = { randomizeWonderSides(currentGame) }, - ) - bpButton( - title = "Choose side A for everyone", - onClick = { setWonderSides(currentGame, WonderSide.A) }, - ) { - +"A" - } - bpButton( - title = "Choose side B for everyone", - onClick = { setWonderSides(currentGame, WonderSide.B) }, - ) { - +"B" - } + BpButton { + title = "Choose side B for everyone" + onClick = { reassignWonders(assignedWondersWithForcedSide(props.currentGame, WonderSide.B)) } + + +"B" } } +} - private fun randomizeWonders(currentGame: LobbyDTO) { - props.reassignWonders(currentGame.allWonders.deal(currentGame.players.size)) - } +private fun randomWonderAssignments(currentGame: LobbyDTO): List<AssignedWonder> = + currentGame.allWonders.deal(currentGame.players.size) - private fun randomizeWonderSides(currentGame: LobbyDTO) { - props.reassignWonders(currentGame.players.map { currentGame.findWonder(it.wonder.name).withRandomSide() }) - } +private fun assignedWondersWithForcedSide( + currentGame: LobbyDTO, + side: WonderSide +) = currentGame.players.map { currentGame.findWonder(it.wonder.name).withSide(side) } - private fun setWonderSides(currentGame: LobbyDTO, side: WonderSide) { - props.reassignWonders(currentGame.players.map { currentGame.findWonder(it.wonder.name).withSide(side) }) - } +private fun assignedWondersWithRandomSides(currentGame: LobbyDTO) = + currentGame.players.map { currentGame.findWonder(it.wonder.name) }.map { it.withRandomSide() } - private fun RBuilder.leaveButton() { - bpButton( - large = true, - intent = Intent.WARNING, - icon = "arrow-left", - title = "Leave the lobby and go back to the game browser", - onClick = { props.leaveLobby() }, - ) { - +"LEAVE" - } - } +private fun ChildrenBuilder.leaveButton(leaveLobby: () -> Unit) { + BpButton { + large = true + intent = Intent.WARNING + icon = "arrow-left" + title = "Leave the lobby and go back to the game browser" + onClick = { leaveLobby() } - private fun RBuilder.disbandButton() { - bpButton( - large = true, - intent = Intent.DANGER, - icon = IconNames.DELETE, - title = "Disband the group and go back to the game browser", - onClick = { props.disbandLobby() }, - ) { - +"DISBAND" - } + +"LEAVE" } } -fun RBuilder.lobby() = lobby {} - -private val lobby = connectStateAndDispatch<LobbyStateProps, LobbyDispatchProps, LobbyProps>( - clazz = LobbyPresenter::class, - mapStateToProps = { state, _ -> - currentGame = state.currentLobby - currentPlayer = state.currentPlayer - }, - mapDispatchToProps = { dispatch, _ -> - startGame = { dispatch(RequestStartGame()) } - addBot = { name -> dispatch(RequestAddBot(name)) } - leaveLobby = { dispatch(RequestLeaveLobby()) } - disbandLobby = { dispatch(RequestDisbandLobby()) } - reorderPlayers = { orderedPlayers -> dispatch(RequestReorderPlayers(orderedPlayers)) } - reassignWonders = { wonders -> dispatch(RequestReassignWonders(wonders)) } - }, -) +private fun ChildrenBuilder.disbandButton(disbandLobby: () -> Unit) { + BpButton { + large = true + intent = Intent.DANGER + icon = IconNames.DELETE + title = "Disband the group and go back to the game browser" + onClick = { disbandLobby() } + + +"DISBAND" + } +} diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/LobbyStyles.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/LobbyStyles.kt index bbfc491f..fe20ac86 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/LobbyStyles.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/LobbyStyles.kt @@ -1,17 +1,17 @@ package org.luxons.sevenwonders.ui.components.lobby -import kotlinx.css.* -import org.luxons.sevenwonders.ui.components.GlobalStyles -import styled.StyleSheet +import csstype.* +import emotion.css.* +import org.luxons.sevenwonders.ui.components.* -object LobbyStyles : StyleSheet("LobbyStyles", isStatic = true) { +object LobbyStyles { - val contentContainer by css { - margin(horizontal = LinearDimension.auto) + val contentContainer = ClassName { + margin = Margin(vertical = 0.px, horizontal = Auto.auto) maxWidth = GlobalStyles.preGameWidth } - val setupPanel by css { + val setupPanel = ClassName { position = Position.fixed top = 2.rem right = 1.rem diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialList.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialList.kt index 8e99c23d..e9853588 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialList.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialList.kt @@ -1,19 +1,16 @@ package org.luxons.sevenwonders.ui.components.lobby -import kotlinx.css.* -import kotlinx.css.properties.* -import kotlinx.html.DIV -import org.luxons.sevenwonders.ui.components.GlobalStyles -import react.RBuilder -import react.ReactElement -import react.dom.* -import styled.StyledDOMBuilder -import styled.css -import styled.styledDiv -import styled.styledLi -import styled.styledUl +import csstype.* +import emotion.react.* +import org.luxons.sevenwonders.ui.components.* +import react.* +import react.dom.html.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.li +import react.dom.html.ReactHTML.ul +import web.html.* -fun <T> RBuilder.radialList( +fun <T> ChildrenBuilder.radialList( items: List<T>, centerElement: ReactElement<*>, renderItem: (T) -> ReactElement<*>, @@ -21,15 +18,14 @@ fun <T> RBuilder.radialList( itemWidth: Int, itemHeight: Int, options: RadialConfig = RadialConfig(), - block: StyledDOMBuilder<DIV>.() -> Unit = {}, + block: HTMLAttributes<HTMLDivElement>.() -> Unit = {}, ) { val containerWidth = options.diameter + itemWidth val containerHeight = options.diameter + itemHeight - styledDiv { - css { + div { + css(GlobalStyles.fixedCenter) { zeroMargins() - +GlobalStyles.fixedCenter width = containerWidth.px height = containerHeight.px } @@ -39,18 +35,22 @@ fun <T> RBuilder.radialList( } } -private fun <T> RBuilder.radialListItems( +private fun <T> ChildrenBuilder.radialListItems( items: List<T>, renderItem: (T) -> ReactElement<*>, getKey: (T) -> String, radialConfig: RadialConfig, ) { val offsets = offsetsFromCenter(items.size, radialConfig) - styledUl { + ul { css { zeroMargins() - transition(property = "all", duration = 500.ms, timing = Timing.easeInOut) - zIndex = 1 + transition = Transition( + property = TransitionProperty.all, + duration = 500.ms, + timingFunction = TransitionTimingFunction.easeInOut, + ) + zIndex = integer(1) width = radialConfig.diameter.px height = radialConfig.diameter.px absoluteCenter() @@ -67,52 +67,50 @@ private fun <T> RBuilder.radialListItems( } } -private fun RBuilder.radialListItem(item: ReactElement<*>, key: String, offset: CartesianCoords) { - styledLi { +private fun ChildrenBuilder.radialListItem(item: ReactElement<*>, key: String, offset: CartesianCoords) { + li { css { display = Display.block position = Position.absolute top = 50.pct left = 50.pct zeroMargins() - listStyleType = ListStyleType.unset - transition("all", 500.ms, Timing.easeInOut) - zIndex = 1 - transform { - translate(offset.x.px, offset.y.px) - translate((-50).pct, (-50).pct) - } - } - attrs { - this.key = key + listStyleType = Globals.unset + transition = Transition( + property = TransitionProperty.all, + duration = 500.ms, + timingFunction = TransitionTimingFunction.easeInOut, + ) + zIndex = integer(1) + transform = translate(offset.x.px - 50.pct, offset.y.px - 50.pct) } + this.key = key + child(item) } } -private fun RBuilder.radialListCenter(centerElement: ReactElement<*>?) { +private fun ChildrenBuilder.radialListCenter(centerElement: ReactElement<*>?) { if (centerElement == null) { return } - styledDiv { + div { css { - zIndex = 0 + zIndex = integer(0) absoluteCenter() } child(centerElement) } } -private fun CssBuilder.absoluteCenter() { +private fun PropertiesBuilder.absoluteCenter() { position = Position.absolute left = 50.pct top = 50.pct - transform { - translate((-50).pct, (-50).pct) - } + transform = translate((-50).pct, (-50).pct) } -private fun CssBuilder.zeroMargins() { - margin(all = 0.px) - padding(all = 0.px) +private fun PropertiesBuilder.zeroMargins() { + margin = Margin(vertical = 0.px, horizontal = 0.px) + padding = Padding(vertical = 0.px, horizontal = 0.px) } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialPlayerList.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialPlayerList.kt index 2084b7c0..290dd83f 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialPlayerList.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/RadialPlayerList.kt @@ -1,39 +1,36 @@ package org.luxons.sevenwonders.ui.components.lobby -import blueprintjs.core.bpIcon -import blueprintjs.core.bpTag +import blueprintjs.core.* +import blueprintjs.icons.* import csstype.* -import kotlinx.css.* -import kotlinx.css.Color -import kotlinx.css.Display -import kotlinx.css.FlexDirection -import kotlinx.css.px -import kotlinx.css.rem -import kotlinx.html.DIV -import org.luxons.sevenwonders.model.api.PlayerDTO +import emotion.react.* +import org.luxons.sevenwonders.model.api.* import org.luxons.sevenwonders.model.api.actions.Icon -import org.luxons.sevenwonders.model.wonders.WonderSide -import react.RBuilder -import react.ReactElement -import react.buildElement -import styled.* +import org.luxons.sevenwonders.model.wonders.* +import org.luxons.sevenwonders.ui.utils.* +import react.* +import react.dom.html.* +import react.dom.html.ReactHTML.div +import react.dom.html.ReactHTML.span +import web.html.* -fun RBuilder.radialPlayerList( +fun ChildrenBuilder.radialPlayerList( players: List<PlayerDTO>, currentPlayer: PlayerDTO, - block: StyledDOMBuilder<DIV>.() -> Unit = {}, + block: HTMLAttributes<HTMLDivElement>.() -> Unit = {}, ) { val playerItems = players // .map { PlayerItem.Player(it) } .growWithPlaceholders(targetSize = 3) .withUserFirst(currentPlayer) - val tableImg = buildElement { lobbyWoodenTable(diameter = 200.px, borderSize = 15.px) } - radialList( items = playerItems, - centerElement = tableImg, - renderItem = { buildElement { playerElement(it) } }, + centerElement = LobbyWoodenTable.create { + diameter = 200.px + borderSize = 15.px + }, + renderItem = { PlayerElement.create { playerItem = it } }, getKey = { it.key }, itemWidth = 120, itemHeight = 100, @@ -60,74 +57,79 @@ private fun List<PlayerItem>.withUserFirst(me: PlayerDTO): List<PlayerItem> { private sealed class PlayerItem { abstract val key: String abstract val playerText: String - abstract val opacity: Double + abstract val opacity: Opacity abstract val icon: ReactElement<*> data class Player(val player: PlayerDTO) : PlayerItem() { override val key = player.username override val playerText = player.displayName - override val opacity = 1.0 - override val icon = buildElement { - userIcon( - icon = player.icon ?: when { - player.isGameOwner -> Icon("badge") - else -> Icon("user") - }, - title = if (player.isGameOwner) "Game owner" else null, - ) - } + override val opacity = number(1.0) + override val icon = createUserIcon( + icon = player.icon ?: when { + player.isGameOwner -> Icon(IconNames.BADGE) + else -> Icon(IconNames.USER) + }, + title = if (player.isGameOwner) "Game owner" else null, + ) } data class Placeholder(val index: Int) : PlayerItem() { override val key = "player-placeholder-$index" override val playerText = "?" - override val opacity = 0.4 - override val icon = buildElement { - userIcon( - icon = Icon("user"), - title = "Waiting for player...", - ) - } + override val opacity = number(0.4) + override val icon = createUserIcon( + icon = Icon(IconNames.USER), + title = "Waiting for player...", + ) } } -private fun RBuilder.userIcon(icon: Icon, title: String?) = bpIcon( - name = icon.name, - size = 50, - title = title, -) +private fun createUserIcon(icon: Icon, title: String?) = BpIcon.create { + this.icon = icon.name + this.size = 50 + this.title = title +} -private fun RBuilder.playerElement(playerItem: PlayerItem) { - styledDiv { +private external interface PlayerElementProps : Props { + var playerItem: PlayerItem +} + +private val PlayerElement = FC<PlayerElementProps>(displayName = "PlayerElement") { props -> + val playerItem = props.playerItem + div { css { display = Display.flex flexDirection = FlexDirection.column - alignItems = Align.center + alignItems = AlignItems.center opacity = playerItem.opacity } child(playerItem.icon) - styledSpan { + span { css { fontSize = if (playerItem is PlayerItem.Placeholder) 1.5.rem else 0.9.rem } +playerItem.playerText } if (playerItem is PlayerItem.Player) { - styledDiv { + div { val wonder = playerItem.player.wonder + css { marginTop = 0.3.rem - // this is to overcome ".bp4-dark .bp4-tag" on the nested bpTag children(".wonder-tag") { color = Color("#f5f8fa") // blueprintjs dark theme color (removed by .bp4-tag) backgroundColor = when (wonder.side) { - WonderSide.A -> Color.seaGreen - WonderSide.B -> Color.darkRed + WonderSide.A -> NamedColor.seagreen + WonderSide.B -> NamedColor.darkred } } } - bpTag(round = true, className = ClassName("wonder-tag")) { + + BpTag { + round = true + className = ClassName("wonder-tag") + +"${wonder.name} ${wonder.side}" } } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Table.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Table.kt index b9c799e2..2fa3b246 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Table.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/components/lobby/Table.kt @@ -1,81 +1,96 @@ package org.luxons.sevenwonders.ui.components.lobby -import kotlinx.css.* -import kotlinx.css.properties.* -import kotlinx.html.DIV -import react.RBuilder -import styled.StyledDOMBuilder -import styled.animation -import styled.css -import styled.styledDiv +import csstype.* +import emotion.css.* +import emotion.react.* +import emotion.styled.* +import org.luxons.sevenwonders.ui.utils.* +import react.* +import react.dom.html.ReactHTML.div -private const val FIRE_REFLECTION_COLOR = "#b85e00" +private val FIRE_REFLECTION_COLOR = Color("#b85e00") + +private external interface CircleProps : PropsWithChildren, PropsWithClassName { + var diameter: Length +} + +private val Circle = FC<CircleProps>("Circle") { props -> + div { + css(props.className) { + width = props.diameter + height = props.diameter + borderRadius = 50.pct + } + child(props.children) + } +} + +private val OverlayCircle = Circle.styled { _, _ -> + position = Position.absolute + top = 0.px + left = 0.px +} + +external interface LobbyWoodenTableProps : Props { + var diameter: Length + var borderSize: Length +} + +val LobbyWoodenTable = FC<LobbyWoodenTableProps>("LobbyWoodenTable") { props -> + Circle { + diameter = props.diameter -fun RBuilder.lobbyWoodenTable(diameter: LinearDimension, borderSize: LinearDimension = 20.px) { - circle(diameter) { css { backgroundColor = Color("#3d1e0e") } - circle(diameter = diameter - borderSize) { + + Circle { + diameter = props.diameter - props.borderSize css { position = Position.absolute - top = borderSize / 2 - left = borderSize / 2 - background = "linear-gradient(45deg, #88541e, #995645, #52251a)" + top = props.borderSize / 2 + left = props.borderSize / 2 + background = linearGradient(45.deg, Color("#88541e"), Color("#995645"), Color("#52251a")) } } // flame reflection coming from bottom-right - overlayCircle(diameter) { + OverlayCircle { + diameter = props.diameter + css { - background = "linear-gradient(-45deg, $FIRE_REFLECTION_COLOR 10%, transparent 50%)" + background = + linearGradient((-45).deg, stop(FIRE_REFLECTION_COLOR, 10.pct), stop(NamedColor.transparent, 50.pct)) opacityAnimation(duration = 1.3.s) } } // flame reflection coming from bottom-left - overlayCircle(diameter) { + OverlayCircle { + diameter = props.diameter + css { - background = "linear-gradient(45deg, $FIRE_REFLECTION_COLOR 20%, transparent 40%)" + background = + linearGradient(45.deg, stop(FIRE_REFLECTION_COLOR, 20.pct), stop(NamedColor.transparent, 40.pct)) opacityAnimation(duration = 0.8.s) } } } } -private fun RBuilder.overlayCircle(diameter: LinearDimension, block: StyledDOMBuilder<DIV>.() -> Unit) { - circle(diameter) { - css { - position = Position.absolute - top = 0.px - left = 0.px - } - block() - } -} - -private fun RBuilder.circle(diameter: LinearDimension, block: StyledDOMBuilder<DIV>.() -> Unit) { - styledDiv { - css { - width = diameter - height = diameter - borderRadius = 50.pct - } - block() - } -} - -private fun CssBuilder.opacityAnimation(duration: Time) { - animation( - duration = duration, - direction = AnimationDirection.alternate, - iterationCount = IterationCount.infinite, - timing = cubicBezier(0.4, 0.4, 0.4, 2.0) - ) { +private fun PropertiesBuilder.opacityAnimation(duration: Time) { + val keyframes = keyframes { from { - opacity = 0.0 + opacity = number(0.0) } to { - opacity = 0.35 + opacity = number(0.35) } } + animation = Animation( + name = keyframes, + duration = duration, + timingFunction = cubicBezier(0.4, 0.4, 0.4, 2.0), + ) + animationDirection = AnimationDirection.alternate + animationIterationCount = AnimationIterationCount.infinite } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Utils.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Utils.kt index d5b3fffd..eb182dc7 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Utils.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/redux/Utils.kt @@ -1,43 +1,31 @@ package org.luxons.sevenwonders.ui.redux import react.* -import react.redux.rConnect -import redux.RAction -import redux.WrapperAction -import kotlin.reflect.KClass +import react.redux.* +import redux.* +import kotlin.reflect.* -fun <DP : PropsWithChildren> connectDispatch( - clazz: KClass<out RComponent<DP, out State>>, - mapDispatchToProps: DP.((RAction) -> WrapperAction, PropsWithChildren) -> Unit, -): ComponentClass<PropsWithChildren> { - val connect = rConnect(mapDispatchToProps = mapDispatchToProps) - return connect.invoke(clazz.js.unsafeCast<ComponentClass<DP>>()) -} - -fun <SP : PropsWithChildren> connectState( - clazz: KClass<out RComponent<SP, out State>>, - mapStateToProps: SP.(SwState, PropsWithChildren) -> Unit, -): ComponentClass<PropsWithChildren> { - val connect = rConnect(mapStateToProps = mapStateToProps) - return connect.invoke(clazz.js.unsafeCast<ComponentClass<SP>>()) -} +fun <R> useSwSelector(selector: (SwState) -> R) = useSelector(selector) +fun useSwDispatch() = useDispatch<RAction, WrapperAction>() -fun <SP : PropsWithChildren, OP : PropsWithChildren> connectStateWithOwnProps( - clazz: KClass<out RComponent<SP, out State>>, - mapStateToProps: SP.(SwState, OP) -> Unit, -): ComponentClass<OP> { - val connect = rConnect(mapStateToProps = mapStateToProps) - return connect.invoke(clazz.js.unsafeCast<ComponentClass<SP>>()) -} +fun <SP : Props, DP : Props, P : Props> connectStateAndDispatch( + clazz: KClass<out Component<P, out State>>, + mapStateToProps: SP.(SwState, Props) -> Unit, + mapDispatchToProps: DP.((RAction) -> WrapperAction, Props) -> Unit, +): ComponentClass<Props> = connectStateAndDispatch( + component = clazz.react, + mapStateToProps = mapStateToProps, + mapDispatchToProps = mapDispatchToProps, +) -fun <SP : PropsWithChildren, DP : PropsWithChildren, P : PropsWithChildren> connectStateAndDispatch( - clazz: KClass<out RComponent<P, out State>>, - mapStateToProps: SP.(SwState, PropsWithChildren) -> Unit, - mapDispatchToProps: DP.((RAction) -> WrapperAction, PropsWithChildren) -> Unit, -): ComponentClass<PropsWithChildren> { - val connect = rConnect<SwState, RAction, WrapperAction, PropsWithChildren, SP, DP, P>( +fun <SP : Props, DP : Props, P : Props> connectStateAndDispatch( + component: ComponentClass<P>, + mapStateToProps: SP.(SwState, Props) -> Unit, + mapDispatchToProps: DP.((RAction) -> WrapperAction, Props) -> Unit, +): ComponentClass<Props> { + val connect = rConnect<SwState, RAction, WrapperAction, Props, SP, DP, P>( mapStateToProps = mapStateToProps, mapDispatchToProps = mapDispatchToProps, ) - return connect.invoke(clazz.js.unsafeCast<ComponentClass<P>>()) + return connect.invoke(component) } diff --git a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/utils/StyleUtils.kt b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/utils/StyleUtils.kt index d438c259..5dd274de 100644 --- a/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/utils/StyleUtils.kt +++ b/sw-ui/src/main/kotlin/org/luxons/sevenwonders/ui/utils/StyleUtils.kt @@ -1,10 +1,42 @@ package org.luxons.sevenwonders.ui.utils -import csstype.ClassName -import kotlinx.css.* -import styled.* -import kotlin.reflect.* +import csstype.* +import js.core.* +import react.dom.html.* -fun <T : StyleSheet> T.getTypedClassName(getClass: (T) -> KProperty0<RuleSet>): ClassName { - return ClassName(getClassName(getClass)) +/** + * The cubic-bezier() function defines a Cubic Bezier curve. + * + * A Cubic Bezier curve is defined by four points P0, P1, P2, and P3. P0 and P3 are the start and the end of the curve + * and, in CSS these points are fixed as the coordinates are ratios. P0 is (0, 0) and represents the initial time and + * the initial state, P3 is (1, 1) and represents the final time and the final state. + * + * The x coordinates provided here must be between 0 and 1 (the bezier curve points should be between the start time + * and end time, giving other values would make the curve go back in the past or further into the future). + * + * The y coordinates may be any value: the intermediate states can be below or above the start (0) or end (1) values. + */ +fun cubicBezier(x1: Double, y1: Double, x2: Double, y2: Double) = + "cubic-bezier($x1, $y1, $x2, $y2)".unsafeCast<AnimationTimingFunction>() + +fun Margin(all: AutoLength) = Margin(vertical = all, horizontal = all) + +fun Padding(all: Length) = Padding(vertical = all, horizontal = all) + +// this should work because NamedColor is ultimately a hex string in JS, not the actual name +fun NamedColor.withAlpha(alpha: Double) = "$this${(alpha * 255).toInt().toString(16)}".unsafeCast<BackgroundColor>() + +operator fun FilterFunction.plus(other: FilterFunction) = "$this $other".unsafeCast<FilterFunction>() + +fun PropertiesBuilder.ancestorHover(selector: String, block: PropertiesBuilder.() -> Unit) = + "$selector:hover &".invoke(block) + +fun PropertiesBuilder.children(selector: String, block: PropertiesBuilder.() -> Unit) = + "& > $selector".invoke(block) + +fun PropertiesBuilder.descendants(selector: String, block: PropertiesBuilder.() -> Unit) = + "& $selector".invoke(block) + +fun HTMLAttributes<*>.inlineStyles(block: PropertiesBuilder.() -> Unit) { + style = jso(block) } |