diff --git a/build.sbt b/build.sbt index 35d9c09..967f7fd 100644 --- a/build.sbt +++ b/build.sbt @@ -32,11 +32,12 @@ lazy val frontend = project */ libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.4.0", libraryDependencies += "com.raquo" %%% "laminar" % "15.0.1", - libraryDependencies += "io.laminext" %%% "websocket" % "0.15.0" + libraryDependencies += "io.laminext" %%% "websocket-circe" % "0.15.0", + libraryDependencies += "io.laminext" %%% "fetch" % "0.15.0" ) .dependsOn(common.js) -val circeVersion = "0.14.1" +val circeVersion = "0.14.5" val http4sVersion = "1.0.0-M39" lazy val backend = project diff --git a/common/src/main/scala/industries/sunshine/planningpoker/Models.scala b/common/src/main/scala/industries/sunshine/planningpoker/Models.scala index 9509bb9..3a79d1f 100644 --- a/common/src/main/scala/industries/sunshine/planningpoker/Models.scala +++ b/common/src/main/scala/industries/sunshine/planningpoker/Models.scala @@ -6,14 +6,10 @@ import io.circe._ object Models { /** view of the single planning poker round - * @param players - * \- people who are currently playing - * @param allowedCards - * \- the cards values that can be used by players - * @param round - * \- state of the selected cards of the players - * @param canCloseRound - * \- whether current player has access to button to finish the round + * @param players - people who are currently playing + * @param allowedCards- the cards values that can be used by players + * @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( players: List[Player], @@ -25,9 +21,15 @@ object Models { def playersCount: Int = players.size } - given Encoder[Map[PlayerID, String]] = Encoder.encodeMap[PlayerID, String] - given Decoder[Map[PlayerID, String]] = Decoder.decodeMap[PlayerID, String] - given Codec[Map[PlayerID, String]] = Codec.from(summon[Decoder[Map[PlayerID, String]]], summon[Encoder[Map[PlayerID, String]]]) + object RoomStateView { + given Encoder[Map[PlayerID, String]] = Encoder.encodeMap[PlayerID, String] + given Decoder[Map[PlayerID, String]] = Decoder.decodeMap[PlayerID, String] + given Codec[Map[PlayerID, String]] = Codec.from(summon[Decoder[Map[PlayerID, String]]], summon[Encoder[Map[PlayerID, String]]]) + + val empty = RoomStateView( + List.empty, PlayerID(0), List.empty, RoundState.Voting(None, List.empty), false + ) + } enum RoundState derives Codec.AsObject: diff --git a/frontend/src/main/scala/industries/sunshine/planningpoker/PlanningPokerUrgh.scala b/frontend/src/main/scala/industries/sunshine/planningpoker/PlanningPokerUrgh.scala index 7d3d13f..662140e 100644 --- a/frontend/src/main/scala/industries/sunshine/planningpoker/PlanningPokerUrgh.scala +++ b/frontend/src/main/scala/industries/sunshine/planningpoker/PlanningPokerUrgh.scala @@ -4,6 +4,7 @@ import scala.scalajs.js import scala.scalajs.js.annotation.* import com.raquo.laminar.api.L.{*, given} import industries.sunshine.planningpoker.common.Models.* +import scala.concurrent.duration._ import org.scalajs.dom @@ -20,20 +21,59 @@ def PlanningPokerUrgh(): Unit = object Main { final case class AppState( - myId: Option[PlayerID] + myId: Option[PlayerID] ) // TODO is this ok for state creation? link with auth component and use in another? val appStateSignal = Var(AppState(Some(TestModels.me.id))).signal val staticStateSignal = Var(TestModels.testRoom).signal + import io.laminext.websocket.circe.WebSocket._ + import io.laminext.websocket.circe.webSocketReceiveBuilderSyntax + + val roomStateWSStream = io.laminext.websocket.circe.WebSocket + .url("ws://0.0.0.0:5153/api/subscribe") + // .json[RoomStateView, Unit] + .build( + managed = true, + autoReconnect = false, + reconnectDelay = 1.second, + reconnectDelayOffline = 20.seconds, + reconnectRetries = 1 + ) + + val stateStream = roomStateWSStream.received + // and what's the difference between EventStream and Signal??? + val stateSignal = + stateStream.startWith(TestModels.testRoom, cacheInitialValue = true) + + // NOTE let's try with fetch \ rest + // import io.laminext.fetch.Fetch + // import com.raquo.laminar.api.L._ + // import scala.concurrent.ExecutionContext.Implicits.global + // val testRest = Fetch.get("http://0.0.0.0:5173/api/subscribe").text + + import scala.scalajs.js.Dynamic.{global => g} def appElement(): Element = { div( className := "w-screen h-screen flex flex-col justify-center items-center", div( - className := "h-24 w-full flex flex-for justify-center items-center bg-green-200", - p(className := "text-2xl", "Here be header"), + className := "bg-yellow-400", + child.text <-- roomStateWSStream.events + .map { ev => + { + val a = ev.toString() + g.console.warn(s"got state $a") + a + } + } + .startWith("BEFORE WS") ), - RoomView.renderRoom(staticStateSignal) + div( + className := "h-24 w-full flex flex-for justify-center items-center bg-green-200", + p(className := "text-2xl", "Here be header") + ), + RoomView.renderRoom(staticStateSignal), + roomStateWSStream.connect ) } } diff --git a/main.js b/main.js index eb7afca..cb4afe8 100644 --- a/main.js +++ b/main.js @@ -1,3 +1,22 @@ import './style.css' import './frontend/target/scala-3.2.0/frontend-fastopt/main.js' // import 'scalajs:main.js' + +// // const socket = new WebSocket("/api/subscribe"); +// const socket = new WebSocket("ws:0.0.0.0:5173/api/subscribe"); + +// socket.addEventListener("open", (event) => { +// console.log("!! WebSocket connected:", event); +// }); + +// socket.addEventListener("message", (event) => { +// console.log("!! WebSocket message received:", event); +// }); + +// socket.addEventListener("close", (event) => { +// console.log("!! WebSocket closed:", event); +// }); + +// socket.addEventListener("error", (event) => { +// console.log("!! WebSocket error:", event); +// }); diff --git a/vite.config.js b/vite.config.js index 3b0b85e..b710bb7 100644 --- a/vite.config.js +++ b/vite.config.js @@ -3,4 +3,16 @@ import scalaJSPlugin from "@scala-js/vite-plugin-scalajs"; export default defineConfig({ plugins: [scalaJSPlugin()], + server: { + proxy: { + // Proxy all requests starting with /api to your backend server + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, + ws: true, // Enable WebSocket proxying + rewrite: (path) => path.replace(/^\/api/, ''), + }, + }, + }, + });