ScalaJS-based community web hub for Lund Faculty of Engineering course EDAB05, also known as pgk or introprog.
This project uses Vite as the build tool and development server for fast hot-reloading and optimized builds. Vite provides excellent development experience with instant server start and lightning-fast HMR.
src/main/scala/pgkn/
├── Main.scala # Application entry point
├── Router.scala # Central routing configuration and page definitions
├── components/ # Shared UI components
│ └── NavHeader.scala
│ └── ...
└── pages/ # Individual page components
├── Home.scala
└── ...
Key files:
Router.scala: Defines all pages as case objects and manages routing logic with Waypoint- Page objects: Each has an
apply(router)method that returns anHtmlElement - Component objects: Reusable UI elements with
apply()methods; router is passed only when navigation is needed
styles/
├── main.css # Main stylesheet, imports domain-level indexes
├── components/
│ ├── index.css # Imports all component stylesheets
│ └── nav-header.css # Individual component styles
└── pages/
├── index.css # Imports all page stylesheets
├── home.css # Individual page styles
└── sigrid.css
The CSS follows a Feature-Sliced Design pattern where domain index files aggregate individual stylesheets, keeping the main CSS file clean and organized.
Run the following to command to install the Node.js dependencies:
npm installRun these commands in separate terminals:
-
Scala compilation (watches for changes):
sbt "~fastLinkJS" -
Development server:
npm run dev
Vite simplifies the otherwise complex building process. Simply run the following command:
npm run buildThe built web application can now be found inside the dist subdirectory.
Scala.js compiles Scala code to JavaScript, allowing you to write type-safe frontend applications in Scala.
Key points:
- Write Scala, get optimized JavaScript output
- Strong typing catches errors at compile time
scalajs-domlibrary provides typed DOM APIs
Example:
import org.scalajs.dom
dom.console.log("Hello from Scala.js!")Laminar is a functional reactive UI library for building web interfaces.
Core concepts:
- Elements: Created with functions like
div(),button(),h1() - Modifiers: Add attributes/styles with
:=(static) or<--(reactive) - Signals: Reactive values that automatically update the UI
- EventStreams: Handle user interactions and events
Example:
val nameVar = Var("World")
div(
input(
placeholder := "Enter name",
onInput.mapToValue --> nameVar
),
h1(child.text <-- nameVar.signal.map(name => s"Hello, $name!"))
)Common patterns:
className := "my-class"- static CSS classchild <-- signal- reactive child elementonClick --> observer- event handlingchild.text <-- signal.map(...)- reactive text content
Waypoint provides type-safe, composable routing.
Key concepts:
- Routes defined as
Route.static(PageObject, root / "path" / endOfSegments) - Navigation with
router.navigateTo(PageObject) - Current page observed via
router.currentPageSignal - Browser history integration built-in
Example:
// In Router.scala
val homeRoute = Route.static(HomePage, root / endOfSegments)
// In components
a(
router.navigateTo(HomePage),
"Go Home"
)This codebase uses Scala 3 syntax:
- Top-level definitions: Functions and values without wrapping objects
- Enums and sealed hierarchies:
sealed abstract class Pagewith case objects - Indentation-based syntax: No braces required
@mainannotation: Simple entry points
-
Create
src/main/scala/pgkn/pages/NewPage.scala:package pgkn.pages import com.raquo.laminar.api.L.* import com.raquo.waypoint.Router import pgkn.components.{NavHeader, Footer} object NewPage: def apply(router: Router[pgkn.Page]): HtmlElement = mainTag( className := "new-page", NavHeader(router), div( h1("My New Page") ), Footer() )
-
Add to Router.scala:
// Add case object case object NewPagePage extends Page("PGKN - New Page") // Add route val newPageRoute = Route.static(NewPagePage, root / "new-page" / endOfSegments) // Add to routes list routes = List(homeRoute, kaptenAllocRoute, sigridRoute, newPageRoute) // Add to splitter .collectStatic(NewPagePage)(NewPage(RouterInstance))
-
Optionally add navigation link in NavHeader.scala
-
Create
styles/pages/new-page.cssand import instyles/pages/index.css
The application supports light and dark themes with automatic persistence and system preference detection.
ThemeService (src/main/scala/pgkn/services/ThemeService.scala):
- Manages theme state using Laminar's reactive
Var/Signalpattern - Exposes a
Signal[Theme]for components to reactively observe theme changes - Provides
toggle()method to switch between themes - Persists theme preference to localStorage
- Detects system color scheme preference on first visit
CSS Implementation:
- Theme-specific CSS variables are defined in
styles/themes/light.css: Uses:rootselector for default light theme (loads before JS)dark.css: Uses[data-theme="dark"]attribute selector
- ThemeService sets
data-themeattribute on<html>element to apply dark theme - Components use CSS variables (e.g.,
var(--color-bg)) that automatically update when theme changes