Added airline and clipboard support
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
module Controller
|
||||
|
||||
open System
|
||||
open System.Net.Http
|
||||
open System.Text.RegularExpressions
|
||||
open Giraffe.ViewEngine.HtmlElements
|
||||
@@ -51,6 +52,27 @@ let getAirportAbbrTags airportGroup =
|
||||
|> Array.sortBy _.Code
|
||||
|> Array.map View.airportAbbr
|
||||
|
||||
let rec extractTextValues (node: XmlNode) =
|
||||
match node with
|
||||
| Text code when code.Trim().Length > 1 -> [| code |] // Only include codes with exactly three letters
|
||||
| ParentNode (_, children) ->
|
||||
children
|
||||
|> List.toArray
|
||||
|> Array.collect extractTextValues // Collect all text values from children
|
||||
| _ -> [| |] // Ignore any other node types
|
||||
|
||||
|
||||
let formatAirportsPlainText (airports: (string * XmlNode[])[]) (message: string) =
|
||||
message + "\n" + (airports
|
||||
|> Array.map (fun (letter, nodes) ->
|
||||
let airportCodes =
|
||||
nodes
|
||||
|> Array.collect extractTextValues
|
||||
match airportCodes with
|
||||
| [||] -> letter
|
||||
| _ -> String.concat ", " airportCodes
|
||||
)
|
||||
|> String.concat "\n")
|
||||
|
||||
let processAirports (alphabet: char[]) (allAirports: Airport[]) =
|
||||
|
||||
@@ -79,27 +101,36 @@ let processAirports (alphabet: char[]) (allAirports: Airport[]) =
|
||||
| _ -> 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"
|
||||
$"username={username}&listType={user.searchType}&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.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!"
|
||||
let percentageOfAlphabetString = $"{System.Math.Round(100. - percentageOfLettersWithNoAirports, 1)}%% of the alphabet!"
|
||||
let message = $"{username} has flown {percentageOfAlphabetString}"
|
||||
let messagePlainText = $"I've flown {percentageOfAlphabetString}"
|
||||
|
||||
let title = $"{username}'s {user.searchType} flown"
|
||||
let plaintextAirports = formatAirportsPlainText airports messagePlainText
|
||||
airports
|
||||
|> Array.map (fun (letter, nodes) -> View.tableRow letter nodes)
|
||||
|> View.table message username
|
||||
|> View.table message title plaintextAirports
|
||||
|
||||
let RenderPageWithUser (username: string) =
|
||||
let airportList = RenderAirportList {fr24user = username}
|
||||
let RenderPageWithUserAndSearchType (searchType: string) (username: string) =
|
||||
let airportList = RenderAirportList {fr24user = username; searchType = searchType}
|
||||
View.index [|airportList|]
|
||||
|
||||
let RenderPageWithUser = RenderPageWithUserAndSearchType "airports"
|
||||
@@ -1,22 +1,42 @@
|
||||
open Microsoft.AspNetCore.Http
|
||||
open Microsoft.Extensions.DependencyInjection
|
||||
open Saturn
|
||||
open Giraffe
|
||||
open Types
|
||||
|
||||
open System.Net
|
||||
open System.Net.Sockets
|
||||
|
||||
module Program =
|
||||
|
||||
let router = router {
|
||||
not_found_handler (setStatusCode 404 >=> text "404")
|
||||
get "/" ( (View.index [||]) |> htmlView)
|
||||
getf "/%s" (fun username -> htmlView(Controller.RenderPageWithUser username))
|
||||
getf "/%s/%s" (fun (username, searchType) -> htmlView(Controller.RenderPageWithUserAndSearchType searchType username))
|
||||
post "/search" (bindJson<usernameQuery> (fun username ->
|
||||
fun next ctx ->
|
||||
ctx.Response.Headers.Add("HX-Replace-Url", Controller.parseUsername username.fr24user)
|
||||
ctx.Response.Headers.Add("HX-Push-Url", $"/{Controller.parseUsername username.fr24user}/{username.searchType}")
|
||||
htmlView (Controller.RenderAirportList username) next ctx
|
||||
))
|
||||
}
|
||||
|
||||
let ServiceConfig (services: IServiceCollection) = services.AddHttpContextAccessor()
|
||||
let ServiceConfig (services: IServiceCollection) =
|
||||
// Get the server IP address
|
||||
let serverIpAddress =
|
||||
match Dns.GetHostEntry(Dns.GetHostName()).AddressList |> Array.tryFind(fun ip -> ip.AddressFamily = AddressFamily.InterNetwork) with
|
||||
| Some ip -> ip.ToString()
|
||||
| None -> "IP address not found"
|
||||
|
||||
let boldCode = "\u001b[1m"
|
||||
let greenCode = "\u001b[32m"
|
||||
let resetCode = "\u001b[0m"
|
||||
|
||||
// Print the server IP address
|
||||
printfn $"{boldCode}Now Running On: {greenCode}%s{serverIpAddress}{resetCode}"
|
||||
services.AddHttpContextAccessor()
|
||||
|
||||
|
||||
let app =
|
||||
application {
|
||||
use_mime_types [(".woff", "application/font-woff")]
|
||||
@@ -24,6 +44,7 @@ module Program =
|
||||
use_router router
|
||||
use_developer_exceptions
|
||||
service_config ServiceConfig
|
||||
url "http://0.0.0.0:5001"
|
||||
}
|
||||
|
||||
run app
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
type usernameQuery = {
|
||||
fr24user: string
|
||||
searchType: string
|
||||
}
|
||||
|
||||
type AirportResponseData = obj[][]
|
||||
|
||||
@@ -16,10 +16,10 @@ let index content =
|
||||
head [] [
|
||||
meta [_name "viewport"; _content "width=device-width"]
|
||||
title [] [str "Have you flown the alphabet?"]
|
||||
link [_rel "stylesheet" ; _href "/styles/core.css"]
|
||||
link [_rel "stylesheet" ; _href "/styles/core.css?v=2"]
|
||||
script [_src "/scripts/htmx.min.js"] []
|
||||
script [_src "/scripts/json-enc.js"] []
|
||||
script [_src "/scripts/index.js"] []
|
||||
script [_src "/scripts/index.js?v=2"] []
|
||||
]
|
||||
body [] [
|
||||
div [_id "pageContainer"] [
|
||||
@@ -34,9 +34,22 @@ let index content =
|
||||
_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!"]
|
||||
img [_class "loading"; _alt "Loading..."; _width "200"; _height "30"; _src "/images/loading.svg"]
|
||||
section [] [
|
||||
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!"]
|
||||
img [_class "loading"; _alt "Loading..."; _width "200"; _height "30"; _src "/images/loading.svg"]
|
||||
]
|
||||
section [] [
|
||||
label [_class "checked"] [
|
||||
input [_checked; _type "radio"; _name "searchType"; _value "airports"]
|
||||
str "Airports"
|
||||
]
|
||||
label [] [
|
||||
input [_type "radio"; _name "searchType"; _value "airlines"]
|
||||
str "Airlines"
|
||||
]
|
||||
]
|
||||
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -63,12 +76,15 @@ let tableRow (letter: string ) (codes: XmlNode[]) =
|
||||
]
|
||||
]
|
||||
|
||||
let table (message) (user: string) (rows: XmlNode[]) =
|
||||
let table (message) (title: string) (plaintextAirports: string) (rows: XmlNode[]) =
|
||||
article [] [
|
||||
h1 [] [str message]
|
||||
table [] [
|
||||
tr [] [
|
||||
th [_colspan "2"] [str $"{user}'s Airports Flown"]
|
||||
th [_colspan "2"] [
|
||||
str title
|
||||
small [(attr "data-airportList") plaintextAirports; _onclick "navigator.clipboard.writeText(this.getAttribute('data-airportList')).then(() => { const originalText = this.innerText; this.innerText = '✅'; this.style.color = 'green'; setTimeout(() => { this.innerText = originalText; this.style.color = ''; }, 1000); })"] [str "📋"]
|
||||
]
|
||||
]
|
||||
yield! rows
|
||||
]
|
||||
|
||||
@@ -68,26 +68,45 @@ h1
|
||||
font-size: 5em
|
||||
|
||||
form
|
||||
@include flex
|
||||
flex-basis: 25%
|
||||
@include flex-column
|
||||
flex-basis: 50%
|
||||
width: 100%
|
||||
gap: 0.6em
|
||||
|
||||
input
|
||||
flex-basis: 75%
|
||||
height: 100%
|
||||
font-size: 2em
|
||||
padding-left: 1em
|
||||
color: #666
|
||||
section
|
||||
@include flex
|
||||
flex-basis: 50%
|
||||
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
|
||||
> label
|
||||
@include flex
|
||||
flex-basis: 10%
|
||||
color: white
|
||||
border: solid 1px white
|
||||
height: 100%
|
||||
|
||||
.loading
|
||||
display: none
|
||||
> input
|
||||
display: none
|
||||
|
||||
> label.checked
|
||||
color: #666666
|
||||
background-color: white
|
||||
|
||||
button
|
||||
flex-basis: 25%
|
||||
font-size: 2em
|
||||
height: 100%
|
||||
color: white
|
||||
background: grey
|
||||
|
||||
.loading
|
||||
display: none
|
||||
|
||||
|
||||
@include mobile
|
||||
@@ -150,6 +169,9 @@ h1
|
||||
background-color: gray
|
||||
color: white
|
||||
|
||||
small
|
||||
cursor: pointer
|
||||
|
||||
td, th
|
||||
padding: 0.3em
|
||||
|
||||
|
||||
@@ -1 +1,14 @@
|
||||
document.addEventListener("DOMContentLoaded", (event) => document.getElementById('results').scrollIntoView());
|
||||
document.addEventListener("DOMContentLoaded", (event) => {
|
||||
document.querySelectorAll('input[type="radio"]').forEach((radio) => {
|
||||
radio.addEventListener('change', () => {
|
||||
document.querySelectorAll(`input[name="${radio.name}"]`).forEach((input) => {
|
||||
input.parentElement.classList.remove('checked');
|
||||
});
|
||||
|
||||
if (radio.checked) {
|
||||
radio.parentElement.classList.add('checked');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", (event) => document.getElementById('results').scrollIntoView());
|
||||
|
||||
@@ -69,24 +69,49 @@ h1 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-basis: 25%;
|
||||
flex-direction: column;
|
||||
flex-basis: 50%;
|
||||
width: 100%;
|
||||
gap: 0.6em;
|
||||
}
|
||||
#pageContainer section#input article form section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-basis: 50%;
|
||||
width: 100%;
|
||||
}
|
||||
#pageContainer section#input article form input {
|
||||
#pageContainer section#input article form section > input {
|
||||
flex-basis: 75%;
|
||||
height: 100%;
|
||||
font-size: 2em;
|
||||
padding-left: 1em;
|
||||
color: #666;
|
||||
}
|
||||
#pageContainer section#input article form button {
|
||||
#pageContainer section#input article form section > label {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-basis: 10%;
|
||||
color: white;
|
||||
border: solid 1px white;
|
||||
height: 100%;
|
||||
}
|
||||
#pageContainer section#input article form section > label > input {
|
||||
display: none;
|
||||
}
|
||||
#pageContainer section#input article form section > label.checked {
|
||||
color: #666666;
|
||||
background-color: white;
|
||||
}
|
||||
#pageContainer section#input article form section button {
|
||||
flex-basis: 25%;
|
||||
font-size: 2em;
|
||||
height: 100%;
|
||||
color: white;
|
||||
background: grey;
|
||||
}
|
||||
#pageContainer section#input article form .loading {
|
||||
#pageContainer section#input article form section .loading {
|
||||
display: none;
|
||||
}
|
||||
@media screen and (max-width: 900px) {
|
||||
@@ -149,6 +174,9 @@ h1 {
|
||||
background-color: gray;
|
||||
color: white;
|
||||
}
|
||||
#pageContainer section#results article table th small {
|
||||
cursor: pointer;
|
||||
}
|
||||
#pageContainer section#results article table td, #pageContainer section#results article table th {
|
||||
padding: 0.3em;
|
||||
}
|
||||
|
||||
@@ -1 +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"}
|
||||
{"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;EAIA;EAkDY;EACA;EACA;;AAEA;EA5DZ;EACA;EACA;EA4DgB;EACA;;AACA;EACI;EACA;EACA;EACA;EACA;;AAEJ;EAvEhB;EACA;EACA;EAuEoB;EACA;EACA;EACA;;AAEA;EACI;;AAER;EACI;EACA;;AAEJ;EACI;EACA;EACA;EACA;EACA;;AAEJ;EACI;;AAjGpB;EAiDI;IAoDQ;IACA;;EAEA;IACI;;EAEJ;IACI;IACA;IACA;IACA;;EAEA;IACI;IACA;IACA;;;AAKR;EACI;;AAEJ;EACI;;AAEhB;EACI;EACA;EACA;;AAEA;EACI;EACA;;AAGJ;EArIJ;EACA;EACA;EAIA;EAiIQ;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;AAAA;EAEI;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAEA;EACI;;AAER;EACI;;AAGA;EACI;;AAEJ;EACI;EACA;;AAGJ;EACI;;AACJ;EACI;EACA;;AAnLpB;EAyII;IA4CQ;;EACA;IACI;IACA;IACA;;EAEJ;IACI;;EAEA;AAAA;IAEI;IACA;IACA;IACA;;EAEJ;AAAA;IAEI;IACA;IACA;IACA;IACA","file":"core.css"}
|
||||
Reference in New Issue
Block a user