diff options
Diffstat (limited to 'client')
-rw-r--r-- | client/src/Main.scala | 15 | ||||
-rw-r--r-- | client/src/model/FileRights.scala | 7 | ||||
-rw-r--r-- | client/src/model/Licence.scala | 8 | ||||
-rw-r--r-- | client/src/model/Pose.scala | 11 | ||||
-rw-r--r-- | client/src/model/PoseCategory.scala | 18 | ||||
-rw-r--r-- | client/src/model/Route.scala | 13 | ||||
-rw-r--r-- | client/src/router/Router.scala | 105 | ||||
-rw-r--r-- | client/src/service/Licences.scala | 31 | ||||
-rw-r--r-- | client/src/service/Poses.scala | 58 | ||||
-rw-r--r-- | client/src/view/View.scala | 22 | ||||
-rw-r--r-- | client/src/view/index/Index.scala | 27 | ||||
-rw-r--r-- | client/src/view/licence/LicencePage.scala | 24 | ||||
-rw-r--r-- | client/src/view/notFound/NotFound.scala | 12 |
13 files changed, 351 insertions, 0 deletions
diff --git a/client/src/Main.scala b/client/src/Main.scala new file mode 100644 index 0000000..e72c4a0 --- /dev/null +++ b/client/src/Main.scala @@ -0,0 +1,15 @@ +package yoga + +import mhtml.mount +import org.scalajs.dom.document + +import yoga.view.View + +object Main { + + def main(args: Array[String]): Unit = { + mount(document.body, View()) + () + } + +} diff --git a/client/src/model/FileRights.scala b/client/src/model/FileRights.scala new file mode 100644 index 0000000..3b91d37 --- /dev/null +++ b/client/src/model/FileRights.scala @@ -0,0 +1,7 @@ +package yoga.model + +final case class FileRights( + licence: Licence, + author: String, + modification: Option[String] = None +) diff --git a/client/src/model/Licence.scala b/client/src/model/Licence.scala new file mode 100644 index 0000000..883d01d --- /dev/null +++ b/client/src/model/Licence.scala @@ -0,0 +1,8 @@ +package yoga.model + +import java.net.URI + +final case class Licence( + name: String, + uri: URI +) diff --git a/client/src/model/Pose.scala b/client/src/model/Pose.scala new file mode 100644 index 0000000..e073466 --- /dev/null +++ b/client/src/model/Pose.scala @@ -0,0 +1,11 @@ +package yoga.model + +import java.net.URI + +final case class Pose( + name: String, + otherNames: Seq[String] = Nil, + category: PoseCategory, + image: URI, + rights: FileRights +) diff --git a/client/src/model/PoseCategory.scala b/client/src/model/PoseCategory.scala new file mode 100644 index 0000000..95eb974 --- /dev/null +++ b/client/src/model/PoseCategory.scala @@ -0,0 +1,18 @@ +package yoga.model + +import scala.collection.immutable.IndexedSeq + +import enumeratum.{ Enum, EnumEntry } + +sealed trait PoseCategory extends EnumEntry + +object PoseCategory extends Enum[PoseCategory] { + + case object Standing extends PoseCategory + case object Balancing extends PoseCategory + case object Sitting extends PoseCategory + case object Reclining extends PoseCategory + + val values: IndexedSeq[PoseCategory] = findValues + +} diff --git a/client/src/model/Route.scala b/client/src/model/Route.scala new file mode 100644 index 0000000..34cb1fc --- /dev/null +++ b/client/src/model/Route.scala @@ -0,0 +1,13 @@ +package yoga.model + +import java.net.URI + +sealed trait Route + +case object Route { + + case object Index extends Route + final case class Licence(uri: URI) extends Route + final object NotFound extends Route + +} diff --git a/client/src/router/Router.scala b/client/src/router/Router.scala new file mode 100644 index 0000000..60e5ef2 --- /dev/null +++ b/client/src/router/Router.scala @@ -0,0 +1,105 @@ +package yoga.router + +import scala.scalajs.js +import scala.scalajs.js.URIUtils +import scala.xml.Node +import java.net.URI + +import mhtml.Var +import org.scalajs.dom.raw.{ Element, Node => RawNode } +import org.scalajs.dom.{ document, window, Event, MouseEvent } + +import yoga.model.Route +import yoga.view.index.Index +import yoga.view.licence.LicencePage +import yoga.view.notFound.NotFound + +object Router { + + private val route: Var[Route] = Var(parseRoute(Path.current())) + + def render(): Node = + <div + mhtml-onmount={ (n: RawNode) => + document.addEventListener("click", Handler.click) + window.addEventListener("popstate", Handler.popState) + } + mhtml-onumnount={ (n: RawNode) => + document.removeEventListener("click", Handler.click) + window.removeEventListener("popstate", Handler.popState) + } + > + {route.map { + case Route.Index => Index() + case Route.Licence(uri) => LicencePage(uri) + case Route.NotFound => NotFound() + }} + </div> + + object Path { + + val index: String = "/" + + def licence(uri: URI): String = s"/licence/${uri}" + + def current(): String = + URIUtils.decodeURI(s"${window.location.pathname}${window.location.search}") + + } + + private def parseRoute(path: String): Route = + pathAndParams(path) match { + case (List(), _) => Route.Index + case (List("licence", uri), _) => Route.Licence(URI.create(uri)) + case _ => Route.NotFound + } + + private def pathAndParams(path: String): (List[String], List[String]) = { + println(path) + def splitPath(path: String) = path.split("/").drop(1).toList + path.split('?') match { + case Array(path) => (splitPath(path), Nil) + case Array(path, params) => (splitPath(path), params.split("&").toList) + } + } + + private object Handler { + + lazy val click: js.Function1[MouseEvent, Unit] = + routeTransitionOnAnchorClick { path => + route := parseRoute(path) + window.history.pushState(null, "", path) + } + + lazy val popState: js.Function1[Event, Unit] = + _ => route := parseRoute(Path.current()) + + } + + private def routeTransitionOnAnchorClick(goTo: String => Unit)(event: MouseEvent): Unit = + if (event.button == 0) + Dom.findSelfOrParent("a", event.target.asInstanceOf[RawNode]) match { + case Some(node) => + val elem = node.asInstanceOf[Element] + if (elem.getAttribute("data-nav") != "ignore") { + event.preventDefault() + val href = elem.getAttribute("href") + if (href != Path.current()) goTo(href) + } + case _ => + } + + private object Dom { + + @annotation.tailrec + def findSelfOrParent(nodeName: String, current: RawNode): Option[RawNode] = + if (current.nodeName.toString.toLowerCase == nodeName.toLowerCase) + Some(current) + else if (current.parentNode == null) + None + else + findSelfOrParent(nodeName, current.parentNode) + + } + +} diff --git a/client/src/service/Licences.scala b/client/src/service/Licences.scala new file mode 100644 index 0000000..77bfe5d --- /dev/null +++ b/client/src/service/Licences.scala @@ -0,0 +1,31 @@ +package yoga.service + +import java.net.URI + +import yoga.model.Licence + +object Licences { + + object CreativeCommons { + + val attributionAlike_2_5: Licence = + Licence( + name = "Creative Commons Attribution 2.5 Generic", + uri = URI.create("https://creativecommons.org/licenses/by/2.5/deed.en") + ) + + val attributionAlike_3_0: Licence = + Licence( + name = "Creative Commons Attribution-Share Alike 3.0 Unported license", + uri = URI.create("https://creativecommons.org/licenses/by-sa/3.0/deed.en") + ) + + val attributionAlike_4_0: Licence = + Licence( + name = "Creative Commons Attribution 4.0 International license", + uri = URI.create("https://creativecommons.org/licenses/by/4.0/deed.en") + ) + + } + +} diff --git a/client/src/service/Poses.scala b/client/src/service/Poses.scala new file mode 100644 index 0000000..584c13c --- /dev/null +++ b/client/src/service/Poses.scala @@ -0,0 +1,58 @@ +package yoga.service + +import java.net.URI + +import yoga.model.{ Pose, PoseCategory, FileRights } + +object Poses { + + def values: Seq[Pose] = Seq( + Pose( + name = "Downward-Facing Dog", + category = PoseCategory.Standing, + image = URI.create("downward-facing-dog.jpg"), + rights = FileRights( + author = "Joseph RENGER", + licence = Licences.CreativeCommons.attributionAlike_3_0 + ) + ), + Pose( + name = "Downward-Facing Tree", + otherNames = Seq("Yoga Handstand"), + category = PoseCategory.Balancing, + image = URI.create("downward-facing-tree.jpg"), + rights = FileRights( + author = "Chiswick Chap", + licence = Licences.CreativeCommons.attributionAlike_4_0, + modification = Some("cropped 81 % horizontally, 9 % vertically using CropTool with precise mode.") + ) + ), + // Pose( + // name = "Shooting bow", + // otherNames = Seq("Archer", "Bow and arrow"), + // category = PoseCategory.Sitting, + // image = URI.create("shooting-bow.jpg"), + // licence = Licences.CreativeCommons.attributionAlike_4_0 + // ), + // Pose( + // name = "Ananta’s pose", + // otherNames = Seq("Vishnu’s Couch pose"), + // category = PoseCategory.Reclining, + // image = URI.create("ananta-pose.jpg"), + // licence = Licences.CreativeCommons.attributionAlike_4_0 + // ), + // Pose( + // name = "Crescent Moon", + // category = PoseCategory.Standing, + // image = URI.create("crescent-moon.jpg"), + // licence = Licences.CreativeCommons.attributionAlike_3_0 + // ), + // Pose( + // name = "Half moon", + // category = PoseCategory.Standing, + // image = URI.create("half-moon.jpg"), + // licence = Licences.CreativeCommons.attributionAlike_3_0 + // ) + ) + +} diff --git a/client/src/view/View.scala b/client/src/view/View.scala new file mode 100644 index 0000000..5c79a71 --- /dev/null +++ b/client/src/view/View.scala @@ -0,0 +1,22 @@ +package yoga.view + +import scala.xml.Node + +import yoga.router.Router + +object View { + + def apply(): Node = + <div> + <header> + <a href={Router.Path.index} class="header__link"> + YOGA + </a> + </header> + + <main> + {Router.render()} + </main> + </div> + +} diff --git a/client/src/view/index/Index.scala b/client/src/view/index/Index.scala new file mode 100644 index 0000000..29cb6ca --- /dev/null +++ b/client/src/view/index/Index.scala @@ -0,0 +1,27 @@ +package yoga.view.index + +import scala.xml.Node + +import yoga.model.Pose +import yoga.router.Router +import yoga.service.Poses + +object Index { + + def apply(): Node = + <div class="index__poses"> + {Poses.values.map(renderPose)} + </div> + + def renderPose(pose: Pose): Node = + <a class="index__poseLink" href={Router.Path.licence(pose.image)}> + <div class="index__poseName"> + {pose.name} + </div> + <div class="index__poseImageParent"> + <img class="index__poseImage" src={s"images/${pose.image}"} /> + </div> + </a> + + +} diff --git a/client/src/view/licence/LicencePage.scala b/client/src/view/licence/LicencePage.scala new file mode 100644 index 0000000..fc76522 --- /dev/null +++ b/client/src/view/licence/LicencePage.scala @@ -0,0 +1,24 @@ +package yoga.view.licence + +import java.net.URI +import scala.xml.Node + +import yoga.service.Poses + +object LicencePage { + + def apply(uri: URI): Node = + <div> + {Poses.values.find(_.image == uri).map { pose => + <div> + <div> + <img src={s"images/${pose.image}"} /> + </div> + <a href={pose.rights.licence.uri.toString}> + {pose.rights.licence.name} + </a> + </div> + }} + </div> + +} diff --git a/client/src/view/notFound/NotFound.scala b/client/src/view/notFound/NotFound.scala new file mode 100644 index 0000000..a877640 --- /dev/null +++ b/client/src/view/notFound/NotFound.scala @@ -0,0 +1,12 @@ +package yoga.view.notFound + +import scala.xml.Node + +object NotFound { + + def apply(): Node = + <div> + Page not found + </div> + +} |