Skip to content
Snippets Groups Projects
Commit 7f9815e8 authored by Clément Pit-Claudel's avatar Clément Pit-Claudel
Browse files

client: Allow customization of banner and home page

parent 060e8e3b
Branches
Tags
1 merge request!38Customizable homepage + automatic reconnection
...@@ -4,7 +4,6 @@ package client ...@@ -4,7 +4,6 @@ package client
import java.net.URLEncoder import java.net.URLEncoder
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
import org.scalajs.dom import org.scalajs.dom
import org.scalajs.dom.{Element, KeyCode}
import org.scalajs.dom.html.{Input, Select, TextArea} import org.scalajs.dom.html.{Input, Select, TextArea}
import scalatags.JsDom.all.* import scalatags.JsDom.all.*
import scalatags.JsDom.tags2.{article, section} import scalatags.JsDom.tags2.{article, section}
...@@ -15,7 +14,7 @@ abstract class Page: ...@@ -15,7 +14,7 @@ abstract class Page:
def renderInto(target: dom.Element): Unit def renderInto(target: dom.Element): Unit
def pageHeader(subtitle: String) = 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 = def replaceChildren(target: dom.Element)(frag: Frag): Unit =
target.replaceChildren(frag.render) target.replaceChildren(frag.render)
...@@ -26,32 +25,39 @@ object Page: ...@@ -26,32 +25,39 @@ object Page:
def from(url: String): Page = WebClient.route(url) def from(url: String): Page = WebClient.route(url)
/** The initial home page which fetches the list of apps */ /** The initial home page which fetches the list of apps */
object HomePage extends Page: abstract class AppCatalogPage extends Page:
val classList = "HomePage" def render(apps: Seq[AppInfo]): Frag
def selectApp(appId: AppId): Unit = def selectApp(appId: AppId): Unit =
WebClient.navigateTo(InstanceCreationPage(appId)) 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 => 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( frag(
pageHeader("Select an app"), pageHeader("Pick an app!"),
apps.groupBy(_.year).toList.map: (year, appsOfYear) => appInfos.groupBy(_.year).toList.map: (year, apps) =>
article(cls := "app-list", article(cls := "app-list",
h3(year.toString), h2(year.toString),
section(cls := "app-grid", section(cls := "app-grid",
appsOfYear.sortBy(_.name).map: appInfo => apps.sortBy(_.name).map: appInfo =>
figure( renderApp(appInfo)
onclick := (() => selectApp(appInfo.id)),
img(src := s"/static/${appInfo.id}.png"),
figcaption(
h4(appInfo.name),
p(appInfo.description)
)
)
) )
) )
) )
...@@ -67,7 +73,7 @@ case class InstanceCreationPage(appId: AppId) extends Page: ...@@ -67,7 +73,7 @@ case class InstanceCreationPage(appId: AppId) extends Page:
Requests.createInstance(appId, userIds).map: resp => Requests.createInstance(appId, userIds).map: resp =>
WebClient.navigateTo(UIPage(appId, resp.instanceId)) WebClient.navigateTo(UIPage(appId, resp.instanceId))
def renderInto(target: Element) = replaceChildren(target): def renderInto(target: dom.Element) = replaceChildren(target):
frag( frag(
pageHeader(s"Create a new $appId instance"), pageHeader(s"Create a new $appId instance"),
form( form(
...@@ -96,7 +102,7 @@ case class UIPage(appId: AppId, instanceId: InstanceId) extends Page: ...@@ -96,7 +102,7 @@ case class UIPage(appId: AppId, instanceId: InstanceId) extends Page:
private def cssId(idx: Int) = f"ui-$idx" private def cssId(idx: Int) = f"ui-$idx"
private val handleKeyboardEvent: Function1[dom.KeyboardEvent, Unit] = (e: dom.KeyboardEvent) => 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() e.preventDefault()
joinPage() joinPage()
...@@ -113,7 +119,7 @@ case class UIPage(appId: AppId, instanceId: InstanceId) extends Page: ...@@ -113,7 +119,7 @@ case class UIPage(appId: AppId, instanceId: InstanceId) extends Page:
getSelected.map: ui => getSelected.map: ui =>
WebClient.navigateTo(JoinPage(appId, instanceId, ui.uiId)) 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.") require(appUIs.nonEmpty, f"No UI found for app with id $appId.")
if appUIs.size == 1 then if appUIs.size == 1 then
WebClient.navigateTo(JoinPage(appId, instanceId, appUIs(0).uiId), overwriteHistory = true) WebClient.navigateTo(JoinPage(appId, instanceId, appUIs(0).uiId), overwriteHistory = true)
...@@ -145,7 +151,7 @@ case class UIPage(appId: AppId, instanceId: InstanceId) extends Page: ...@@ -145,7 +151,7 @@ case class UIPage(appId: AppId, instanceId: InstanceId) extends Page:
/** The pre-connection menu, which fetches the user list. */ /** The pre-connection menu, which fetches the user list. */
case class JoinPage(appId: AppId, instanceId: InstanceId, uiId: UIId) extends Page: case class JoinPage(appId: AppId, instanceId: InstanceId, uiId: UIId) extends Page:
val classList = "JoinPageLoader" val classList = "JoinPageLoader"
def renderInto(target: Element) = def renderInto(target: dom.Element) =
Requests.instanceInfo(instanceId).map: resp => Requests.instanceInfo(instanceId).map: resp =>
JoinPage_(appId, instanceId, uiId, resp.userIds).renderInto(target) JoinPage_(appId, instanceId, uiId, resp.userIds).renderInto(target)
...@@ -158,7 +164,7 @@ case class JoinPage_(appId: AppId, instanceId: InstanceId, uiId: UIId, userIds: ...@@ -158,7 +164,7 @@ case class JoinPage_(appId: AppId, instanceId: InstanceId, uiId: UIId, userIds:
private def cssId(idx: Int) = f"user-$idx" private def cssId(idx: Int) = f"user-$idx"
private val handleKeyboardEvent: Function1[dom.KeyboardEvent, Unit] = (e: dom.KeyboardEvent) => 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() e.preventDefault()
joinAppInstance() joinAppInstance()
...@@ -175,7 +181,7 @@ case class JoinPage_(appId: AppId, instanceId: InstanceId, uiId: UIId, userIds: ...@@ -175,7 +181,7 @@ case class JoinPage_(appId: AppId, instanceId: InstanceId, uiId: UIId, userIds:
getSelected.map: userId => getSelected.map: userId =>
WebClient.navigateTo(AppPage(appId, instanceId, uiId, userId)) WebClient.navigateTo(AppPage(appId, instanceId, uiId, userId))
def renderInto(target: Element): Unit = def renderInto(target: dom.Element): Unit =
if userIds.size == 1 then if userIds.size == 1 then
WebClient.navigateTo(AppPage(appId, instanceId, uiId, userIds(0)), overwriteHistory = true) WebClient.navigateTo(AppPage(appId, instanceId, uiId, userIds(0)), overwriteHistory = true)
else replaceChildren(target): else replaceChildren(target):
...@@ -231,7 +237,7 @@ case class AppPage(appId: AppId, instanceId: InstanceId, uiId: UIId, userId: Use ...@@ -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("{{authority}}", hostname + (if port.nonEmpty then f":$port" else ""))
.replace("{{userId}}", URLEncoder.encode(userId, "UTF-8")) .replace("{{userId}}", URLEncoder.encode(userId, "UTF-8"))
def renderInto(target: Element) = def renderInto(target: dom.Element) =
replaceChildren(target): replaceChildren(target):
frag(header(id := "banner"), tag("section")(id := "app")) frag(header(id := "banner"), tag("section")(id := "app"))
Requests.instanceInfo(instanceId).map: appInfo => Requests.instanceInfo(instanceId).map: appInfo =>
...@@ -244,7 +250,7 @@ case class AppPage(appId: AppId, instanceId: InstanceId, uiId: UIId, userId: Use ...@@ -244,7 +250,7 @@ case class AppPage(appId: AppId, instanceId: InstanceId, uiId: UIId, userId: Use
case class IpBanner(shareUrl: String) extends Page: case class IpBanner(shareUrl: String) extends Page:
val classList = "IpBanner" val classList = "IpBanner"
def renderInto(target: Element) = def renderInto(target: dom.Element) =
replaceChildren(target): replaceChildren(target):
val copyToClipboardButtonText = "📋 Copy to clipboard." val copyToClipboardButtonText = "📋 Copy to clipboard."
frag( frag(
......
...@@ -4,8 +4,8 @@ package client ...@@ -4,8 +4,8 @@ package client
import java.net.{URLDecoder, URLEncoder} import java.net.{URLDecoder, URLEncoder}
abstract class Router: abstract class Router:
protected def path(page: Page): Seq[String] protected def path(page: Page): List[String]
protected def page(path: Seq[String]): Page protected def page(path: List[String]): Page
def url(page: Page): String = def url(page: Page): String =
val fragments = path(page).map(s => URLEncoder.encode(s, "UTF-8")) val fragments = path(page).map(s => URLEncoder.encode(s, "UTF-8"))
...@@ -16,8 +16,8 @@ abstract class Router: ...@@ -16,8 +16,8 @@ abstract class Router:
val decoded = components.map(c => URLDecoder.decode(c, "UTF-8")).toList val decoded = components.map(c => URLDecoder.decode(c, "UTF-8")).toList
page(decoded) page(decoded)
object DefaultRouter extends Router: class DefaultRouter extends Router:
def path(page: Page): Seq[String] = page match override def path(page: Page): List[String] = page match
case HomePage => case HomePage =>
List("") List("")
case InstanceCreationPage(appId) => case InstanceCreationPage(appId) =>
...@@ -31,7 +31,7 @@ object DefaultRouter extends Router: ...@@ -31,7 +31,7 @@ object DefaultRouter extends Router:
case _ => case _ =>
throw IllegalArgumentException(f"No path for $this!") 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("") => case List("") =>
HomePage HomePage
case List("app", appId) => case List("app", appId) =>
......
...@@ -8,7 +8,7 @@ import org.scalajs.dom ...@@ -8,7 +8,7 @@ import org.scalajs.dom
object WebClient: object WebClient:
private val appLibrary: mutable.Map[AppId, Seq[ClientApp]] = mutable.Map.empty 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 setRouter(router: Router): Unit = this.router = router
def route(url: String): Page = router.route(url) def route(url: String): Page = router.route(url)
def url(page: Page): String = router.url(page) def url(page: Page): String = router.url(page)
......
...@@ -8,6 +8,9 @@ object Config: ...@@ -8,6 +8,9 @@ object Config:
On a LAN, the server includes its own local IP in `appInfo` responses. */ On a LAN, the server includes its own local IP in `appInfo` responses. */
val PUBLIC_INSTANCE = sys.env.getOrElse("WEBAPPLIB_PUBLIC_INSTANCE", "") == "1" 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 */ /** Unique identifier for an app */
type AppId = String type AppId = String
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment