Initial
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
/packages/
|
||||||
|
riderModule.iml
|
||||||
|
/_ReSharper.Caches/
|
||||||
16
AirportAlphabetGame.sln
Normal file
16
AirportAlphabetGame.sln
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "AirportAlphabetGame", "AirportAlphabetGame\AirportAlphabetGame.fsproj", "{80323801-F224-4E26-B6A0-5232AED36275}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{80323801-F224-4E26-B6A0-5232AED36275}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{80323801-F224-4E26-B6A0-5232AED36275}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{80323801-F224-4E26-B6A0-5232AED36275}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{80323801-F224-4E26-B6A0-5232AED36275}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
26
AirportAlphabetGame/AirportAlphabetGame.fsproj
Normal file
26
AirportAlphabetGame/AirportAlphabetGame.fsproj
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Types.fs" />
|
||||||
|
<Compile Include="Svg.fs" />
|
||||||
|
<Compile Include="Htmx.fs" />
|
||||||
|
<Compile Include="View.fs" />
|
||||||
|
<Compile Include="Controller.fs" />
|
||||||
|
<Compile Include="Router.fs" />
|
||||||
|
<Compile Include="Program.fs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Saturn" Version="0.16.1" />
|
||||||
|
<PackageReference Include="Thoth.Json.Net" Version="11.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="sass\core.sass" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
106
AirportAlphabetGame/Controller.fs
Normal file
106
AirportAlphabetGame/Controller.fs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
module Controller
|
||||||
|
|
||||||
|
open System.Net.Http
|
||||||
|
open System.Text.RegularExpressions
|
||||||
|
open Giraffe.ViewEngine.HtmlElements
|
||||||
|
open Types
|
||||||
|
open System.Text
|
||||||
|
open Thoth.Json.Net
|
||||||
|
|
||||||
|
|
||||||
|
let parseUsername (username: string) =
|
||||||
|
let pattern = @"https?://(?:www\.)?my\.flightradar24\.com/([^/?#]+)"
|
||||||
|
let matches = Regex.Match(username, pattern)
|
||||||
|
|
||||||
|
if matches.Success then matches.Groups.[1].Value
|
||||||
|
else username
|
||||||
|
|
||||||
|
let getJsonResult result =
|
||||||
|
match result with
|
||||||
|
| Error _ -> failwith "Invalid Json"
|
||||||
|
| Ok finalResult -> finalResult
|
||||||
|
|
||||||
|
|
||||||
|
let makePostRequest<'x> (url: string) payload =
|
||||||
|
async {
|
||||||
|
use httpClient = new HttpClient()
|
||||||
|
let content = new StringContent(payload, Encoding.UTF8, "application/x-www-form-urlencoded")
|
||||||
|
let request = new HttpRequestMessage(HttpMethod.Post, url)
|
||||||
|
request.Content <- content
|
||||||
|
let! response = httpClient.SendAsync(request) |> Async.AwaitTask
|
||||||
|
let! responseContent = response.Content.ReadAsStringAsync() |> Async.AwaitTask
|
||||||
|
return responseContent
|
||||||
|
}
|
||||||
|
|> Async.RunSynchronously
|
||||||
|
|> Decode.Auto.fromString<'x>
|
||||||
|
|> getJsonResult
|
||||||
|
|
||||||
|
|
||||||
|
let decodeResponseData (data: AirportResponseData) =
|
||||||
|
data
|
||||||
|
|> Array.map (fun airport -> {Code=string airport[0];City=string airport[1]})
|
||||||
|
|
||||||
|
let groupAirportsByLetter (airports: Airport[]) =
|
||||||
|
airports
|
||||||
|
|> Array.groupBy (_.Code[0])
|
||||||
|
|> Array.map snd
|
||||||
|
|
||||||
|
let getAirportAbbrTags airportGroup =
|
||||||
|
airportGroup
|
||||||
|
|> Array.distinct
|
||||||
|
|> Array.sortBy (_.Code)
|
||||||
|
|> Array.map View.airportAbbr
|
||||||
|
|
||||||
|
|
||||||
|
let processAirports (alphabet: char[]) (allAirports: Airport[]) =
|
||||||
|
|
||||||
|
let intersperseCommas (nodes: XmlNode seq) =
|
||||||
|
nodes
|
||||||
|
|> Seq.mapi (fun i node -> if i < Seq.length nodes - 1 then [node; View.comma] else [node])
|
||||||
|
|> Seq.concat
|
||||||
|
|> Seq.toArray
|
||||||
|
|
||||||
|
// Group airports by the first letter of their code
|
||||||
|
let groupedAirports =
|
||||||
|
allAirports
|
||||||
|
|> Seq.groupBy (fun airport -> airport.Code.[0])
|
||||||
|
|> Seq.map (fun (key, group) ->
|
||||||
|
let sortedGroup = group |> Seq.sortBy (fun airport -> airport.Code)
|
||||||
|
key, (sortedGroup |> Seq.map View.airportAbbr |> intersperseCommas))
|
||||||
|
|> dict
|
||||||
|
|
||||||
|
|
||||||
|
let dictionary =
|
||||||
|
alphabet
|
||||||
|
|> Array.map (fun letter ->
|
||||||
|
let key = letter.ToString().ToUpper()
|
||||||
|
match groupedAirports.TryGetValue(letter) with
|
||||||
|
| (true, value) -> key, value
|
||||||
|
| _ -> key, [||])
|
||||||
|
|
||||||
|
dictionary
|
||||||
|
let RenderAirportList (user: usernameQuery) =
|
||||||
|
let username = parseUsername user.fr24user
|
||||||
|
let alphabet = [|'A'..'Z'|]
|
||||||
|
let airports =
|
||||||
|
$"username={username}&listType=airports&order=no&limit=0"
|
||||||
|
|> makePostRequest<AirportResponseData> "https://my.flightradar24.com/public-scripts/profileToplist"
|
||||||
|
|> decodeResponseData
|
||||||
|
|> processAirports alphabet
|
||||||
|
|
||||||
|
let numberOfAirportsNotFlown =
|
||||||
|
airports
|
||||||
|
|> Array.filter(fun (_, nodes) -> Array.isEmpty nodes)
|
||||||
|
|> Array.length
|
||||||
|
|
||||||
|
let percentageOfLettersWithNoAirports = (float numberOfAirportsNotFlown / float alphabet.Length) * 100.0
|
||||||
|
let message = $"{username} has flown {System.Math.Round(100. - percentageOfLettersWithNoAirports, 1)}%% of the alphabet!"
|
||||||
|
|
||||||
|
|
||||||
|
airports
|
||||||
|
|> Array.map (fun (letter, nodes) -> View.tableRow letter nodes)
|
||||||
|
|> View.table message username
|
||||||
|
|
||||||
|
let RenderPageWithUser (username: string) =
|
||||||
|
let airportList = RenderAirportList {fr24user = username}
|
||||||
|
View.index [|airportList|]
|
||||||
12
AirportAlphabetGame/Htmx.fs
Normal file
12
AirportAlphabetGame/Htmx.fs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module Htmx
|
||||||
|
|
||||||
|
open Giraffe.ViewEngine
|
||||||
|
|
||||||
|
let _hxGet = attr "data-hx-get"
|
||||||
|
let _hxPost = attr "data-hx-post"
|
||||||
|
let _hxTrigger = attr "data-hx-trigger"
|
||||||
|
let _hxTarget = attr "data-hx-target"
|
||||||
|
let _hxExt = attr "data-hx-ext"
|
||||||
|
let _hxSwap = attr "data-hx-swap"
|
||||||
|
let _hxReplaceUrl = attr "data-hx-replace-url"
|
||||||
|
let _hxValidate = attr "data-hx-validate"
|
||||||
37
AirportAlphabetGame/Program.fs
Normal file
37
AirportAlphabetGame/Program.fs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
open Microsoft.AspNetCore.Mvc
|
||||||
|
open Microsoft.Extensions.DependencyInjection
|
||||||
|
open Microsoft.AspNetCore.Http
|
||||||
|
open Saturn
|
||||||
|
open Giraffe
|
||||||
|
open Types
|
||||||
|
|
||||||
|
module Program =
|
||||||
|
|
||||||
|
let pipeline = pipeline {
|
||||||
|
use_warbler
|
||||||
|
}
|
||||||
|
|
||||||
|
let router = router {
|
||||||
|
pipe_through pipeline
|
||||||
|
not_found_handler (setStatusCode 404 >=> text "404")
|
||||||
|
get "/" ( (View.index [||]) |> htmlView)
|
||||||
|
getf "/%s" (fun username -> htmlView(Controller.RenderPageWithUser username))
|
||||||
|
post "/search" (bindJson<usernameQuery> (fun username ->
|
||||||
|
fun next ctx ->
|
||||||
|
ctx.Response.Headers.Add("HX-Replace-Url", Controller.parseUsername username.fr24user)
|
||||||
|
htmlView (Controller.RenderAirportList username) next ctx
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
let ServiceConfig (services: IServiceCollection) = services.AddHttpContextAccessor()
|
||||||
|
let ipAddress = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName()).AddressList[2];
|
||||||
|
let app =
|
||||||
|
application {
|
||||||
|
use_mime_types [(".woff", "application/font-woff")]
|
||||||
|
use_static "public"
|
||||||
|
use_router router
|
||||||
|
service_config ServiceConfig
|
||||||
|
url "http://*:5001"
|
||||||
|
}
|
||||||
|
|
||||||
|
run app
|
||||||
38
AirportAlphabetGame/Properties/launchSettings.json
Normal file
38
AirportAlphabetGame/Properties/launchSettings.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:4828",
|
||||||
|
"sslPort": 44396
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "http://localhost:5001",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "https://localhost:7051;http://localhost:5001",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
93
AirportAlphabetGame/Router.fs
Normal file
93
AirportAlphabetGame/Router.fs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
module Router
|
||||||
|
|
||||||
|
open System
|
||||||
|
open System.Linq
|
||||||
|
open System.Threading.Tasks
|
||||||
|
open Microsoft.AspNetCore.Http
|
||||||
|
open Microsoft.AspNetCore.Builder
|
||||||
|
open Microsoft.Extensions.Hosting
|
||||||
|
|
||||||
|
let inline (~%) (x: ^A) : ^B = (^B : (static member From: ^A -> ^B) x)
|
||||||
|
|
||||||
|
type RouteMethod = Get | Post
|
||||||
|
type Static<'x> =
|
||||||
|
| HtmlDocument of (unit -> Giraffe.ViewEngine.HtmlElements.XmlNode)
|
||||||
|
| HtmlElement of (unit -> Giraffe.ViewEngine.HtmlElements.XmlNode)
|
||||||
|
| HtmlElements of (unit -> Giraffe.ViewEngine.HtmlElements.XmlNode list)
|
||||||
|
| HtmlString of (unit -> string)
|
||||||
|
| Text of (unit -> string)
|
||||||
|
| Json of (unit -> 'x)
|
||||||
|
|
||||||
|
type Dynamic<'x, 'y> =
|
||||||
|
| HtmlDocument of ('x -> Giraffe.ViewEngine.HtmlElements.XmlNode)
|
||||||
|
| HtmlElement of ('x -> Giraffe.ViewEngine.HtmlElements.XmlNode)
|
||||||
|
| HtmlElements of ('x -> Giraffe.ViewEngine.HtmlElements.XmlNode list)
|
||||||
|
| HtmlString of ('x -> string)
|
||||||
|
| Text of ('x -> string)
|
||||||
|
| Json of ('x -> 'y)
|
||||||
|
|
||||||
|
|
||||||
|
let private GetMimeTypeFromStaticResponseFunction (func: Static<'x>) =
|
||||||
|
match func with
|
||||||
|
| Static.HtmlDocument _
|
||||||
|
| Static.HtmlElements _
|
||||||
|
| Static.HtmlElement _
|
||||||
|
| Static.HtmlString _ -> "text/html"
|
||||||
|
| Static.Json _ -> "application/json"
|
||||||
|
| Static.Text _ -> "text/plain"
|
||||||
|
|
||||||
|
let private GetMimeTypeFromDynamicResponseFunction (func: Dynamic<'x, 'y>) =
|
||||||
|
match func with
|
||||||
|
| Dynamic.HtmlDocument _
|
||||||
|
| Dynamic.HtmlElements _
|
||||||
|
| Dynamic.HtmlElement _
|
||||||
|
| Dynamic.HtmlString _ -> "text/html"
|
||||||
|
| Dynamic.Json _ -> "application/json"
|
||||||
|
| Dynamic.Text _ -> "text/plain"
|
||||||
|
|
||||||
|
let StaticView (func: unit -> Giraffe.ViewEngine.HtmlElements.XmlNode) = Static.HtmlDocument func
|
||||||
|
|
||||||
|
|
||||||
|
let BuildApp (args: string[]) =
|
||||||
|
WebApplication
|
||||||
|
.CreateBuilder(args)
|
||||||
|
.Build()
|
||||||
|
|
||||||
|
|
||||||
|
let AddStaticRoute (route: string) (func: Static<'x>) (app: WebApplication) =
|
||||||
|
let htmlResponseFunc (context: HttpContext) =
|
||||||
|
let htmlContent =
|
||||||
|
match func with
|
||||||
|
| Static.HtmlDocument view -> view () |> Giraffe.ViewEngine.RenderView.AsString.htmlDocument
|
||||||
|
| Static.HtmlElement view -> view () |> Giraffe.ViewEngine.RenderView.AsString.htmlNode
|
||||||
|
| Static.HtmlElements view -> view () |> Giraffe.ViewEngine.RenderView.AsString.htmlNodes
|
||||||
|
| Static.HtmlString view -> view ()
|
||||||
|
| Static.Text view -> view ()
|
||||||
|
| Static.Json view -> view () |> Thoth.Json.Net.Encode.Auto.toString
|
||||||
|
context.Response.ContentType <- GetMimeTypeFromStaticResponseFunction func
|
||||||
|
context.Response.WriteAsync(htmlContent)
|
||||||
|
|
||||||
|
app.MapGet(route, Func<HttpContext, Task>(htmlResponseFunc))
|
||||||
|
|> ignore
|
||||||
|
app
|
||||||
|
|
||||||
|
let AddDynamicRoute (route: string) (func: Dynamic<'x, 'y>) (app: WebApplication) =
|
||||||
|
let dynamicResponseFunc (context: HttpContext) =
|
||||||
|
let param = context.Request.RouteValues.Values.First() :?> 'x
|
||||||
|
let content =
|
||||||
|
match func with
|
||||||
|
| Dynamic.HtmlDocument view -> view param |> Giraffe.ViewEngine.RenderView.AsString.htmlDocument
|
||||||
|
| Dynamic.HtmlElement view -> view param |> Giraffe.ViewEngine.RenderView.AsString.htmlNode
|
||||||
|
| Dynamic.HtmlElements view -> view param |> Giraffe.ViewEngine.RenderView.AsString.htmlNodes
|
||||||
|
| Dynamic.HtmlString view -> view param
|
||||||
|
| Dynamic.Text view -> view param
|
||||||
|
| Dynamic.Json view -> view param |> Thoth.Json.Net.Encode.Auto.toString
|
||||||
|
context.Response.ContentType <- GetMimeTypeFromDynamicResponseFunction func
|
||||||
|
context.Response.WriteAsync(content)
|
||||||
|
|
||||||
|
app.MapGet(route, dynamicResponseFunc) |> ignore
|
||||||
|
app
|
||||||
|
|
||||||
|
let Run (app: WebApplication) =
|
||||||
|
app.Run()
|
||||||
|
0
|
||||||
15
AirportAlphabetGame/Svg.fs
Normal file
15
AirportAlphabetGame/Svg.fs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
module Svg
|
||||||
|
open Giraffe.ViewEngine
|
||||||
|
|
||||||
|
let svg = tag "svg"
|
||||||
|
let rect = tag "rect"
|
||||||
|
let animate = tag "animate"
|
||||||
|
|
||||||
|
let _attributeName = attr "attributeName"
|
||||||
|
let _attributeType = attr "attributeType"
|
||||||
|
|
||||||
|
let _values = attr "values"
|
||||||
|
let _dur = attr "dur"
|
||||||
|
let _fill = attr "fill"
|
||||||
|
let _begin = attr "begin"
|
||||||
|
let _repeatCount = attr "repeatCount"
|
||||||
12
AirportAlphabetGame/Types.fs
Normal file
12
AirportAlphabetGame/Types.fs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module Types
|
||||||
|
|
||||||
|
type usernameQuery = {
|
||||||
|
fr24user: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AirportResponseData = obj[][]
|
||||||
|
|
||||||
|
type Airport = {
|
||||||
|
Code: string
|
||||||
|
City: string
|
||||||
|
}
|
||||||
84
AirportAlphabetGame/View.fs
Normal file
84
AirportAlphabetGame/View.fs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
module View
|
||||||
|
|
||||||
|
open System
|
||||||
|
open Htmx
|
||||||
|
open Giraffe.ViewEngine
|
||||||
|
open Giraffe.Core
|
||||||
|
open Types
|
||||||
|
|
||||||
|
let index content =
|
||||||
|
let hide =
|
||||||
|
match content |> Seq.length with
|
||||||
|
| 0 -> "hide"
|
||||||
|
| _ -> ""
|
||||||
|
|
||||||
|
html [] [
|
||||||
|
head [] [
|
||||||
|
meta [_name "viewport"; _content "width=device-width"]
|
||||||
|
title [] [str "Have you flown the alphabet?"]
|
||||||
|
link [_rel "stylesheet" ; _href "/styles/core.css"]
|
||||||
|
script [_src "/scripts/htmx.min.js"] []
|
||||||
|
script [_src "/scripts/json-enc.js"] []
|
||||||
|
script [_src "/scripts/index.js"] []
|
||||||
|
]
|
||||||
|
body [] [
|
||||||
|
div [_id "pageContainer"] [
|
||||||
|
section [_id "input"] [
|
||||||
|
article [] [
|
||||||
|
h1 [] [str "Have you flown the alphabet?"]
|
||||||
|
form [
|
||||||
|
_id "username"
|
||||||
|
_hxTrigger "submit"
|
||||||
|
_hxExt "json-enc"
|
||||||
|
_hxPost "/search"
|
||||||
|
_hxTarget "#results"
|
||||||
|
_hxSwap "innerHTML show:top"
|
||||||
|
] [
|
||||||
|
input [_type "text"; _name "fr24user"; _autocomplete "off"; _placeholder "Enter your MyFlightRadar24 username..."; _required; _hxValidate "true"; _pattern ".{4,}"]
|
||||||
|
button [_type "submit";] [str "Let's find out!"]
|
||||||
|
Svg.svg [_width "200"; _height "30"] [
|
||||||
|
Svg.rect [_width "200"; _height "30"; Svg._fill "lightgray"] []
|
||||||
|
Svg.rect [_width "50"; _height "30"; Svg._fill "white"] [
|
||||||
|
Svg.animate [Svg._attributeName "x"; Svg._attributeType "XML"; Svg._values "0;150;0"; Svg._dur "2s"; Svg._begin "0s"; Svg._repeatCount "indefinite"] []
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
section [_id "results"; _class hide; attr "hx-on::after-swap" "document.getElementById('results').style.display='flex'"] [
|
||||||
|
yield! content
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
let airportAbbr airport = abbr [_title airport.City] [str airport.Code]
|
||||||
|
let comma = str ", "
|
||||||
|
|
||||||
|
let tableRow (letter: string ) (codes: XmlNode[]) =
|
||||||
|
let className =
|
||||||
|
match codes.Length with
|
||||||
|
| 0 -> "not-flown"
|
||||||
|
| _ -> "flown"
|
||||||
|
|
||||||
|
tr [_class className] [
|
||||||
|
th [] [str letter]
|
||||||
|
td [] [
|
||||||
|
yield! codes
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
let table (message) (user: string) (rows: XmlNode[]) =
|
||||||
|
article [] [
|
||||||
|
h1 [] [str message]
|
||||||
|
table [] [
|
||||||
|
tr [] [
|
||||||
|
th [_colspan "2"] [str $"{user}'s Airports Flown"]
|
||||||
|
]
|
||||||
|
yield! rows
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
let name (name:usernameQuery) : HttpHandler = h1 [] [str name.fr24user] |> htmlView
|
||||||
|
let error = b [] [str "Error"]
|
||||||
8
AirportAlphabetGame/appsettings.Development.json
Normal file
8
AirportAlphabetGame/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
AirportAlphabetGame/appsettings.json
Normal file
9
AirportAlphabetGame/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
1
AirportAlphabetGame/public/scripts/htmx.min.js
vendored
Normal file
1
AirportAlphabetGame/public/scripts/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
AirportAlphabetGame/public/scripts/index.js
Normal file
1
AirportAlphabetGame/public/scripts/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", (event) => document.getElementById('results').scrollIntoView());
|
||||||
12
AirportAlphabetGame/public/scripts/json-enc.js
Normal file
12
AirportAlphabetGame/public/scripts/json-enc.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
htmx.defineExtension('json-enc', {
|
||||||
|
onEvent: function (name, evt) {
|
||||||
|
if (name === "htmx:configRequest") {
|
||||||
|
evt.detail.headers['Content-Type'] = "application/json";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
encodeParameters : function(xhr, parameters, elt) {
|
||||||
|
xhr.overrideMimeType('text/json');
|
||||||
|
return (JSON.stringify(parameters));
|
||||||
|
}
|
||||||
|
});
|
||||||
198
AirportAlphabetGame/public/styles/core.css
Normal file
198
AirportAlphabetGame/public/styles/core.css
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
font-family: manrope, sans-serif;
|
||||||
|
overscroll-behavior: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: normal;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pageContainer::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pageContainer {
|
||||||
|
height: 100dvh;
|
||||||
|
width: 100dvw;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
scroll-snap-type: y mandatory;
|
||||||
|
white-space: nowrap;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
#pageContainer abbr {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#pageContainer section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100dvh;
|
||||||
|
width: 100dvw;
|
||||||
|
max-width: 100%;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
}
|
||||||
|
#pageContainer section.hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#pageContainer section#input {
|
||||||
|
background: lightcoral;
|
||||||
|
}
|
||||||
|
#pageContainer section#input article {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 25%;
|
||||||
|
}
|
||||||
|
#pageContainer section#input article h1 {
|
||||||
|
flex-basis: 75%;
|
||||||
|
color: white;
|
||||||
|
font-size: 5em;
|
||||||
|
}
|
||||||
|
#pageContainer section#input article form {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-basis: 25%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
#pageContainer section#input article form input {
|
||||||
|
flex-basis: 75%;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 2em;
|
||||||
|
padding-left: 1em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
#pageContainer section#input article form button {
|
||||||
|
flex-basis: 25%;
|
||||||
|
font-size: 2em;
|
||||||
|
height: 100%;
|
||||||
|
color: white;
|
||||||
|
background: grey;
|
||||||
|
}
|
||||||
|
#pageContainer section#input article form svg {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
#pageContainer section#input article {
|
||||||
|
gap: 2em;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
#pageContainer section#input article h1 {
|
||||||
|
font-size: 4em;
|
||||||
|
}
|
||||||
|
#pageContainer section#input article form {
|
||||||
|
flex-direction: column;
|
||||||
|
flex-basis: 60%;
|
||||||
|
width: 90%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
#pageContainer section#input article form input, #pageContainer section#input article form button {
|
||||||
|
flex-basis: 25%;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#pageContainer section#input article form.htmx-request input, #pageContainer section#input article form.htmx-request button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#pageContainer section#input article form.htmx-request svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
#pageContainer section#results {
|
||||||
|
background: lightblue;
|
||||||
|
min-height: 100dvh;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
#pageContainer section#results h1 {
|
||||||
|
color: white;
|
||||||
|
font-size: 5em;
|
||||||
|
}
|
||||||
|
#pageContainer section#results article {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
#pageContainer section#results article table {
|
||||||
|
border: solid 2px black;
|
||||||
|
background: white;
|
||||||
|
border-collapse: collapse;
|
||||||
|
min-width: 50%;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
#pageContainer section#results article table tr th:first-child,
|
||||||
|
#pageContainer section#results article table tr td:first-child {
|
||||||
|
width: 10%;
|
||||||
|
min-width: 10%;
|
||||||
|
max-width: 10%;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
#pageContainer section#results article table th {
|
||||||
|
background-color: gray;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
#pageContainer section#results article table td, #pageContainer section#results article table th {
|
||||||
|
padding: 0.3em;
|
||||||
|
}
|
||||||
|
#pageContainer section#results article table tr.flown th {
|
||||||
|
border-right: 1px solid gray;
|
||||||
|
}
|
||||||
|
#pageContainer section#results article table tr.flown th, #pageContainer section#results article table tr.flown td {
|
||||||
|
background-color: lightgreen;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
#pageContainer section#results article table tr.not-flown th {
|
||||||
|
border-right: 1px solid gray;
|
||||||
|
}
|
||||||
|
#pageContainer section#results article table tr.not-flown th, #pageContainer section#results article table tr.not-flown td {
|
||||||
|
background-color: lightcoral;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
#pageContainer section#results article {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
#pageContainer section#results article h1 {
|
||||||
|
width: 90%;
|
||||||
|
flex-basis: 25%;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
#pageContainer section#results article table {
|
||||||
|
width: 100dvw;
|
||||||
|
}
|
||||||
|
#pageContainer section#results article table tr th:first-child,
|
||||||
|
#pageContainer section#results article table tr td:first-child {
|
||||||
|
width: 10dvw;
|
||||||
|
min-width: 10dvw;
|
||||||
|
max-width: 10dvw;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
#pageContainer section#results article table tr th:nth-child(2),
|
||||||
|
#pageContainer section#results article table tr td:nth-child(2) {
|
||||||
|
width: 90dvw;
|
||||||
|
min-width: 90dvw;
|
||||||
|
max-width: 90dvw;
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*# sourceMappingURL=core.css.map */
|
||||||
1
AirportAlphabetGame/public/styles/core.css.map
Normal file
1
AirportAlphabetGame/public/styles/core.css.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"sourceRoot":"","sources":["../../sass/core.sass"],"names":[],"mappings":"AAAA;EACI;EACA;EACA;EACA;EACA;EACA;;;AACJ;EACI;;;AAeJ;EARI;EACA;EACA;EAQA;EACA;EACA;;;AAEJ;EACI;EACA;;;AAEJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAEJ;EA/BA;EACA;EACA;EA+BI;EACA;EACA;EACA;;AAEJ;EACI;;AAGJ;EACI;;AAEA;EA7CJ;EACA;EACA;EAIA;EAyCQ;;AAEA;EACI;EACA;EACA;;AAEJ;EAtDR;EACA;EACA;EAsDY;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;EACA;;AAEJ;EACI;;AA9EhB;EAiDI;IAiCQ;IACA;;EAEA;IACI;;EAEJ;IACI;IACA;IACA;IACA;;EAEA;IACI;IACA;IACA;;;AAKR;EACI;;AAEJ;EACI;;AAEhB;EACI;EACA;EACA;;AAEA;EACI;EACA;;AAGJ;EAlHJ;EACA;EACA;EAIA;EA8GQ;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;AAAA;EAEI;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEJ;EACI;;AAGA;EACI;;AAEJ;EACI;EACA;;AAGJ;EACI;;AACJ;EACI;EACA;;AA7JpB;EAsHI;IAyCQ;;EACA;IACI;IACA;IACA;;EAEJ;IACI;;EAEA;AAAA;IAEI;IACA;IACA;IACA;;EAEJ;AAAA;IAEI;IACA;IACA;IACA;IACA","file":"core.css"}
|
||||||
193
AirportAlphabetGame/sass/core.sass
Normal file
193
AirportAlphabetGame/sass/core.sass
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
*
|
||||||
|
margin: 0
|
||||||
|
padding: 0
|
||||||
|
box-sizing: border-box
|
||||||
|
scroll-behavior: smooth
|
||||||
|
font-family: manrope, sans-serif
|
||||||
|
overscroll-behavior: none
|
||||||
|
a
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
|
@mixin mobile
|
||||||
|
@media screen and (max-width: 900px)
|
||||||
|
@content
|
||||||
|
|
||||||
|
@mixin flex
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
|
||||||
|
@mixin flex-column
|
||||||
|
@include flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
h1
|
||||||
|
@include flex
|
||||||
|
word-wrap: break-word
|
||||||
|
white-space: normal
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
#pageContainer::-webkit-scrollbar
|
||||||
|
width: 0
|
||||||
|
height: 0
|
||||||
|
|
||||||
|
#pageContainer
|
||||||
|
height: 100dvh
|
||||||
|
width: 100dvw
|
||||||
|
max-width: 100%
|
||||||
|
overflow-y: auto
|
||||||
|
scroll-snap-type: y mandatory
|
||||||
|
white-space: nowrap
|
||||||
|
scrollbar-width: none /* Firefox */
|
||||||
|
-ms-overflow-style: none /* Internet Explorer 10+ */
|
||||||
|
|
||||||
|
abbr
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
|
section
|
||||||
|
@include flex
|
||||||
|
height: 100dvh
|
||||||
|
width: 100dvw
|
||||||
|
max-width: 100%
|
||||||
|
scroll-snap-align: start
|
||||||
|
|
||||||
|
section.hide
|
||||||
|
display: none
|
||||||
|
|
||||||
|
|
||||||
|
section#input
|
||||||
|
background: lightcoral
|
||||||
|
|
||||||
|
article
|
||||||
|
@include flex-column
|
||||||
|
height: 25%
|
||||||
|
|
||||||
|
h1
|
||||||
|
flex-basis: 75%
|
||||||
|
color: white
|
||||||
|
font-size: 5em
|
||||||
|
|
||||||
|
form
|
||||||
|
@include flex
|
||||||
|
flex-basis: 25%
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
input
|
||||||
|
flex-basis: 75%
|
||||||
|
height: 100%
|
||||||
|
font-size: 2em
|
||||||
|
padding-left: 1em
|
||||||
|
color: #666
|
||||||
|
|
||||||
|
button
|
||||||
|
flex-basis: 25%
|
||||||
|
font-size: 2em
|
||||||
|
height: 100%
|
||||||
|
color: white
|
||||||
|
background: grey
|
||||||
|
|
||||||
|
svg
|
||||||
|
display: none
|
||||||
|
|
||||||
|
|
||||||
|
@include mobile
|
||||||
|
gap: 2em
|
||||||
|
height: 100%
|
||||||
|
|
||||||
|
h1
|
||||||
|
font-size: 4em
|
||||||
|
|
||||||
|
form
|
||||||
|
flex-direction: column
|
||||||
|
flex-basis: 60%
|
||||||
|
width: 90%
|
||||||
|
justify-content: flex-start
|
||||||
|
|
||||||
|
input, button
|
||||||
|
flex-basis: 25%
|
||||||
|
width: 100%
|
||||||
|
font-size: 1em
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
form.htmx-request
|
||||||
|
input, button
|
||||||
|
display: none
|
||||||
|
|
||||||
|
svg
|
||||||
|
display: block
|
||||||
|
|
||||||
|
section#results
|
||||||
|
background: lightblue
|
||||||
|
min-height: 100dvh
|
||||||
|
height: auto
|
||||||
|
|
||||||
|
h1
|
||||||
|
color: white
|
||||||
|
font-size: 5em
|
||||||
|
|
||||||
|
|
||||||
|
article
|
||||||
|
@include flex-column
|
||||||
|
gap: 1em
|
||||||
|
|
||||||
|
table
|
||||||
|
border: solid 2px black
|
||||||
|
background: white
|
||||||
|
border-collapse: collapse
|
||||||
|
min-width: 50%
|
||||||
|
max-width: 90%
|
||||||
|
|
||||||
|
tr th:first-child,
|
||||||
|
tr td:first-child
|
||||||
|
width: 10%
|
||||||
|
min-width: 10%
|
||||||
|
max-width: 10%
|
||||||
|
word-break: break-all
|
||||||
|
|
||||||
|
|
||||||
|
th
|
||||||
|
background-color: gray
|
||||||
|
color: white
|
||||||
|
|
||||||
|
td, th
|
||||||
|
padding: 0.3em
|
||||||
|
|
||||||
|
tr.flown
|
||||||
|
th
|
||||||
|
border-right: 1px solid gray
|
||||||
|
|
||||||
|
th, td
|
||||||
|
background-color: lightgreen
|
||||||
|
color: black
|
||||||
|
|
||||||
|
tr.not-flown
|
||||||
|
th
|
||||||
|
border-right: 1px solid gray
|
||||||
|
th, td
|
||||||
|
background-color: lightcoral
|
||||||
|
color: black
|
||||||
|
@include mobile
|
||||||
|
min-height: 100%
|
||||||
|
h1
|
||||||
|
width: 90%
|
||||||
|
flex-basis: 25%
|
||||||
|
font-size: 2em
|
||||||
|
|
||||||
|
table
|
||||||
|
width: 100dvw
|
||||||
|
|
||||||
|
tr th:first-child,
|
||||||
|
tr td:first-child
|
||||||
|
width: 10dvw
|
||||||
|
min-width: 10dvw
|
||||||
|
max-width: 10dvw
|
||||||
|
word-break: break-all
|
||||||
|
|
||||||
|
tr th:nth-child(2),
|
||||||
|
tr td:nth-child(2)
|
||||||
|
width: 90dvw
|
||||||
|
min-width: 90dvw
|
||||||
|
max-width: 90dvw
|
||||||
|
white-space: normal
|
||||||
|
word-break: break-all
|
||||||
Reference in New Issue
Block a user