aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Stålnacke <joel@saker.fi>2025-08-02 18:25:14 +0300
committerJoel Stålnacke <joel@saker.fi>2025-08-02 18:25:14 +0300
commit6b46930cfd23c59a359460c84085e9333aea4521 (patch)
tree9d1d11838ee4e836c0338ef1ab338fe234fc7f4b
parent40525e4f0ff494a3b5bf9cebad5d00ac25a3d840 (diff)
Initial frontend
-rw-r--r--.gitignore3
-rw-r--r--TJLaskuri.Core/TJLaskuri.Core.fsproj5
-rw-r--r--TJLaskuri.Core/Views.fs88
-rw-r--r--TJLaskuri.Frontend/App.fs34
-rw-r--r--TJLaskuri.Frontend/index.html3
-rw-r--r--TJLaskuri.Frontend/vite.config.ts12
-rw-r--r--TJLaskuri.Web/Modules/Index.fs31
-rw-r--r--TJLaskuri.Web/Program.fs38
-rw-r--r--TJLaskuri.Web/TJLaskuri.Web.fsproj39
-rw-r--r--TJLaskuri.Web/wwwroot/styles.css72
10 files changed, 278 insertions, 47 deletions
diff --git a/.gitignore b/.gitignore
index 021778a..5f54bfb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
bin/
obj/
+core
# Fable
*.fs.js
@@ -7,3 +8,5 @@ fable_modules/
node_modules/
dist/
*.fable-temp.csproj
+
+TJLaskuri.Web/wwwroot/js/app.js
diff --git a/TJLaskuri.Core/TJLaskuri.Core.fsproj b/TJLaskuri.Core/TJLaskuri.Core.fsproj
index c3b606a..bc6029f 100644
--- a/TJLaskuri.Core/TJLaskuri.Core.fsproj
+++ b/TJLaskuri.Core/TJLaskuri.Core.fsproj
@@ -7,7 +7,12 @@
<ItemGroup>
<Compile Include="Types.fs" />
+ <Compile Include="Views.fs" />
<Compile Include="Library.fs" />
</ItemGroup>
+ <ItemGroup>
+ <PackageReference Include="Falco.Markup" Version="1.2.0" />
+ </ItemGroup>
+
</Project>
diff --git a/TJLaskuri.Core/Views.fs b/TJLaskuri.Core/Views.fs
new file mode 100644
index 0000000..939acec
--- /dev/null
+++ b/TJLaskuri.Core/Views.fs
@@ -0,0 +1,88 @@
+namespace TJLaskuri.Core.Views
+
+open System
+open Falco.Markup
+open Elem
+open Attr
+open Text
+
+module Document =
+ let view documentBody =
+ html [ lang "fi" ] [
+ head [] [
+ meta [ charset "utf-8" ]
+ meta [ name "viewport"; content "width=device-width" ]
+ Elem.title [] [ raw "TJ-laskuri" ]
+ script [ type' "module"; src "/js/app.js" ] []
+ link [ rel "stylesheet"; href "/styles.css" ]
+ ]
+ body [] [
+ documentBody
+ ]
+ ]
+
+module Main =
+ type Model = {
+ TimeLeft : TimeSpan
+ TimeCompleted : TimeSpan
+ }
+
+ let view model =
+ main [] [
+ div [ class' "counter" ] [
+ Elem.span [ class' "counter_title" ] [
+ raw "Tänään jäljellä"
+ ]
+ Elem.span [ class' "counter_value" ] [
+ enc (string <| Math.Ceiling model.TimeLeft.TotalDays)
+ ]
+ Elem.span [ class' "counter_mornings" ] [
+ raw " aamua"
+ ]
+ ]
+
+ div [ class' "progress-bar" ] [
+ let p = 100.0 * (1.0 - model.TimeLeft / (model.TimeLeft + model.TimeCompleted))
+ let cssWidth = sprintf "%s%%" (p.ToString("F2"))
+
+ div [
+ class' "progress-bar_value"
+ style <|
+ sprintf "background-color: green; width: %s"
+ cssWidth
+ ] [
+ span <| sprintf "%s %%" (p.ToString("F2"))
+ ]
+ ]
+
+ Elem.p [
+ style "text-align: center; font-size: 1.5rem;"
+ ] [
+ enc "2/25 347"
+ ]
+
+ // div [] [
+ // Elem.label [ for' "kontingent-select" ] [ raw "Saapumiserä:" ]
+ // select [ id "kontingent-select" ] [
+ // option [ value "1/25" ] [ enc "1/25" ]
+ // option [ value "2/25" ] [ enc "2/25" ]
+ // option [ value "1/26" ] [ enc "1/26" ]
+ // option [ value "2/26" ] [ enc "2/26" ]
+ // ]
+ // ]
+
+ // div [] [
+ // Elem.p [] [
+ // span "Tunteina"
+ // enc (string model.TimeLeft.TotalHours)
+ // ]
+ // Elem.p [] [
+ // span "Sekunteina"
+ // enc (string model.TimeLeft.TotalSeconds)
+ // ]
+ // Elem.p [] [
+ // span "Aamuja ohi "
+ // enc (string <| Math.Floor model.TimeCompleted.TotalDays)
+ // ]
+ // ]
+ ]
diff --git a/TJLaskuri.Frontend/App.fs b/TJLaskuri.Frontend/App.fs
index d19c455..c83ed6a 100644
--- a/TJLaskuri.Frontend/App.fs
+++ b/TJLaskuri.Frontend/App.fs
@@ -9,25 +9,25 @@ let k = kontingent 2 2025
let st = ThreeFourSeven
let currentTime =
- DateTime(2025, 7, 5)
+ // DateTime(2025, 7, 5)
// DateTime(2026, 6, 18)
// DateTime(2026, 6, 17)
- // DateTime.Today
+ DateTime.Today
// DateTime.Now
// Domain.getStartDate k |> Option.get
-currentTime
-|> Domain.getTimeLeft k st
-|> function
- | Some left ->
- let el = document.createElement "p"
- el.innerHTML <- sprintf "Tänään jäljellä:<br>%O" left.TotalDays
- document.body.appendChild(el) |> ignore
- | None ->
- printfn "Error: unknown kontingent"
-
-let startDate = Domain.getStartDate k |> Option.get
-let el = document.createElement "p"
-el.innerHTML <- sprintf "Aloituspäivä: %O<br>Aamuja ohi %d"
- startDate (Domain.getTimeCompleted k currentTime |> Option.get).Days
-document.body.appendChild(el) |> ignore
+// currentTime
+// |> Domain.getTimeLeft k st
+// |> function
+// | Some left ->
+// let el = document.createElement "p"
+// el.innerHTML <- sprintf "Tänään jäljellä:<br>%O<br>aamua" left.TotalDays
+// document.body.appendChild(el) |> ignore
+// | None ->
+// printfn "Error: unknown kontingent"
+//
+// let startDate = Domain.getStartDate k |> Option.get
+// let el = document.createElement "p"
+// el.innerHTML <- sprintf "Aloituspäivä: %O<br>Aamuja ohi %d"
+// startDate (Domain.getTimeCompleted k currentTime |> Option.get).Days
+// document.body.appendChild(el) |> ignore
diff --git a/TJLaskuri.Frontend/index.html b/TJLaskuri.Frontend/index.html
index 9fadce0..79b1c82 100644
--- a/TJLaskuri.Frontend/index.html
+++ b/TJLaskuri.Frontend/index.html
@@ -3,10 +3,9 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
- <title>TJ-laskuri</title>
+ <title>TJ-laskuri</title>
</head>
<body>
<script type="module" src="/App.fs.js"></script>
</body>
</html>
-
diff --git a/TJLaskuri.Frontend/vite.config.ts b/TJLaskuri.Frontend/vite.config.ts
index 37b081b..2dd7341 100644
--- a/TJLaskuri.Frontend/vite.config.ts
+++ b/TJLaskuri.Frontend/vite.config.ts
@@ -8,6 +8,18 @@ export default defineConfig({
ignored: [
"**/*.fs" // Don't watch F# files
]
+ },
+ },
+ build: {
+ rollupOptions: {
+ output: {
+ // Remove hash from output so we can copy the file
+ // to the content root of the web app
+ entryFileNames: `[name].js`,
+
+ // chunkFileNames: `[name].js`,
+ // assetFileNames: `[name].[ext]`
+ }
}
}
})
diff --git a/TJLaskuri.Web/Modules/Index.fs b/TJLaskuri.Web/Modules/Index.fs
new file mode 100644
index 0000000..7623779
--- /dev/null
+++ b/TJLaskuri.Web/Modules/Index.fs
@@ -0,0 +1,31 @@
+module TJLaskuri.Web.Modules.Index
+
+open System
+open Falco
+open TJLaskuri.Core
+
+[<AutoOpen>]
+module Views =
+ open TJLaskuri.Core.Views
+
+ type Model = {
+ Counter : Main.Model
+ }
+
+ let view model =
+ Main.view model.Counter
+ |> Document.view
+
+let get : HttpHandler =
+ fun ctx ->
+ let time = DateTime.Now
+ let kontingent = kontingent 2 2025
+ let timeLeft = Domain.getTimeLeft kontingent ThreeFourSeven time |> Option.get
+ let completed = Domain.getTimeCompleted kontingent time |> Option.get
+
+ view {
+ Counter = {
+ TimeLeft = timeLeft
+ TimeCompleted = completed
+ }
+ } |> Response.ofHtml <| ctx
diff --git a/TJLaskuri.Web/Program.fs b/TJLaskuri.Web/Program.fs
index fb776e0..831d8a8 100644
--- a/TJLaskuri.Web/Program.fs
+++ b/TJLaskuri.Web/Program.fs
@@ -1,15 +1,23 @@
-open System
-open Microsoft.AspNetCore.Builder
-open Microsoft.Extensions.Hosting
-
-[<EntryPoint>]
-let main args =
- let builder = WebApplication.CreateBuilder(args)
- let app = builder.Build()
-
- app.MapGet("/", Func<string>(fun () -> "Hello World!")) |> ignore
-
- app.Run()
-
- 0 // Exit code
-
+open Microsoft.AspNetCore.Builder
+open Microsoft.Extensions.Hosting
+open Falco
+open Falco.Routing
+open TJLaskuri.Web.Modules
+
+let endpoints = [
+ get "/" Index.get
+ ]
+
+[<EntryPoint>]
+let main args =
+ let builder = WebApplication.CreateBuilder(args)
+ let app = builder.Build()
+
+ app.MapStaticAssets() |> ignore
+
+ app
+ .UseRouting()
+ .UseFalco(endpoints)
+ .Run()
+
+ 0 // Exit code
diff --git a/TJLaskuri.Web/TJLaskuri.Web.fsproj b/TJLaskuri.Web/TJLaskuri.Web.fsproj
index 6ddf66a..6cd0654 100644
--- a/TJLaskuri.Web/TJLaskuri.Web.fsproj
+++ b/TJLaskuri.Web/TJLaskuri.Web.fsproj
@@ -1,15 +1,28 @@
-<Project Sdk="Microsoft.NET.Sdk.Web">
-
- <PropertyGroup>
- <TargetFramework>net9.0</TargetFramework>
- </PropertyGroup>
-
- <ItemGroup>
- <Compile Include="Program.fs" />
- </ItemGroup>
-
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>net9.0</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="Modules/Index.fs" />
+ <Compile Include="Program.fs" />
+ </ItemGroup>
+
<ItemGroup>
<ProjectReference Include="..\TJLaskuri.Core\TJLaskuri.Core.fsproj" />
- </ItemGroup>
-
-</Project>
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Falco" Version="5.0.2" />
+ </ItemGroup>
+
+ <Target Name="CopyStaticAssets" BeforeTargets="Build">
+ <Message Text="Copying static assets" />
+ <Copy
+ SourceFiles="../TJLaskuri.Frontend/dist/index.js"
+ DestinationFiles="wwwroot/js/app.js"
+ />
+ </Target>
+
+</Project>
diff --git a/TJLaskuri.Web/wwwroot/styles.css b/TJLaskuri.Web/wwwroot/styles.css
new file mode 100644
index 0000000..66a6ef5
--- /dev/null
+++ b/TJLaskuri.Web/wwwroot/styles.css
@@ -0,0 +1,72 @@
+* {
+ box-sizing: border-box;
+ max-width: none;
+}
+
+html {
+ font-size: 100%;
+}
+
+@font-face {
+ font-family: 'Cooper Black';
+ font-weight: normal;
+ font-style: normal;
+ font-display: swap;
+ src:
+ local('Cooper Black'),
+ url('https://cdn.jsdelivr.net/npm/fonts-archive-cooper-black/CooperBlack-Regular.woff2') format('woff2'),
+ url('https://cdn.jsdelivr.net/npm/fonts-archive-cooper-black/CooperBlack-Regular.woff') format('woff'),
+ url('https://cdn.jsdelivr.net/npm/fonts-archive-cooper-black/CooperBlack-Regular.otf') format('opentype'),
+ url('https://cdn.jsdelivr.net/npm/fonts-archive-cooper-black/CooperBlack-Regular.ttf') format('truetype');
+}
+
+body {
+ font-family: "Cooper Black", serif;
+ font-weight: bold;
+ max-width: 50rem;
+ margin: 0 auto;
+ padding: 1rem;
+}
+
+.counter {
+ padding: 2rem;
+ text-align: center;
+ font-size: 2.5rem;
+ font-weight: 900;
+}
+
+.counter_title {
+ display: block;
+ font-size: 1.2em;
+}
+
+.counter_value {
+ display: block;
+ margin: 0;
+ font-weight: 900;
+ font-size: 2.5em;
+}
+
+.counter_mornings {
+ display: block;
+}
+
+.progress-bar {
+ height: 25px;
+ border-radius: 5px;
+ border: 1px solid black;
+ background-color: lightgrey;
+}
+
+.progress-bar_value {
+ color: white;
+ height: 100%;
+ display: flex;
+ padding: 0 10px;
+ justify-content: flex-end;
+ align-items: center;
+ white-space: nowrap;
+ text-align: center;
+ border-radius: 5px;
+ min-width: 0;
+}