diff --git a/client/src/main/scala/ScalaJS/AjaxClient.scala b/client/src/main/scala/ScalaJS/AjaxClient.scala new file mode 100644 index 0000000..26e4204 --- /dev/null +++ b/client/src/main/scala/ScalaJS/AjaxClient.scala @@ -0,0 +1,29 @@ +package ScalaJS + +import autowire.Bounds.None +import org.scalajs.dom.ext.Ajax + +import scala.scalajs.js +import scala.scalajs.js.JSON +import scala.concurrent.ExecutionContext.Implicits.global + +@js.native +trait randomWordResult extends js.Object { + def word: String +} + +object AjaxClient extends autowire.Client[String, None, None] { + override def doCall(req: AjaxClient.Request) = { + Ajax.get("/spa/api/" + req.path.mkString("/")).map { xhr => + JSON.parse(xhr.responseText).asInstanceOf[randomWordResult].word + } + } + + override def read[Result](p: String)(implicit evidence$1: None[Result]) = ??? + + override def write[Result](r: Result)(implicit evidence$2: None[Result]) = ??? +} + +trait Api { + def getRandomWord(): String +} \ No newline at end of file diff --git a/client/src/main/scala/ScalaJS/App.scala b/client/src/main/scala/ScalaJS/App.scala index 959edfc..8101ad8 100644 --- a/client/src/main/scala/ScalaJS/App.scala +++ b/client/src/main/scala/ScalaJS/App.scala @@ -1,6 +1,5 @@ package ScalaJS -import ScalaJS.Component.Main import org.scalajs.dom.document import scala.scalajs.js.JSApp @@ -8,7 +7,7 @@ import scala.scalajs.js.JSApp object App extends JSApp { def main(): Unit = { println("App starting...") - // document.getElementById("root").textContent = "scalaJs entry point" - Main().renderIntoDOM(document.getElementById("root")) +// Main().renderIntoDOM(document.getElementById("root")) + AppRouter.router().renderIntoDOM(document.getElementById("root")) } } diff --git a/client/src/main/scala/ScalaJS/AppRouter.scala b/client/src/main/scala/ScalaJS/AppRouter.scala new file mode 100644 index 0000000..bd45838 --- /dev/null +++ b/client/src/main/scala/ScalaJS/AppRouter.scala @@ -0,0 +1,24 @@ +package ScalaJS + +import ScalaJS.Component.{Main, Show} +import japgolly.scalajs.react.extra.router.{BaseUrl, Redirect, Router, RouterConfigDsl} +import japgolly.scalajs.react.vdom.Implicits._ + + +object AppRouter { + case object Home extends View +// case class Play(game: HangPersonGame) extends View + case object Play extends View + + val routerConfig = RouterConfigDsl[View].buildConfig { dsl => + import dsl._ + + (emptyRule + | staticRoute(root, Home) ~> renderR(ctl => Main.component(ctl)) + | staticRoute("#show", Play) ~> renderR(ctl => Show.component(ctl)) + ).notFound(redirectToPath("/")(Redirect.Replace)) + .verify(Home) + } + + val router = Router(BaseUrl.fromWindowOrigin + "/hangperson/spa", routerConfig) +} diff --git a/client/src/main/scala/ScalaJS/Component/Bootstrap.scala b/client/src/main/scala/ScalaJS/Component/Bootstrap.scala new file mode 100644 index 0000000..7ffdf0f --- /dev/null +++ b/client/src/main/scala/ScalaJS/Component/Bootstrap.scala @@ -0,0 +1,7 @@ +package ScalaJS.Component + +object Bootstrap { + +@inline private def bss = ??? + +} diff --git a/client/src/main/scala/ScalaJS/Component/Button.scala b/client/src/main/scala/ScalaJS/Component/Button.scala new file mode 100644 index 0000000..19d9ecc --- /dev/null +++ b/client/src/main/scala/ScalaJS/Component/Button.scala @@ -0,0 +1,15 @@ +package ScalaJS.Component + +import ScalaJS.Styles.Common +import japgolly.scalajs.react +import japgolly.scalajs.react.Callback +import japgolly.scalajs.react.vdom.html_<^._ + +import scala.language.implicitConversions + +object Button { + case class Props(onClick: Callback, style: Common.Value = Common.default) + +// val component = react.ScalaComponent.builder[String]("Button") +// .render_P((_, props, children) => <.button(^.cls := "btn-"+ props)) +} \ No newline at end of file diff --git a/client/src/main/scala/ScalaJS/Component/Main.scala b/client/src/main/scala/ScalaJS/Component/Main.scala index f5cbd1b..b44cd7c 100644 --- a/client/src/main/scala/ScalaJS/Component/Main.scala +++ b/client/src/main/scala/ScalaJS/Component/Main.scala @@ -1,12 +1,19 @@ package ScalaJS.Component +import ScalaJS.AppRouter.Play +import ScalaJS.View import japgolly.scalajs.react.ScalaComponent +import japgolly.scalajs.react.extra.router.RouterCtl import japgolly.scalajs.react.vdom.html_<^._ object Main { - val component = ScalaComponent.builder[Unit]("Main") - .renderStatic(<.p("ReactJS Hello World")) + val component = ScalaComponent.builder[RouterCtl[View]]("Main") + .render_P( ctl => + <.div(^.cls := "panel-footer", + <.a(^.cls := "btn btn-primary", ^.id := "newgame", ^.value := "New Game", ctl setOnClick Play, "New Game") + ) + ) .build - def apply() = component() -} + def apply() = component +} diff --git a/client/src/main/scala/ScalaJS/Component/Show.scala b/client/src/main/scala/ScalaJS/Component/Show.scala new file mode 100644 index 0000000..c5f50ea --- /dev/null +++ b/client/src/main/scala/ScalaJS/Component/Show.scala @@ -0,0 +1,38 @@ +package ScalaJS.Component + +import ScalaJS.{AjaxClient, Api, View, randomWordResult} +import helpers.HangPersonGame +import japgolly.scalajs.react.ScalaComponent +import japgolly.scalajs.react.extra.router.RouterCtl +import japgolly.scalajs.react.vdom.html_<^._ +import org.scalajs.dom.ext.Ajax + +import scala.concurrent.Await +import scala.scalajs.js.JSON +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ +import scala.language.postfixOps + + +object Show { +// val getRandomWord: String = AjaxClient[Api].getRandomWord() + val getRandomWord = Ajax.get("/hangperson/spa/api/randomWord") + .map(xhr => JSON.parse(xhr.responseText).asInstanceOf[randomWordResult].word) + + val component = ScalaComponent.builder[RouterCtl[View]]("Show") + .render_P(ctl => + <.div(^.cls := "panel-body", + <.h2("Guess a letter"), + <.p("Wrong Guesses:", + <.span(^.cls := "text-guesses","wrong_guesses") + ), + <.p("Word so far:", + <.span(^.cls := "text-word", "word_with_guesses") + ), + <.p("error handling is a todo"), + <.p(s"$getRandomWord") + ) + ) + .build + def apply() = component +} diff --git a/client/src/main/scala/ScalaJS/Styles/Common.scala b/client/src/main/scala/ScalaJS/Styles/Common.scala new file mode 100644 index 0000000..4786936 --- /dev/null +++ b/client/src/main/scala/ScalaJS/Styles/Common.scala @@ -0,0 +1,5 @@ +package ScalaJS.Styles + +object Common extends Enumeration { + val default, primary, success, info, warning, danger = Value +} diff --git a/client/src/main/scala/ScalaJS/View.scala b/client/src/main/scala/ScalaJS/View.scala new file mode 100644 index 0000000..a4e84b4 --- /dev/null +++ b/client/src/main/scala/ScalaJS/View.scala @@ -0,0 +1,5 @@ +package ScalaJS + +trait View { + +} diff --git a/project/Settings.scala b/project/Settings.scala index 927dba4..9c8b9c4 100644 --- a/project/Settings.scala +++ b/project/Settings.scala @@ -35,7 +35,7 @@ object Settings { val scalaDom = "0.9.1" val scalajsReact = "1.0.1" // val log4js = "1.4.10" - // val autowire = "0.2.5" + val autowire = "0.2.5" // val booPickle = "1.2.5" // val diode = "1.1.0" // val uTest = "0.4.4" @@ -45,6 +45,8 @@ object Settings { val compass = "1.0.2" val fontawesome = "4.3.0-1" val scalajsScripts = "1.0.0" + val specs2Extra = "3.8" + val circe = "0.8" } /** @@ -52,7 +54,7 @@ object Settings { * the special %%% function selects the correct version for each project */ val sharedDependencies = Def.setting(Seq( - // "com.lihaoyi" %%% "autowire" % versions.autowire, + "com.lihaoyi" %%% "autowire" % Versions.autowire // "me.chrons" %%% "boopickle" % versions.booPickle )) @@ -65,7 +67,8 @@ object Settings { // "com.lihaoyi" %% "utest" % versions.uTest % Test cache, ws, - specs2 % Test + specs2 % Test, + "org.specs2" %% "specs2-matcher-extra" % Versions.specs2Extra % "test" )) /** Dependencies only used by the JS project (note the use of %%% instead of %%) */ diff --git a/server/app/controllers/HangPersonSpa.scala b/server/app/controllers/HangPersonSpa.scala index fd56b8d..e2c9512 100644 --- a/server/app/controllers/HangPersonSpa.scala +++ b/server/app/controllers/HangPersonSpa.scala @@ -1,9 +1,27 @@ package controllers +import javax.inject.Inject + +import play.api.libs.json._ +import play.api.libs.ws.WSClient import play.api.mvc._ -class HangPersonSpa extends Controller { +class HangPersonSpa @Inject()(val ws: WSClient) extends Controller with RandomWordClient { def index = Action { Ok(views.html.HangPerson.spa.index()) } + + /** + * TODO: if someone inspect the network tab of the browser can read the word and cheat. + * find a solution to communicate in a secure way the data among client and server. + * try use JWT or just an encrypted one + * + */ + def getRandomWord = Action { + val json: JsValue = JsObject(Seq( + "word" -> JsString(randomWord) + )) + + Ok(Json.toJson(json)) + } } diff --git a/server/app/views/HangPerson/newActionSnippet.scala.html b/server/app/views/HangPerson/newActionSnippet.scala.html index 9efcfd1..fc24f66 100644 --- a/server/app/views/HangPerson/newActionSnippet.scala.html +++ b/server/app/views/HangPerson/newActionSnippet.scala.html @@ -1,5 +1,5 @@
diff --git a/server/conf/routes b/server/conf/routes index 3ab6ad4..f2ee24b 100644 --- a/server/conf/routes +++ b/server/conf/routes @@ -3,17 +3,18 @@ # ~~~~ # Home page -GET / controllers.Application.index +GET / controllers.Application.index -GET /hangperson controllers.Default.redirect(to = "/hangperson/new") -GET /hangperson/new controllers.HangPerson.newAction -GET /hangperson/create controllers.Default.redirect(to = "/hangperson/show") -POST /hangperson/create controllers.HangPerson.create -GET /hangperson/show controllers.HangPerson.show -POST /hangperson/guess controllers.HangPerson.guess -GET /hangperson/win controllers.HangPerson.win -GET /hangperson/lose controllers.HangPerson.lose -GET /hangperson/spa controllers.HangPersonSpa.index +GET /hangperson controllers.Default.redirect(to = "/hangperson/new") +GET /hangperson/new controllers.HangPerson.newAction +GET /hangperson/create controllers.Default.redirect(to = "/hangperson/show") +POST /hangperson/create controllers.HangPerson.create +GET /hangperson/show controllers.HangPerson.show +POST /hangperson/guess controllers.HangPerson.guess +GET /hangperson/win controllers.HangPerson.win +GET /hangperson/lose controllers.HangPerson.lose +GET /hangperson/spa controllers.HangPersonSpa.index +GET /hangperson/spa/api/randomWord controllers.HangPersonSpa.getRandomWord # Map static resources from the /public folder to the /assets URL path -GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) +GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) diff --git a/server/test/controllers/HanPersonSpaSpec.scala b/server/test/controllers/HanPersonSpaSpec.scala deleted file mode 100644 index 4271939..0000000 --- a/server/test/controllers/HanPersonSpaSpec.scala +++ /dev/null @@ -1,13 +0,0 @@ -package controllers - -import org.junit.runner.RunWith -import org.specs2.runner.JUnitRunner -import play.api.test.{PlaySpecification, WithBrowser} - -@RunWith(classOf[JUnitRunner]) -class HanPersonSpaSpec extends PlaySpecification { - "work from within a browser SPA" in new WithBrowser { - browser.goTo("http://localhost:" + port + "/hangperson/spa") - browser.pageSource must contain("Hang Person SPA") - } -} diff --git a/server/test/controllers/HangPersonSpaSpec.scala b/server/test/controllers/HangPersonSpaSpec.scala new file mode 100644 index 0000000..34b0745 --- /dev/null +++ b/server/test/controllers/HangPersonSpaSpec.scala @@ -0,0 +1,25 @@ +package controllers + +import org.junit.runner.RunWith +import org.specs2.matcher.JsonMatchers +import org.specs2.runner.JUnitRunner +import play.api.test.{FakeRequest, PlaySpecification, WithApplication, WithBrowser} +import play.api.libs.json._ + + +@RunWith(classOf[JUnitRunner]) +class HangPersonSpaSpec extends PlaySpecification with JsonMatchers { + "work from within a browser SPA" in new WithBrowser { + browser.goTo("http://localhost:" + port + "/hangperson/spa") + browser.pageSource must contain("Hang Person SPA") + } + + "random word in JSON response format" in new WithApplication { + val Some(response) = route(app, FakeRequest(GET, "/hangperson/spa/randomWord")) + status(response) must equalTo(OK) + contentType(response) must beSome("application/json") + + val wordLength = (contentAsJson(response) \ "word").getOrElse(JsString("")).as[String].length() + contentAsString(response) must /("word" -> s"[a-z]{$wordLength}".r) + } +}