From 7f9815e83fa072e537772de3ba32bc35e6466243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pit-Claudel?= <clement.pit-claudel@epfl.ch> Date: Sun, 22 Dec 2024 16:07:38 +0100 Subject: [PATCH] client: Allow customization of banner and home page --- .../scala/cs214/webapp/client/Pages.scala | 60 ++++++++++--------- .../scala/cs214/webapp/client/Router.scala | 10 ++-- .../scala/cs214/webapp/client/WebClient.scala | 2 +- .../src/main/scala/cs214/webapp/Common.scala | 3 + 4 files changed, 42 insertions(+), 33 deletions(-) diff --git a/js/src/main/scala/cs214/webapp/client/Pages.scala b/js/src/main/scala/cs214/webapp/client/Pages.scala index 9603741..99c093e 100644 --- a/js/src/main/scala/cs214/webapp/client/Pages.scala +++ b/js/src/main/scala/cs214/webapp/client/Pages.scala @@ -4,7 +4,6 @@ package client import java.net.URLEncoder import scala.concurrent.ExecutionContext.Implicits.global import org.scalajs.dom -import org.scalajs.dom.{Element, KeyCode} import org.scalajs.dom.html.{Input, Select, TextArea} import scalatags.JsDom.all.* import scalatags.JsDom.tags2.{article, section} @@ -15,7 +14,7 @@ abstract class Page: def renderInto(target: dom.Element): Unit def pageHeader(subtitle: String) = - frag(h1(cls := "title", "ScalApp"), h2(subtitle)) + frag(h1(cls := "title", Config.BANNER), h2(subtitle)) def replaceChildren(target: dom.Element)(frag: Frag): Unit = target.replaceChildren(frag.render) @@ -26,32 +25,39 @@ object Page: def from(url: String): Page = WebClient.route(url) /** The initial home page which fetches the list of apps */ -object HomePage extends Page: - val classList = "HomePage" +abstract class AppCatalogPage extends Page: + def render(apps: Seq[AppInfo]): Frag def selectApp(appId: AppId): Unit = WebClient.navigateTo(InstanceCreationPage(appId)) - def renderInto(target: Element) = + def renderApp(appInfo: AppInfo): Frag = + figure( + onclick := (() => selectApp(appInfo.id)), + img(src := s"/static/${appInfo.id}.png"), + figcaption( + h4(appInfo.name), + p(appInfo.description) + ) + ) + + override def renderInto(target: dom.Element) = Requests.listApps.map: resp => - doRender(resp.apps)(target) + replaceChildren(target): + render(resp.apps) - private def doRender(apps: Seq[AppInfo])(target: Element) = replaceChildren(target): +object HomePage extends AppCatalogPage: + val classList = "HomePage" + + override def render(appInfos: Seq[AppInfo]): Frag = frag( - pageHeader("Select an app"), - apps.groupBy(_.year).toList.map: (year, appsOfYear) => + pageHeader("Pick an app!"), + appInfos.groupBy(_.year).toList.map: (year, apps) => article(cls := "app-list", - h3(year.toString), + h2(year.toString), section(cls := "app-grid", - appsOfYear.sortBy(_.name).map: appInfo => - figure( - onclick := (() => selectApp(appInfo.id)), - img(src := s"/static/${appInfo.id}.png"), - figcaption( - h4(appInfo.name), - p(appInfo.description) - ) - ) + apps.sortBy(_.name).map: appInfo => + renderApp(appInfo) ) ) ) @@ -67,7 +73,7 @@ case class InstanceCreationPage(appId: AppId) extends Page: Requests.createInstance(appId, userIds).map: resp => WebClient.navigateTo(UIPage(appId, resp.instanceId)) - def renderInto(target: Element) = replaceChildren(target): + def renderInto(target: dom.Element) = replaceChildren(target): frag( pageHeader(s"Create a new $appId instance"), form( @@ -96,7 +102,7 @@ case class UIPage(appId: AppId, instanceId: InstanceId) extends Page: private def cssId(idx: Int) = f"ui-$idx" private val handleKeyboardEvent: Function1[dom.KeyboardEvent, Unit] = (e: dom.KeyboardEvent) => - if e.keyCode == KeyCode.Enter then + if e.keyCode == dom.KeyCode.Enter then e.preventDefault() joinPage() @@ -113,7 +119,7 @@ case class UIPage(appId: AppId, instanceId: InstanceId) extends Page: getSelected.map: ui => WebClient.navigateTo(JoinPage(appId, instanceId, ui.uiId)) - def renderInto(target: Element) = + def renderInto(target: dom.Element) = require(appUIs.nonEmpty, f"No UI found for app with id $appId.") if appUIs.size == 1 then WebClient.navigateTo(JoinPage(appId, instanceId, appUIs(0).uiId), overwriteHistory = true) @@ -145,7 +151,7 @@ case class UIPage(appId: AppId, instanceId: InstanceId) extends Page: /** The pre-connection menu, which fetches the user list. */ case class JoinPage(appId: AppId, instanceId: InstanceId, uiId: UIId) extends Page: val classList = "JoinPageLoader" - def renderInto(target: Element) = + def renderInto(target: dom.Element) = Requests.instanceInfo(instanceId).map: resp => JoinPage_(appId, instanceId, uiId, resp.userIds).renderInto(target) @@ -158,7 +164,7 @@ case class JoinPage_(appId: AppId, instanceId: InstanceId, uiId: UIId, userIds: private def cssId(idx: Int) = f"user-$idx" private val handleKeyboardEvent: Function1[dom.KeyboardEvent, Unit] = (e: dom.KeyboardEvent) => - if e.keyCode == KeyCode.Enter then + if e.keyCode == dom.KeyCode.Enter then e.preventDefault() joinAppInstance() @@ -175,7 +181,7 @@ case class JoinPage_(appId: AppId, instanceId: InstanceId, uiId: UIId, userIds: getSelected.map: userId => WebClient.navigateTo(AppPage(appId, instanceId, uiId, userId)) - def renderInto(target: Element): Unit = + def renderInto(target: dom.Element): Unit = if userIds.size == 1 then WebClient.navigateTo(AppPage(appId, instanceId, uiId, userIds(0)), overwriteHistory = true) else replaceChildren(target): @@ -231,7 +237,7 @@ case class AppPage(appId: AppId, instanceId: InstanceId, uiId: UIId, userId: Use .replace("{{authority}}", hostname + (if port.nonEmpty then f":$port" else "")) .replace("{{userId}}", URLEncoder.encode(userId, "UTF-8")) - def renderInto(target: Element) = + def renderInto(target: dom.Element) = replaceChildren(target): frag(header(id := "banner"), tag("section")(id := "app")) Requests.instanceInfo(instanceId).map: appInfo => @@ -244,7 +250,7 @@ case class AppPage(appId: AppId, instanceId: InstanceId, uiId: UIId, userId: Use case class IpBanner(shareUrl: String) extends Page: val classList = "IpBanner" - def renderInto(target: Element) = + def renderInto(target: dom.Element) = replaceChildren(target): val copyToClipboardButtonText = "📋 Copy to clipboard." frag( diff --git a/js/src/main/scala/cs214/webapp/client/Router.scala b/js/src/main/scala/cs214/webapp/client/Router.scala index 103b817..067fb07 100644 --- a/js/src/main/scala/cs214/webapp/client/Router.scala +++ b/js/src/main/scala/cs214/webapp/client/Router.scala @@ -4,8 +4,8 @@ package client import java.net.{URLDecoder, URLEncoder} abstract class Router: - protected def path(page: Page): Seq[String] - protected def page(path: Seq[String]): Page + protected def path(page: Page): List[String] + protected def page(path: List[String]): Page def url(page: Page): String = val fragments = path(page).map(s => URLEncoder.encode(s, "UTF-8")) @@ -16,8 +16,8 @@ abstract class Router: val decoded = components.map(c => URLDecoder.decode(c, "UTF-8")).toList page(decoded) -object DefaultRouter extends Router: - def path(page: Page): Seq[String] = page match +class DefaultRouter extends Router: + override def path(page: Page): List[String] = page match case HomePage => List("") case InstanceCreationPage(appId) => @@ -31,7 +31,7 @@ object DefaultRouter extends Router: case _ => throw IllegalArgumentException(f"No path for $this!") - def page(path: List[String]): Page = path match + override def page(path: List[String]): Page = path match case List("") => HomePage case List("app", appId) => diff --git a/js/src/main/scala/cs214/webapp/client/WebClient.scala b/js/src/main/scala/cs214/webapp/client/WebClient.scala index 6ec1ee2..2155794 100644 --- a/js/src/main/scala/cs214/webapp/client/WebClient.scala +++ b/js/src/main/scala/cs214/webapp/client/WebClient.scala @@ -8,7 +8,7 @@ import org.scalajs.dom object WebClient: private val appLibrary: mutable.Map[AppId, Seq[ClientApp]] = mutable.Map.empty - private var router: Router = DefaultRouter + private var router: Router = DefaultRouter() def setRouter(router: Router): Unit = this.router = router def route(url: String): Page = router.route(url) def url(page: Page): String = router.url(page) diff --git a/shared/src/main/scala/cs214/webapp/Common.scala b/shared/src/main/scala/cs214/webapp/Common.scala index abf60db..c494a49 100644 --- a/shared/src/main/scala/cs214/webapp/Common.scala +++ b/shared/src/main/scala/cs214/webapp/Common.scala @@ -8,6 +8,9 @@ object Config: On a LAN, the server includes its own local IP in `appInfo` responses. */ val PUBLIC_INSTANCE = sys.env.getOrElse("WEBAPPLIB_PUBLIC_INSTANCE", "") == "1" + /** Title shown in website banner **/ + val BANNER = sys.env.getOrElse("WEBAPPLIB_BANNER", "ScalApp") + /** Unique identifier for an app */ type AppId = String -- GitLab