merge v2 into main #2
14 changed files with 424 additions and 192 deletions
60
config.go
60
config.go
|
@ -9,24 +9,64 @@ var serverPort = 8080
|
||||||
var screenDir = "w"
|
var screenDir = "w"
|
||||||
|
|
||||||
|
|
||||||
// which routes do you want to show on the screen? add as many or as few as you'd like.
|
// which routes do you want to show on the screen? check out readme.md for an explanation
|
||||||
var routes = []Route{
|
var query = []Group {
|
||||||
Route{
|
Group {
|
||||||
|
Name: "34th & 45th Street",
|
||||||
|
Stations: []Station {
|
||||||
|
Station {
|
||||||
Id: 15280,
|
Id: 15280,
|
||||||
Name: "46 WB",
|
Routes: []Route {
|
||||||
Dir: "n",
|
Route { // 46 westbound
|
||||||
|
Id: 46,
|
||||||
|
Dir: "Edina",
|
||||||
},
|
},
|
||||||
Route{
|
},
|
||||||
|
},
|
||||||
|
Station {
|
||||||
Id: 15412,
|
Id: 15412,
|
||||||
Name: "46 EB",
|
Routes: []Route {
|
||||||
Dir: "s",
|
Route { // 46 eastbound
|
||||||
|
Id: 46,
|
||||||
|
Dir: "St Paul - 7th St",
|
||||||
},
|
},
|
||||||
Route{
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Group {
|
||||||
|
Name: "46th Street Station",
|
||||||
|
Stations: []Station {
|
||||||
|
Station {
|
||||||
Id: 51430,
|
Id: 51430,
|
||||||
|
Routes: []Route {
|
||||||
|
Route { // blue line northbound
|
||||||
|
Id: 901,
|
||||||
Name: "Blue line",
|
Name: "Blue line",
|
||||||
|
Dir: "Downtown Mpls",
|
||||||
},
|
},
|
||||||
Route{
|
},
|
||||||
|
},
|
||||||
|
Station {
|
||||||
Id: 51415,
|
Id: 51415,
|
||||||
|
Routes: []Route {
|
||||||
|
Route { // blue line southbound
|
||||||
|
Id: 901,
|
||||||
Name: "Blue line",
|
Name: "Blue line",
|
||||||
|
Dir: "Airport / Mall",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Station {
|
||||||
|
Id: 51546,
|
||||||
|
Routes: []Route {
|
||||||
|
Route { Id: 7 },
|
||||||
|
Route { Id: 9 },
|
||||||
|
Route { Id: 74 },
|
||||||
|
Route { Id: 921 }, // A line
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
204
main.go
204
main.go
|
@ -6,80 +6,89 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// configuration query format
|
||||||
|
type Group struct { // list related stops under a section title
|
||||||
|
Name string // friendly readable name for this group
|
||||||
|
Stations []Station
|
||||||
|
}
|
||||||
|
type Station struct {
|
||||||
|
Id uint // number found on the sign or on nextrip / g00gle maps
|
||||||
|
Routes []Route
|
||||||
|
}
|
||||||
type Route struct {
|
type Route struct {
|
||||||
Id uint // stop ID shown on sign or google maps
|
Id uint // route number (may need to dig through the API for this one)
|
||||||
Name string // friendly readable name (optional)
|
Name string // friendly readable name (optional)
|
||||||
Dir string // cardinal direction the bus or train is going (optional, will use value from API)
|
Dir string // direction or location it's heading (optional)
|
||||||
// Route string // filter by route name (optional) /!\ not yet implemented /!\
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// response format
|
||||||
|
type EtaList struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Etas []Eta `json:"etas"`
|
||||||
|
}
|
||||||
type Eta struct {
|
type Eta struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Dir string `json:"dir"`
|
Heading string `json:"heading"`
|
||||||
Class string `json:"class"`
|
Class string `json:"class"`
|
||||||
Delta string `json:"delta"`
|
Delta string `json:"delta"`
|
||||||
}
|
}
|
||||||
var NilEta = Eta{}
|
|
||||||
|
|
||||||
type StrSlice []string
|
// TODO? arrows
|
||||||
func (slice StrSlice) indexOf(value string) int {
|
|
||||||
for p, v := range slice {
|
// type StrSlice []string
|
||||||
if (v == value) {
|
// func (slice StrSlice) indexOf(value string) int {
|
||||||
return p
|
// for p, v := range slice {
|
||||||
}
|
// if (v == value) {
|
||||||
}
|
// return p
|
||||||
return -1
|
// }
|
||||||
}
|
// }
|
||||||
|
// return -1
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var cardinals = StrSlice{"n","e","s","w"}
|
||||||
|
// var bounds = StrSlice{"NB","EB","SB","WB"}
|
||||||
|
// var arrows = StrSlice{"↑","->","↓","<-"}
|
||||||
|
// func arrow(dir string) string {
|
||||||
|
// var format *StrSlice
|
||||||
|
// if len(dir) < 2 {
|
||||||
|
// format = &cardinals
|
||||||
|
// } else {
|
||||||
|
// format = &bounds
|
||||||
|
// }
|
||||||
|
// going := (format.indexOf(dir) + cardinals.indexOf(screenDir)) % 4
|
||||||
|
// return arrows[going]
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
var cardinals = StrSlice{"n","e","s","w"}
|
func (d Departure) parseDeparture(name, dir string) Eta {
|
||||||
var bounds = StrSlice{"NB","EB","SB","WB"}
|
|
||||||
var arrows = StrSlice{"↑","->","↓","<-"}
|
|
||||||
func arrow(dir string) string {
|
|
||||||
var format *StrSlice
|
|
||||||
if len(dir) < 2 {
|
|
||||||
format = &cardinals
|
|
||||||
} else {
|
|
||||||
format = &bounds
|
|
||||||
}
|
|
||||||
going := (format.indexOf(dir) + cardinals.indexOf(screenDir)) % 4
|
|
||||||
return arrows[going]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (r Route) getETA() Eta {
|
|
||||||
d := fetchRouteData(r.Id)
|
|
||||||
e := Eta{}
|
e := Eta{}
|
||||||
if len(d.Departures) == 0 {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
next := d.Departures[0]
|
|
||||||
|
|
||||||
// route name
|
// route name
|
||||||
if r.Name == "" {
|
if name == "" {
|
||||||
e.Name = next.RouteShortName
|
e.Name = d.RouteShortName
|
||||||
} else {
|
} else {
|
||||||
e.Name = r.Name
|
e.Name = name
|
||||||
}
|
}
|
||||||
|
|
||||||
// direction arrow
|
// direction
|
||||||
if r.Dir == "" {
|
if dir == "" {
|
||||||
e.Dir = arrow(next.DirectionText)
|
e.Heading = d.Heading
|
||||||
} else {
|
} else {
|
||||||
e.Dir = arrow(r.Dir)
|
e.Heading = dir
|
||||||
}
|
}
|
||||||
|
|
||||||
// time delta
|
// time delta
|
||||||
t := time.Unix(next.DepartureTime, 0)
|
t := time.Unix(d.DepartureTime, 0)
|
||||||
dur := time.Until(t)
|
dur := time.Until(t)
|
||||||
if dur.Minutes() <= 1 {
|
if dur.Minutes() <= 1 {
|
||||||
e.Delta = "Due"
|
e.Delta = "Due"
|
||||||
} else if dur.Hours() < 60 {
|
} else if dur.Hours() < 60 {
|
||||||
e.Delta = fmt.Sprintf("%.0f minutes", dur.Minutes())
|
e.Delta = fmt.Sprintf("%.0f min", dur.Minutes())
|
||||||
} else {
|
} else {
|
||||||
e.Delta = fmt.Sprintf("%.0fh %.0fm", dur.Hours(), dur.Minutes() - (math.Floor(dur.Hours())*60))
|
e.Delta = fmt.Sprintf("%.0fh %.0fm", dur.Hours(), dur.Minutes() - (math.Floor(dur.Hours())*60))
|
||||||
}
|
}
|
||||||
|
@ -95,6 +104,77 @@ func (r Route) getETA() Eta {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Station) getETAs() []Eta {
|
||||||
|
r := fetchRouteData(s.Id)
|
||||||
|
e := []Eta{}
|
||||||
|
if len(r.Departures) == 0 {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s.Routes) == 0 {
|
||||||
|
e = append(e, r.Departures[0].parseDeparture("",""))
|
||||||
|
} else {
|
||||||
|
// create a map for storing an ETA for each requested route
|
||||||
|
re := make(map[uint]Eta)
|
||||||
|
for _, reqRoute := range s.Routes {
|
||||||
|
// loop through r.Departures
|
||||||
|
for _, d := range r.Departures {
|
||||||
|
// if departure's route id matches the requested one,
|
||||||
|
dId, _ := strconv.Atoi(d.RouteId)
|
||||||
|
// and an ETA hasn't been found yet,
|
||||||
|
_, has := re[reqRoute.Id]
|
||||||
|
if dId == int(reqRoute.Id) && !has {
|
||||||
|
// save it to the map.
|
||||||
|
re[reqRoute.Id] = d.parseDeparture(reqRoute.Name, reqRoute.Dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// convert map to array in the order specified in config
|
||||||
|
for _, r := range s.Routes {
|
||||||
|
d, exists := re[r.Id]
|
||||||
|
if exists {
|
||||||
|
e = append(e, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func api(response http.ResponseWriter, request *http.Request){
|
||||||
|
maxAge, _ := time.ParseDuration("29s")
|
||||||
|
fmt.Fprint(response, getCache(maxAge, func() string {
|
||||||
|
stations := []EtaList{}
|
||||||
|
|
||||||
|
for _, group := range query {
|
||||||
|
etas := []Eta{}
|
||||||
|
for _, station := range group.Stations {
|
||||||
|
etas = append(etas, station.getETAs()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
stations = append(stations, EtaList{group.Name, etas})
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(stations)
|
||||||
|
if err != nil {
|
||||||
|
return "[]"
|
||||||
|
} else {
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheValue string
|
||||||
|
var cacheUpdated time.Time
|
||||||
|
func getCache(maxAge time.Duration, f func() string) string {
|
||||||
|
if time.Since(cacheUpdated) >= maxAge {
|
||||||
|
cacheValue = f()
|
||||||
|
cacheUpdated = time.Now()
|
||||||
|
}
|
||||||
|
return cacheValue
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func main(){
|
func main(){
|
||||||
http.Handle("/", http.FileServer(http.Dir("./static")))
|
http.Handle("/", http.FileServer(http.Dir("./static")))
|
||||||
|
@ -104,39 +184,3 @@ func main(){
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func api(response http.ResponseWriter, request *http.Request){
|
|
||||||
var etas []Eta
|
|
||||||
|
|
||||||
for _, route := range routes {
|
|
||||||
eta := route.getETA()
|
|
||||||
if eta != NilEta {
|
|
||||||
etas = append(etas, eta)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.Marshal(etas)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprint(response, "[]")
|
|
||||||
} else {
|
|
||||||
fmt.Fprint(response, string(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: cache the ETA list
|
|
||||||
|
|
||||||
// let etaCache = {
|
|
||||||
// value: null,
|
|
||||||
// createdAt: 0,
|
|
||||||
// }
|
|
||||||
// let getETAsFromCache = async (maxAge) => {
|
|
||||||
// if (Date.now() / 1000 - etaCache.createdAt >= maxAge) {
|
|
||||||
// etaCache.value = []
|
|
||||||
// for (let r of conf.routes) {
|
|
||||||
// let e = await getETA(r)
|
|
||||||
// if (e !== null) etaCache.value.push(e)
|
|
||||||
// }
|
|
||||||
// etaCache.createdAt = Date.now() / 1000
|
|
||||||
// }
|
|
||||||
// return etaCache.value
|
|
||||||
// }
|
|
||||||
|
|
13
nextrip.go
13
nextrip.go
|
@ -7,6 +7,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
// type Stop struct {
|
// type Stop struct {
|
||||||
// stop_id uint
|
// stop_id uint
|
||||||
// latitude float32
|
// latitude float32
|
||||||
|
@ -23,11 +24,11 @@ type Departure struct {
|
||||||
// stop_id uint
|
// stop_id uint
|
||||||
DepartureText string `json:"departure_text"`
|
DepartureText string `json:"departure_text"`
|
||||||
DepartureTime int64 `json:"departure_time"`
|
DepartureTime int64 `json:"departure_time"`
|
||||||
// description string
|
Heading string `json:"description"`
|
||||||
// route_id uint
|
RouteId string `json:"route_id"`
|
||||||
RouteShortName string `json:"route_short_name"`
|
RouteShortName string `json:"route_short_name"`
|
||||||
// direction_id uint8
|
// direction_id uint8
|
||||||
DirectionText string `json:"direction_text"`
|
// DirectionText string `json:"direction_text"`
|
||||||
// schedule_relationship string
|
// schedule_relationship string
|
||||||
}
|
}
|
||||||
type RouteData struct {
|
type RouteData struct {
|
||||||
|
@ -39,20 +40,20 @@ type RouteData struct {
|
||||||
func fetchRouteData(id uint) RouteData {
|
func fetchRouteData(id uint) RouteData {
|
||||||
resp, err := http.Get(fmt.Sprintf("https://svc.metrotransit.org/nextripv2/%d", id))
|
resp, err := http.Get(fmt.Sprintf("https://svc.metrotransit.org/nextripv2/%d", id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Unable to reach NexTrip API:\n%s", err)
|
fmt.Printf("Unable to reach NexTrip API:\n%s\n", err)
|
||||||
return RouteData{}
|
return RouteData{}
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error when reading NexTrip API response:\n%s", err)
|
fmt.Printf("Error when reading NexTrip API response:\n%s\n", err)
|
||||||
return RouteData{}
|
return RouteData{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var data RouteData
|
var data RouteData
|
||||||
err = json.Unmarshal(body, &data)
|
err = json.Unmarshal(body, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error when parsing NexTrip API data:\n%s", err)
|
fmt.Printf("Error when parsing NexTrip API data:\n%s\n", err)
|
||||||
return RouteData{}
|
return RouteData{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
57
readme.md
57
readme.md
|
@ -24,3 +24,60 @@ we're going to put this on a display in the window of layer zero so that people
|
||||||
### what's it look like?
|
### what's it look like?
|
||||||
|
|
||||||
![screenshot](screenshot.png)
|
![screenshot](screenshot.png)
|
||||||
|
|
||||||
|
### request data format
|
||||||
|
|
||||||
|
```js
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
name: "34th & 45th Street",
|
||||||
|
stations: [
|
||||||
|
{
|
||||||
|
id: 15280,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
id: 46,
|
||||||
|
name?: "46",
|
||||||
|
dir?: "WB"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 15412,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
id: 46,
|
||||||
|
name?: "46",
|
||||||
|
dir?: "EB"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "46th Street Station",
|
||||||
|
stations: [
|
||||||
|
{
|
||||||
|
id: 51430,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
id: 901,
|
||||||
|
name?: "Blue line",
|
||||||
|
dir?: "to Mpls-Target Field"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 51415,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
id: 901,
|
||||||
|
name?: "Blue line",
|
||||||
|
dir?: "to Mall of America"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
2
sass.sh
Executable file
2
sass.sh
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
sass --watch --no-source-map --style=compressed static/style.scss static/style.css
|
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 54 KiB |
BIN
screenshot2.png
BIN
screenshot2.png
Binary file not shown.
Before Width: | Height: | Size: 20 KiB |
Binary file not shown.
Binary file not shown.
BIN
static/Inconsolata-Regular.ttf
Normal file
BIN
static/Inconsolata-Regular.ttf
Normal file
Binary file not shown.
BIN
static/Inconsolata-SemiBold.ttf
Normal file
BIN
static/Inconsolata-SemiBold.ttf
Normal file
Binary file not shown.
|
@ -9,14 +9,28 @@
|
||||||
let getETAs = async () => {
|
let getETAs = async () => {
|
||||||
let response = await fetch('/etas')
|
let response = await fetch('/etas')
|
||||||
let data = await response.json()
|
let data = await response.json()
|
||||||
console.dir(data)
|
|
||||||
|
|
||||||
let tbody = document.getElementById('etas')
|
let main = document.getElementById('etas')
|
||||||
tbody.innerHTML = ''
|
let innerHTML = ''
|
||||||
|
|
||||||
for (let eta of data){
|
for (let station of data){
|
||||||
tbody.innerHTML += `<tr><td>${eta.name}</td><td>${eta.dir}</td><td class="${eta.class}">${eta.delta}</td></tr>`
|
innerHTML += `
|
||||||
|
<table cellspacing="0">
|
||||||
|
<thead>
|
||||||
|
<tr><th colspan="3">${station.name}</th></tr>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 25%;">route</th>
|
||||||
|
<th style="width: 50%;">heading</th>
|
||||||
|
<th style="width: 25%;">eta</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>\n`
|
||||||
|
for (let eta of station.etas){
|
||||||
|
innerHTML += `<tr><td>${eta.name}</td><td>${eta.heading}</td><td class="${eta.class}">${eta.delta}</td></tr>`
|
||||||
}
|
}
|
||||||
|
innerHTML += `</tbody></table>\n`
|
||||||
|
}
|
||||||
|
main.innerHTML = innerHTML
|
||||||
}
|
}
|
||||||
|
|
||||||
let main = () => {
|
let main = () => {
|
||||||
|
@ -30,16 +44,7 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Transit schedule</h1>
|
<h1>Transit schedule</h1>
|
||||||
<table cellspacing="0">
|
<main id="etas"></main>
|
||||||
<!-- <thead>
|
|
||||||
<tr>
|
|
||||||
<th>line</th>
|
|
||||||
<th>direction</th>
|
|
||||||
<th>eta</th>
|
|
||||||
</tr>
|
|
||||||
</thead> -->
|
|
||||||
<tbody id="etas"></tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!-- <img id="qrcode" src="qrcode.png" alt="scan this QR code for the full site"> -->
|
<!-- <img id="qrcode" src="qrcode.png" alt="scan this QR code for the full site"> -->
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -1,70 +1 @@
|
||||||
@font-face {
|
@font-face{font-family:"Inconsolata";src:url("Inconsolata-Regular.ttf") format("truetype");font-weight:400;font-style:normal}@font-face{font-family:"Inconsolata";src:url("Inconsolata-SemiBold.ttf") format("truetype");font-weight:600;font-style:normal}html{margin:0;padding:0;font-family:"Inconsolata",monospace;font-weight:400;font-size:32px}@media(prefers-color-scheme: light){html{background:#fff;color:#000}}@media(prefers-color-scheme: dark){html{background:#000;color:#fff}}@media screen and (max-width: 550px){html{font-size:18px}}body{margin:0;padding:0;text-align:center}h1{font-size:1.25rem;font-weight:600;margin:1rem 0}h2{font-size:1.125rem;font-weight:600;margin:.375rem 0}table{width:90vw;margin:0 5vw;font-size:1rem;text-align:center}table:not(:last-child){margin-bottom:1.5em}table thead{line-height:1.3}table thead tr:nth-child(2){font-size:.9rem}table:nth-child(1) thead{background-color:rgba(169,124,183,.5)}table:nth-child(2) thead{background-color:rgba(123,207,220,.5)}table tbody tr{font-size:1.125rem}@media(prefers-color-scheme: light){table tbody tr:nth-child(2n){background:rgba(0,0,0,.1)}}@media(prefers-color-scheme: dark){table tbody tr:nth-child(2n){background:rgba(255,255,255,.2)}}table th{font-weight:600}table td{padding:.125em}@media(prefers-color-scheme: light){.soon{color:#e19d31}.now{color:#42bd4c}}@media(prefers-color-scheme: dark){.soon{color:#fc6}.now{color:#6f8}}img#qrcode{height:5vh;width:auto;position:fixed;bottom:16px;right:16px}
|
||||||
font-family: 'Fira Code';
|
|
||||||
src: url('FiraCode-Regular.woff2') format('woff2'),
|
|
||||||
url('FiraCode-Regular.woff') format('woff');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
html {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
background: #000;
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
font-family: 'Fira Code', monospace;
|
|
||||||
font-feature-settings: 'cv12', 'cv14';
|
|
||||||
font-size: 32px;
|
|
||||||
}
|
|
||||||
@media screen and (max-width: 550px) {
|
|
||||||
html {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 90vw;
|
|
||||||
margin: 0 5vw;
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
font-size: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
tr:nth-child(2n-1) {
|
|
||||||
background-color: rgba(255,255,255,0.1);
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
padding: .1em;
|
|
||||||
}
|
|
||||||
/*td:not(:last-child) {
|
|
||||||
padding-right: 32px;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
.soon {
|
|
||||||
color: #fc8;
|
|
||||||
}
|
|
||||||
.now {
|
|
||||||
color: #8f8;
|
|
||||||
}
|
|
||||||
|
|
||||||
img#qrcode {
|
|
||||||
height: 5vh;
|
|
||||||
width: auto;
|
|
||||||
position: fixed;
|
|
||||||
bottom: 16px;
|
|
||||||
right: 16px;
|
|
||||||
}
|
|
||||||
|
|
152
static/style.scss
Normal file
152
static/style.scss
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inconsolata';
|
||||||
|
src: url('Inconsolata-Regular.ttf') format('truetype');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inconsolata';
|
||||||
|
src: url('Inconsolata-SemiBold.ttf') format('truetype');
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cyb-black: #353535;
|
||||||
|
$cyb-white: #eaeaea;
|
||||||
|
$cyb-purple: #a97cb7;
|
||||||
|
$cyb-blue: #7bcfdc;
|
||||||
|
|
||||||
|
html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
font-family: 'Inconsolata', monospace;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 32px;
|
||||||
|
|
||||||
|
@media screen and (max-width: 550px) {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0.375rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 90vw;
|
||||||
|
margin: 0 5vw;
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
font-size: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
thead {
|
||||||
|
line-height: 1.3;
|
||||||
|
|
||||||
|
tr:nth-child(2) {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
// th { font-weight: normal; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// colored table headers
|
||||||
|
&:nth-child(1) {
|
||||||
|
thead {
|
||||||
|
background-color: rgba($cyb-purple, .5);
|
||||||
|
// tr:nth-child(2) {
|
||||||
|
// background-color: rgba($cyb-purple, .3)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:nth-child(2) {
|
||||||
|
thead {
|
||||||
|
background-color: rgba($cyb-blue, .5);
|
||||||
|
// tr:nth-child(2) {
|
||||||
|
// background-color: rgba($cyb-blue, .3)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
tr {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
|
||||||
|
// highlight every other row to aid in horizontal scanning
|
||||||
|
&:nth-child(2n) {
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
background: rgba(black, .1);
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
background: rgba(white, .2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding: .125em;
|
||||||
|
}
|
||||||
|
|
||||||
|
// round corners of backgrounds
|
||||||
|
// tr:first-child {
|
||||||
|
// td:first-child, th:first-child {
|
||||||
|
// border-top-left-radius: 10px;
|
||||||
|
// }
|
||||||
|
// td:last-child, th:last-child {
|
||||||
|
// border-top-right-radius: 10px;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// tr:last-child {
|
||||||
|
// td:first-child, th:first-child {
|
||||||
|
// border-bottom-left-radius: 10px;
|
||||||
|
// }
|
||||||
|
// td:last-child, th:last-child {
|
||||||
|
// border-bottom-right-radius: 10px;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
.soon { color: #e19d31; }
|
||||||
|
.now { color: #42bd4c; }
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.soon { color: #fc6; }
|
||||||
|
.now { color: #6f8; }
|
||||||
|
}
|
||||||
|
|
||||||
|
img#qrcode {
|
||||||
|
height: 5vh;
|
||||||
|
width: auto;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 16px;
|
||||||
|
right: 16px;
|
||||||
|
}
|
Loading…
Reference in a new issue