Compare commits
No commits in common. "b29d1a1ef1d8d158c66346bc5976c9994af351f6" and "62a4b265c285953fa7323897f355833d34577be4" have entirely different histories.
b29d1a1ef1
...
62a4b265c2
@ -1,3 +1,2 @@
|
|||||||
runner.dialect = scala3
|
runner.dialect = scala3
|
||||||
version = 3.7.3
|
version = 3.7.3
|
||||||
maxColumn = 100
|
|
||||||
|
@ -1,115 +1,74 @@
|
|||||||
package industries.sunshine.planningpoker
|
package industries.sunshine.planningpoker
|
||||||
|
|
||||||
import cats.effect._
|
import cats.effect._
|
||||||
import cats.syntax.all._
|
|
||||||
import cats.data.Kleisli
|
import cats.data.Kleisli
|
||||||
import cats.data.OptionT
|
import cats.data.OptionT
|
||||||
import org.http4s.Request
|
import org.http4s.Request
|
||||||
import industries.sunshine.planningpoker.common.Models.PlayerID
|
import industries.sunshine.planningpoker.common.Models.PlayerID
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import industries.sunshine.planningpoker.common.Models.{RoomID, Room}
|
import industries.sunshine.planningpoker.common.Models.RoomID
|
||||||
import org.http4s.ResponseCookie
|
|
||||||
import cats.data.EitherT
|
|
||||||
import scala.util.Random
|
|
||||||
|
|
||||||
trait Auth[F[_]] {
|
trait Auth {
|
||||||
|
|
||||||
/** for middleware that converts sessionId into PlayerId
|
/** for middleware that converts sessionId into PlayerId
|
||||||
*/
|
*/
|
||||||
def authUser: Kleisli[[A] =>> cats.data.OptionT[F, A], Request[F], (PlayerID, RoomID)]
|
def authUser: Kleisli[[A] =>> cats.data.OptionT[cats.effect.IO, A], Request[
|
||||||
|
cats.effect.IO
|
||||||
|
], (PlayerID, RoomID)]
|
||||||
|
|
||||||
/** Get sessionId for accessing a room
|
/** Get sessionId for accessing a room
|
||||||
* @param roomPassword
|
* @param roomPassword
|
||||||
* \- requirement to get access to the room
|
* \- requirement to get access to the room
|
||||||
*
|
|
||||||
* check that room exists, password is valid call to add user to the players create session
|
|
||||||
* mapping and return cookie
|
|
||||||
*/
|
*/
|
||||||
def joinRoom(
|
def accessRoom(
|
||||||
roomName: String,
|
roomName: String,
|
||||||
roomPassword: String,
|
roomPassword: String,
|
||||||
nickName: String,
|
nickName: String
|
||||||
nickPassword: String
|
): IO[Either[Unit, Long]]
|
||||||
): F[Either[String, ResponseCookie]]
|
|
||||||
|
|
||||||
def deleteSession(
|
def leaveRoom(
|
||||||
sessionId: Long
|
sessionId: Long
|
||||||
): F[Unit]
|
): IO[Unit]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Auth {
|
object Auth {
|
||||||
def pureBadStub = new Auth[IO] {
|
def pureBadStub = new Auth {
|
||||||
override def authUser =
|
override def authUser =
|
||||||
Kleisli((req: Request[IO]) =>
|
Kleisli((req: Request[IO]) =>
|
||||||
OptionT.liftF(
|
OptionT.liftF(IO(println(s"authUser: $req")) >> IO(PlayerID(14) -> RoomID(101)))
|
||||||
IO(println(s"authUser: $req")) >> IO(
|
|
||||||
PlayerID(14) -> RoomID("testroom")
|
|
||||||
)
|
)
|
||||||
)
|
override def accessRoom(
|
||||||
)
|
|
||||||
override def joinRoom(
|
|
||||||
roomName: String,
|
roomName: String,
|
||||||
roomPassword: String,
|
roomPassword: String,
|
||||||
nickName: String,
|
nickName: String
|
||||||
nickPassword: String
|
|
||||||
) =
|
) =
|
||||||
IO(
|
IO(println(s"> access room for $roomName $roomPassword $nickName, to return stub 111")) >> IO.pure(Right(111L))
|
||||||
println(
|
|
||||||
s"> access room for $roomName $roomPassword $nickName, to return stub 111"
|
|
||||||
)
|
|
||||||
) >> IO.pure(Right(ResponseCookie("authcookie", "1")))
|
|
||||||
|
|
||||||
override def deleteSession(sessionId: Long): IO[Unit] =
|
override def leaveRoom(sessionId: Long): IO[Unit] =
|
||||||
IO(s"got request to leave for $sessionId")
|
IO(s"got request to leave for $sessionId")
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionsMap = Map[Long, (RoomID, PlayerID)]
|
type SessionsMap = Map[Long, (RoomID, PlayerID)]
|
||||||
|
val sessionsRef = Ref.of[IO, SessionsMap](Map.empty)
|
||||||
|
val roomPasswordsRef = Ref.of[IO, Map[Long, String]](Map.empty)
|
||||||
|
|
||||||
class SimpleAuth[F[_]: Sync](
|
def apply(): IO[Auth] =
|
||||||
sessions: Ref[F, SessionsMap],
|
for {
|
||||||
roomService: RoomService[F]
|
store <- sessionsRef
|
||||||
) extends Auth[F] {
|
roomPasswords <- roomPasswordsRef
|
||||||
|
} yield new Auth {
|
||||||
val authcookie = "authcookie"
|
override def authUser = Kleisli { (req: Request[IO]) =>
|
||||||
|
{
|
||||||
override def joinRoom(
|
???
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override def accessRoom(
|
||||||
roomName: String,
|
roomName: String,
|
||||||
roomPassword: String,
|
roomPassword: String,
|
||||||
nickName: String,
|
nickName: String
|
||||||
nickPassword: String
|
): IO[Either[Unit, Long]] = ???
|
||||||
): F[Either[String, ResponseCookie]] = {
|
|
||||||
// TODO check for existing session for same room
|
|
||||||
// and do i want to logout if existing session for another room? ugh
|
|
||||||
val roomId = RoomID(roomName)
|
|
||||||
val result = for {
|
|
||||||
playerId <- EitherT(
|
|
||||||
roomService.joinRoom(roomId, nickName, nickPassword, roomPassword)
|
|
||||||
)
|
|
||||||
.leftMap(_.toString())
|
|
||||||
newSessionId = Random.nextLong()
|
|
||||||
_ <- EitherT.liftF(sessions.update(_.updated(newSessionId, (roomId, playerId))))
|
|
||||||
} yield ResponseCookie(name = authcookie, content = newSessionId.toString(), secure = true)
|
|
||||||
|
|
||||||
result.value
|
override def leaveRoom(sessionId: Long): IO[Unit] = ???
|
||||||
}
|
|
||||||
|
|
||||||
override def authUser
|
|
||||||
: Kleisli[[A] =>> cats.data.OptionT[F, A], Request[F], (PlayerID, RoomID)] = {
|
|
||||||
// check authcookie presence, exchange it for playerID ad roomID
|
|
||||||
???
|
|
||||||
}
|
|
||||||
|
|
||||||
override def deleteSession(sessionId: Long): F[Unit] = {
|
|
||||||
// i suppose leaving the room should just be authed route & method
|
|
||||||
???
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def make[F[_]: Sync](roomsService: RoomService[F]): F[Auth[F]] =
|
|
||||||
for {
|
|
||||||
sessionsMap <- Ref.of[F, SessionsMap](
|
|
||||||
Map(1L -> (RoomID("testroom"), PlayerID(1L)))
|
|
||||||
)
|
|
||||||
} yield new SimpleAuth(sessionsMap, roomsService)
|
|
||||||
}
|
|
||||||
|
@ -12,21 +12,17 @@ object BackendApp extends IOApp {
|
|||||||
val host = host"0.0.0.0"
|
val host = host"0.0.0.0"
|
||||||
val port = port"8080"
|
val port = port"8080"
|
||||||
|
|
||||||
val wiring = for {
|
val server = for {
|
||||||
roomService <- Resource.eval(RoomService.make[IO])
|
srv <- EmberServerBuilder
|
||||||
httpService = MyHttpService.create(Auth.pureBadStub)
|
|
||||||
server <- EmberServerBuilder
|
|
||||||
.default[IO]
|
.default[IO]
|
||||||
.withHost(host)
|
.withHost(host)
|
||||||
.withPort(port)
|
.withPort(port)
|
||||||
.withHttpWebSocketApp(httpService(_))
|
.withHttpWebSocketApp(MyHttpService.create(Auth.pureBadStub)(_))
|
||||||
.build
|
.build
|
||||||
} yield server
|
} yield srv
|
||||||
|
|
||||||
wiring.use(server =>
|
server.use(server =>
|
||||||
IO.delay(println(s"Server Has Started at ${server.address}")) >> IO.never
|
IO.delay(println(s"Server Has Started at ${server.address}")) >> IO.never.as(ExitCode.Success))
|
||||||
.as(ExitCode.Success)
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ import org.http4s.server.AuthMiddleware.apply
|
|||||||
import org.http4s.server.AuthMiddleware
|
import org.http4s.server.AuthMiddleware
|
||||||
|
|
||||||
object MyHttpService {
|
object MyHttpService {
|
||||||
def create(auth: Auth[IO])(
|
def create(auth: Auth)(
|
||||||
wsb: WebSocketBuilder[cats.effect.IO]
|
wsb: WebSocketBuilder[cats.effect.IO]
|
||||||
): HttpApp[cats.effect.IO] = {
|
): HttpApp[cats.effect.IO] = {
|
||||||
|
|
||||||
@ -26,9 +26,7 @@ object MyHttpService {
|
|||||||
val send: Stream[IO, WebSocketFrame] =
|
val send: Stream[IO, WebSocketFrame] =
|
||||||
Stream
|
Stream
|
||||||
.emits(TestModels.testChangesList)
|
.emits(TestModels.testChangesList)
|
||||||
.covary[IO]
|
.covary[IO].metered(1.second).map(state => WebSocketFrame.Text(state.asJson.noSpaces))
|
||||||
.metered(1.second)
|
|
||||||
.map(state => WebSocketFrame.Text(state.asJson.noSpaces))
|
|
||||||
|
|
||||||
val receive: Pipe[IO, WebSocketFrame, Unit] = _.evalMap {
|
val receive: Pipe[IO, WebSocketFrame, Unit] = _.evalMap {
|
||||||
case WebSocketFrame.Text(text, _) => Sync[IO].delay(println(text))
|
case WebSocketFrame.Text(text, _) => Sync[IO].delay(println(text))
|
||||||
@ -56,17 +54,20 @@ object MyHttpService {
|
|||||||
case req @ POST -> Root / "login" => {
|
case req @ POST -> Root / "login" => {
|
||||||
for {
|
for {
|
||||||
data <- req.as[Requests.LogIn]
|
data <- req.as[Requests.LogIn]
|
||||||
authResult <- auth.joinRoom(
|
authResult <- auth.accessRoom(
|
||||||
data.roomName,
|
data.roomName,
|
||||||
data.password,
|
data.password,
|
||||||
data.nickname,
|
data.nickname
|
||||||
data.nickPassword)
|
)
|
||||||
resp <- authResult match {
|
resp <- authResult match {
|
||||||
case Left(error) =>
|
case Left(error) =>
|
||||||
Forbidden(error)
|
Forbidden(error)
|
||||||
case Right(authCookie) => {
|
case Right(sessionId) => {
|
||||||
IO(println(s"> logging in ${data.nickname} to ${data.roomName}")) >>
|
Ok("Logged in!").map(
|
||||||
Ok().map(_.addCookie(authCookie))
|
_.addCookie(
|
||||||
|
ResponseCookie("authcookie", sessionId.toString())
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} yield resp
|
} yield resp
|
||||||
|
@ -1,96 +0,0 @@
|
|||||||
package industries.sunshine.planningpoker
|
|
||||||
|
|
||||||
import industries.sunshine.planningpoker.common.Models.*
|
|
||||||
import cats.effect.{Ref, Sync}
|
|
||||||
import cats.syntax.all._
|
|
||||||
import cats.data.EitherT
|
|
||||||
|
|
||||||
enum RoomError {
|
|
||||||
case RoomAlreadyExists(name: String)
|
|
||||||
case RoomMissing(name: String)
|
|
||||||
case RoomPassIncorrect
|
|
||||||
case NickPassIncorrect
|
|
||||||
}
|
|
||||||
|
|
||||||
trait RoomService[F[_]] {
|
|
||||||
def createRoom(newRoom: Room): F[Either[RoomError, Room]]
|
|
||||||
def updateRoom(room: Room): F[Unit]
|
|
||||||
def joinRoom(
|
|
||||||
id: RoomID,
|
|
||||||
nickName: String,
|
|
||||||
nickPassword: String,
|
|
||||||
roomPassword: String
|
|
||||||
): F[Either[RoomError, PlayerID]]
|
|
||||||
def deleteRoom(roomID: RoomID): F[Unit]
|
|
||||||
def getRoom(roomID: RoomID): F[Option[Room]]
|
|
||||||
}
|
|
||||||
|
|
||||||
class InMemoryRoomService[F[_]: Sync](stateRef: Ref[F, Map[RoomID, Room]]) extends RoomService[F] {
|
|
||||||
override def createRoom(newRoom: Room): F[Either[RoomError, Room]] = {
|
|
||||||
stateRef.modify { rooms =>
|
|
||||||
rooms.get(newRoom.id) match {
|
|
||||||
case Some(_) =>
|
|
||||||
rooms -> RoomError.RoomAlreadyExists(newRoom.id.name).asLeft[Room]
|
|
||||||
case None =>
|
|
||||||
(rooms.updated(newRoom.id, newRoom)) -> newRoom.asRight[RoomError]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override def updateRoom(room: Room): F[Unit] = stateRef.update { state =>
|
|
||||||
state.get(room.id).fold(state)(oldRoom => state.updated(room.id, room))
|
|
||||||
}
|
|
||||||
|
|
||||||
override def deleteRoom(roomID: RoomID): F[Unit] = stateRef.update(_.removed(roomID))
|
|
||||||
|
|
||||||
override def getRoom(roomID: RoomID): F[Option[Room]] = stateRef.get.map(_.get(roomID))
|
|
||||||
|
|
||||||
override def joinRoom(
|
|
||||||
id: RoomID,
|
|
||||||
nickName: String,
|
|
||||||
nickPassword: String,
|
|
||||||
roomPassword: String
|
|
||||||
): F[Either[RoomError, PlayerID]] = stateRef.modify { rooms =>
|
|
||||||
// need to cover cases:
|
|
||||||
// - player already present, then return as is, i guess
|
|
||||||
// - nick not known - add new player and new nick-password mapping
|
|
||||||
// - nick known - add new player
|
|
||||||
|
|
||||||
def addPlayer(room: Room): (PlayerID, Room) = {
|
|
||||||
room.players.find(_.name == nickName) match {
|
|
||||||
case Some(player) => player.id -> room
|
|
||||||
case None => // player is not present, but potentially was previously
|
|
||||||
val addingPlayer = Player.create(nickPassword)
|
|
||||||
val roomWithPlayer = room.copy(players = addingPlayer :: room.players)
|
|
||||||
room.playersPasswords.get(nickName) match {
|
|
||||||
case Some(_) => addingPlayer.id -> roomWithPlayer
|
|
||||||
case None =>
|
|
||||||
addingPlayer.id -> roomWithPlayer.copy(playersPasswords =
|
|
||||||
roomWithPlayer.playersPasswords.updated(nickName, nickPassword)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val joiningWithChecks = for {
|
|
||||||
room <- rooms.get(id).toRight(RoomError.RoomMissing(id.name))
|
|
||||||
_ <- Either.cond(room.password == roomPassword, (), RoomError.RoomPassIncorrect)
|
|
||||||
isNickPassCorrect = room.playersPasswords
|
|
||||||
.get(nickName)
|
|
||||||
.fold(true)(existingPass => existingPass == nickPassword)
|
|
||||||
_ <- Either.cond(
|
|
||||||
isNickPassCorrect,
|
|
||||||
(),
|
|
||||||
RoomError.NickPassIncorrect
|
|
||||||
)
|
|
||||||
(playerId, updatedRoom) = addPlayer(room)
|
|
||||||
} yield playerId
|
|
||||||
|
|
||||||
rooms -> joiningWithChecks
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
object RoomService {
|
|
||||||
def make[F[_]: Sync]: F[RoomService[F]] = {
|
|
||||||
Ref.of[F, Map[RoomID, Room]](Map.empty).map(new InMemoryRoomService[F](_))
|
|
||||||
}
|
|
||||||
}
|
|
@ -55,7 +55,7 @@ lazy val backend = project
|
|||||||
libraryDependencies += "co.fs2" %% "fs2-core" % "3.6.1",
|
libraryDependencies += "co.fs2" %% "fs2-core" % "3.6.1",
|
||||||
libraryDependencies += "org.typelevel" %% "cats-core" % "2.9.0",
|
libraryDependencies += "org.typelevel" %% "cats-core" % "2.9.0",
|
||||||
libraryDependencies += "org.typelevel" %% "cats-effect" % "3.4.9",
|
libraryDependencies += "org.typelevel" %% "cats-effect" % "3.4.9",
|
||||||
assembly / mainClass := Some("industries.sunshine.planningpoker.BackendApp")
|
assembly / mainClass := Some("industries.sunshine.planningpoker.BackendApp"),
|
||||||
)
|
)
|
||||||
.dependsOn(common.jvm)
|
.dependsOn(common.jvm)
|
||||||
|
|
||||||
@ -76,8 +76,6 @@ lazy val commonJS = common.js.settings(
|
|||||||
// scalaJS specific settings
|
// scalaJS specific settings
|
||||||
scalaJSLinkerConfig ~= {
|
scalaJSLinkerConfig ~= {
|
||||||
_.withModuleKind(ModuleKind.ESModule)
|
_.withModuleKind(ModuleKind.ESModule)
|
||||||
.withModuleSplitStyle(
|
.withModuleSplitStyle(ModuleSplitStyle.SmallModulesFor(List("industries.sunshine.planningpoker")))
|
||||||
ModuleSplitStyle.SmallModulesFor(List("industries.sunshine.planningpoker"))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -2,19 +2,14 @@ package industries.sunshine.planningpoker.common
|
|||||||
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import io.circe._
|
import io.circe._
|
||||||
import scala.util.Random
|
|
||||||
|
|
||||||
object Models {
|
object Models {
|
||||||
|
|
||||||
/** view of the single planning poker round
|
/** view of the single planning poker round
|
||||||
* @param players
|
* @param players - people who are currently playing
|
||||||
* \- people who are currently playing
|
* @param allowedCards- the cards values that can be used by players
|
||||||
* @param allowedCards-
|
* @param round - state of the selected cards of the players
|
||||||
* the cards values that can be used by players
|
* @param canCloseRound - whether current player has access to button to finish the round
|
||||||
* @param round
|
|
||||||
* \- state of the selected cards of the players
|
|
||||||
* @param canCloseRound
|
|
||||||
* \- whether current player has access to button to finish the round
|
|
||||||
*/
|
*/
|
||||||
final case class RoomStateView(
|
final case class RoomStateView(
|
||||||
players: List[Player],
|
players: List[Player],
|
||||||
@ -28,18 +23,15 @@ object Models {
|
|||||||
|
|
||||||
object RoomStateView {
|
object RoomStateView {
|
||||||
val empty = RoomStateView(
|
val empty = RoomStateView(
|
||||||
List.empty,
|
List.empty, PlayerID(0), List.empty, RoundState.Voting(None, List.empty), false
|
||||||
PlayerID(0),
|
|
||||||
List.empty,
|
|
||||||
RoundState.Voting(None, List.empty),
|
|
||||||
false
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
enum RoundState derives Codec.AsObject:
|
enum RoundState derives Codec.AsObject:
|
||||||
|
|
||||||
/** view state for round before votes are open player can know their vote and who of the other
|
/** view state for round before votes are open player can know their vote and
|
||||||
* players have voted
|
* who of the other players have voted
|
||||||
*/
|
*/
|
||||||
case Voting(
|
case Voting(
|
||||||
myCard: Option[String],
|
myCard: Option[String],
|
||||||
@ -54,50 +46,7 @@ object Models {
|
|||||||
final case class PlayerID(id: Long) derives Codec.AsObject
|
final case class PlayerID(id: Long) derives Codec.AsObject
|
||||||
|
|
||||||
final case class Player(name: String, id: PlayerID) derives Codec.AsObject
|
final case class Player(name: String, id: PlayerID) derives Codec.AsObject
|
||||||
object Player {
|
|
||||||
def create(name: String) = Player(name, PlayerID(Random.nextLong))
|
|
||||||
}
|
|
||||||
|
|
||||||
final case class RoomID(name: String) derives Codec.AsObject
|
final case class RoomID(id: Long) derives Codec.AsObject
|
||||||
|
|
||||||
final case class Room(
|
|
||||||
id: RoomID,
|
|
||||||
players: List[Player],
|
|
||||||
owner: PlayerID,
|
|
||||||
password: String,
|
|
||||||
allowedCards: List[String],
|
|
||||||
round: RoundState,
|
|
||||||
playersPasswords: Map[String, String] = Map.empty // nickname into password
|
|
||||||
) {
|
|
||||||
def toViewFor(playerId: PlayerID): RoomStateView = {
|
|
||||||
players
|
|
||||||
.find(_.id == playerId)
|
|
||||||
.fold(ifEmpty = RoomStateView.empty)((me: Player) =>
|
|
||||||
RoomStateView(
|
|
||||||
players,
|
|
||||||
me.id,
|
|
||||||
allowedCards,
|
|
||||||
round,
|
|
||||||
playerId == owner
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
object Room {
|
|
||||||
val testRoom = Room(
|
|
||||||
id = RoomID("testroom"),
|
|
||||||
players = List(
|
|
||||||
Player("me", PlayerID(1L)),
|
|
||||||
Player("horsey", PlayerID(444L)),
|
|
||||||
Player("froggy", PlayerID(555L)),
|
|
||||||
Player("owley", PlayerID(777L))
|
|
||||||
),
|
|
||||||
owner = PlayerID(1L),
|
|
||||||
password = "password",
|
|
||||||
allowedCards = List("S", "M", "L"),
|
|
||||||
// TODO - this needs to be a different hting
|
|
||||||
round = RoundState.Voting(None, List.empty)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,5 @@ import io.circe.generic.semiauto._
|
|||||||
import io.circe._
|
import io.circe._
|
||||||
|
|
||||||
object Requests {
|
object Requests {
|
||||||
final case class LogIn(roomName: String, nickname: String, password: String, nickPassword: String)
|
final case class LogIn(roomName: String, nickname: String, password: String) derives Codec.AsObject
|
||||||
derives Codec.AsObject
|
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import org.scalajs.dom
|
|||||||
val javascriptLogo: String = js.native
|
val javascriptLogo: String = js.native
|
||||||
|
|
||||||
@main
|
@main
|
||||||
def FrontendMain(): Unit =
|
def PlanningPokerUrgh(): Unit =
|
||||||
renderOnDomContentLoaded(
|
renderOnDomContentLoaded(
|
||||||
dom.document.getElementById("app"),
|
dom.document.getElementById("app"),
|
||||||
Main.appElement()
|
Main.appElement()
|
||||||
@ -30,9 +30,7 @@ object Main {
|
|||||||
import io.laminext.websocket.circe.WebSocket._
|
import io.laminext.websocket.circe.WebSocket._
|
||||||
import io.laminext.websocket.circe.webSocketReceiveBuilderSyntax
|
import io.laminext.websocket.circe.webSocketReceiveBuilderSyntax
|
||||||
|
|
||||||
val roomStateWSStream = io.laminext.websocket.WebSocket
|
val roomStateWSStream = io.laminext.websocket.WebSocket.path("/api/subscribe").json[RoomStateView, Unit]
|
||||||
.path("/api/subscribe")
|
|
||||||
.json[RoomStateView, Unit]
|
|
||||||
.build(
|
.build(
|
||||||
managed = true,
|
managed = true,
|
||||||
autoReconnect = false,
|
autoReconnect = false,
|
@ -13,15 +13,16 @@ object TableView {
|
|||||||
// so, it's more efficient to share an observable, than to create multiple copies...
|
// so, it's more efficient to share an observable, than to create multiple copies...
|
||||||
def renderTable(roundSignal: Signal[RoomStateView]): Element = {
|
def renderTable(roundSignal: Signal[RoomStateView]): Element = {
|
||||||
val playerIdToCardTypeSignal =
|
val playerIdToCardTypeSignal =
|
||||||
roundSignal
|
roundSignal.combineWith(Main.appStateSignal.map(_.myId)).map((state, myIdOpt) =>
|
||||||
.combineWith(Main.appStateSignal.map(_.myId))
|
state.players.map(p =>
|
||||||
.map((state, myIdOpt) =>
|
p.id -> getPlayerCardType(p.id, state.round, p.name, myIdOpt)
|
||||||
state.players.map(p => p.id -> getPlayerCardType(p.id, state.round, p.name, myIdOpt))
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
div(
|
div(
|
||||||
className := "w-full h-full border-2 border-amber-700 flex flex-row justify-center items-center bg-green-100",
|
className := "w-full h-full border-2 border-amber-700 flex flex-row justify-center items-center bg-green-100",
|
||||||
children <-- playerIdToCardTypeSignal.split(_._1) { (id, initial, cardTypeSignal) =>
|
children <-- playerIdToCardTypeSignal.split(_._1) {
|
||||||
|
(id, initial, cardTypeSignal) =>
|
||||||
renderPlayerCard(cardTypeSignal.map(_._2))
|
renderPlayerCard(cardTypeSignal.map(_._2))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -70,7 +71,7 @@ object TableView {
|
|||||||
case CardBack => ""
|
case CardBack => ""
|
||||||
case Open(vote) => vote
|
case Open(vote) => vote
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user