Initial
This commit is contained in:
commit
dc1df70790
19 files changed
+867
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
/packages/
|
||||||
|
riderModule.iml
|
||||||
|
/_ReSharper.Caches/
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
@@ -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|]
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
module Types
|
||||||
|
|
||||||
|
type usernameQuery = {
|
||||||
|
fr24user: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AirportResponseData = obj[][]
|
||||||
|
|
||||||
|
type Airport = {
|
||||||
|
Code: string
|
||||||
|
City: string
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var Q={onLoad:B,process:zt,on:de,off:ge,trigger:ce,ajax:Nr,find:C,findAll:f,closest:v,values:function(e,t){var r=dr(e,t||"post");return r.values},remove:_,addClass:z,removeClass:n,toggleClass:$,takeClass:W,defineExtension:Ur,removeExtension:Fr,logAll:V,logNone:j,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"],selfRequestsOnly:false,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null},parseInterval:d,_:t,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=Q.config.wsBinaryType;return t},version:"1.9.11"};var r={addTriggerHandler:Lt,bodyContains:se,canAccessLocalStorage:U,findThisElement:xe,filterValues:yr,hasAttribute:o,getAttributeValue:te,getClosestAttributeValue:ne,getClosestMatch:c,getExpressionVars:Hr,getHeaders:xr,getInputValues:dr,getInternalData:ae,getSwapSpecification:wr,getTriggerSpecs:it,getTarget:ye,makeFragment:l,mergeObjects:le,makeSettleInfo:T,oobSwap:Ee,querySelectorExt:ue,selectAndSwap:je,settleImmediately:nr,shouldCancel:ut,triggerEvent:ce,triggerErrorEvent:fe,withExtensions:R};var w=["get","post","put","delete","patch"];var i=w.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");var S=e("head"),q=e("title"),H=e("svg",true);function e(e,t=false){return new RegExp(`<${e}(\\s[^>]*>|>)([\\s\\S]*?)<\\/${e}>`,t?"gim":"im")}function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){return e.parentElement}function re(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function L(e,t,r){var n=te(t,r);var i=te(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function ne(t,r){var n=null;c(t,function(e){return n=L(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function A(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function s(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=re().createDocumentFragment()}return i}function N(e){return/<body/.test(e)}function l(e){var t=!N(e);var r=A(e);var n=e;if(r==="head"){n=n.replace(S,"")}if(Q.config.useTemplateFragments&&t){var i=s("<body><template>"+n+"</template></body>",0);var a=i.querySelector("template").content;if(Q.config.allowScriptTags){oe(a.querySelectorAll("script"),function(e){if(Q.config.inlineScriptNonce){e.nonce=Q.config.inlineScriptNonce}e.htmxExecuted=navigator.userAgent.indexOf("Firefox")===-1})}else{oe(a.querySelectorAll("script"),function(e){_(e)})}return a}switch(r){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return s("<table>"+n+"</table>",1);case"col":return s("<table><colgroup>"+n+"</colgroup></table>",2);case"tr":return s("<table><tbody>"+n+"</tbody></table>",2);case"td":case"th":return s("<table><tbody><tr>"+n+"</tr></tbody></table>",3);case"script":case"style":return s("<div>"+n+"</div>",1);default:return s(n,0)}}function ie(e){if(e){e()}}function I(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return I(e,"Function")}function P(e){return I(e,"Object")}function ae(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function M(e){var t=[];if(e){for(var r=0;r<e.length;r++){t.push(e[r])}}return t}function oe(e,t){if(e){for(var r=0;r<e.length;r++){t(e[r])}}}function X(e){var t=e.getBoundingClientRect();var r=t.top;var n=t.bottom;return r<window.innerHeight&&n>=0}function se(e){Line truncated
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", (event) => document.getElementById('results').scrollIntoView());
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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 */
|
||||||
@@ -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"}
|
||||||
@@ -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