merge v2 into main #2
14 changed files with 424 additions and 192 deletions
76
config.go
76
config.go
|
@ -9,24 +9,64 @@ var serverPort = 8080
|
|||
var screenDir = "w"
|
||||
|
||||
|
||||
// which routes do you want to show on the screen? add as many or as few as you'd like.
|
||||
var routes = []Route{
|
||||
Route{
|
||||
Id: 15280,
|
||||
Name: "46 WB",
|
||||
Dir: "n",
|
||||
// which routes do you want to show on the screen? check out readme.md for an explanation
|
||||
var query = []Group {
|
||||
Group {
|
||||
Name: "34th & 45th Street",
|
||||
Stations: []Station {
|
||||
Station {
|
||||
Id: 15280,
|
||||
Routes: []Route {
|
||||
Route { // 46 westbound
|
||||
Id: 46,
|
||||
Dir: "Edina",
|
||||
},
|
||||
},
|
||||
},
|
||||
Station {
|
||||
Id: 15412,
|
||||
Routes: []Route {
|
||||
Route { // 46 eastbound
|
||||
Id: 46,
|
||||
Dir: "St Paul - 7th St",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Route{
|
||||
Id: 15412,
|
||||
Name: "46 EB",
|
||||
Dir: "s",
|
||||
},
|
||||
Route{
|
||||
Id: 51430,
|
||||
Name: "Blue line",
|
||||
},
|
||||
Route{
|
||||
Id: 51415,
|
||||
Name: "Blue line",
|
||||
Group {
|
||||
Name: "46th Street Station",
|
||||
Stations: []Station {
|
||||
Station {
|
||||
Id: 51430,
|
||||
Routes: []Route {
|
||||
Route { // blue line northbound
|
||||
Id: 901,
|
||||
Name: "Blue line",
|
||||
Dir: "Downtown Mpls",
|
||||
},
|
||||
},
|
||||
},
|
||||
Station {
|
||||
Id: 51415,
|
||||
Routes: []Route {
|
||||
Route { // blue line southbound
|
||||
Id: 901,
|
||||
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
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
210
main.go
210
main.go
|
@ -6,80 +6,89 @@ import (
|
|||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"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 {
|
||||
Id uint // stop ID shown on sign or google maps
|
||||
Name string // friendly readable name (optional)
|
||||
Dir string // cardinal direction the bus or train is going (optional, will use value from API)
|
||||
// Route string // filter by route name (optional) /!\ not yet implemented /!\
|
||||
Id uint // route number (may need to dig through the API for this one)
|
||||
Name string // friendly readable name (optional)
|
||||
Dir string // direction or location it's heading (optional)
|
||||
}
|
||||
|
||||
// response format
|
||||
type EtaList struct {
|
||||
Name string `json:"name"`
|
||||
Etas []Eta `json:"etas"`
|
||||
}
|
||||
type Eta struct {
|
||||
Name string `json:"name"`
|
||||
Dir string `json:"dir"`
|
||||
Class string `json:"class"`
|
||||
Delta string `json:"delta"`
|
||||
}
|
||||
var NilEta = Eta{}
|
||||
|
||||
type StrSlice []string
|
||||
func (slice StrSlice) indexOf(value string) int {
|
||||
for p, v := range slice {
|
||||
if (v == value) {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return -1
|
||||
Name string `json:"name"`
|
||||
Heading string `json:"heading"`
|
||||
Class string `json:"class"`
|
||||
Delta string `json:"delta"`
|
||||
}
|
||||
|
||||
// TODO? arrows
|
||||
|
||||
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]
|
||||
}
|
||||
// type StrSlice []string
|
||||
// func (slice StrSlice) indexOf(value string) int {
|
||||
// for p, v := range slice {
|
||||
// if (v == value) {
|
||||
// return p
|
||||
// }
|
||||
// }
|
||||
// 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]
|
||||
// }
|
||||
|
||||
|
||||
func (r Route) getETA() Eta {
|
||||
d := fetchRouteData(r.Id)
|
||||
func (d Departure) parseDeparture(name, dir string) Eta {
|
||||
e := Eta{}
|
||||
if len(d.Departures) == 0 {
|
||||
return e
|
||||
}
|
||||
next := d.Departures[0]
|
||||
|
||||
// route name
|
||||
if r.Name == "" {
|
||||
e.Name = next.RouteShortName
|
||||
if name == "" {
|
||||
e.Name = d.RouteShortName
|
||||
} else {
|
||||
e.Name = r.Name
|
||||
e.Name = name
|
||||
}
|
||||
|
||||
// direction arrow
|
||||
if r.Dir == "" {
|
||||
e.Dir = arrow(next.DirectionText)
|
||||
// direction
|
||||
if dir == "" {
|
||||
e.Heading = d.Heading
|
||||
} else {
|
||||
e.Dir = arrow(r.Dir)
|
||||
e.Heading = dir
|
||||
}
|
||||
|
||||
// time delta
|
||||
t := time.Unix(next.DepartureTime, 0)
|
||||
t := time.Unix(d.DepartureTime, 0)
|
||||
dur := time.Until(t)
|
||||
if dur.Minutes() <= 1 {
|
||||
e.Delta = "Due"
|
||||
} else if dur.Hours() < 60 {
|
||||
e.Delta = fmt.Sprintf("%.0f minutes", dur.Minutes())
|
||||
e.Delta = fmt.Sprintf("%.0f min", dur.Minutes())
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
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(){
|
||||
http.Handle("/", http.FileServer(http.Dir("./static")))
|
||||
|
@ -104,39 +184,3 @@ func main(){
|
|||
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"
|
||||
)
|
||||
|
||||
|
||||
// type Stop struct {
|
||||
// stop_id uint
|
||||
// latitude float32
|
||||
|
@ -23,11 +24,11 @@ type Departure struct {
|
|||
// stop_id uint
|
||||
DepartureText string `json:"departure_text"`
|
||||
DepartureTime int64 `json:"departure_time"`
|
||||
// description string
|
||||
// route_id uint
|
||||
Heading string `json:"description"`
|
||||
RouteId string `json:"route_id"`
|
||||
RouteShortName string `json:"route_short_name"`
|
||||
// direction_id uint8
|
||||
DirectionText string `json:"direction_text"`
|
||||
// DirectionText string `json:"direction_text"`
|
||||
// schedule_relationship string
|
||||
}
|
||||
type RouteData struct {
|
||||
|
@ -39,20 +40,20 @@ type RouteData struct {
|
|||
func fetchRouteData(id uint) RouteData {
|
||||
resp, err := http.Get(fmt.Sprintf("https://svc.metrotransit.org/nextripv2/%d", id))
|
||||
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{}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
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{}
|
||||
}
|
||||
|
||||
var data RouteData
|
||||
err = json.Unmarshal(body, &data)
|
||||
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{}
|
||||
}
|
||||
|
||||
|
|
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?
|
||||
|
||||
![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 response = await fetch('/etas')
|
||||
let data = await response.json()
|
||||
console.dir(data)
|
||||
|
||||
let tbody = document.getElementById('etas')
|
||||
tbody.innerHTML = ''
|
||||
let main = document.getElementById('etas')
|
||||
let innerHTML = ''
|
||||
|
||||
for (let eta of data){
|
||||
tbody.innerHTML += `<tr><td>${eta.name}</td><td>${eta.dir}</td><td class="${eta.class}">${eta.delta}</td></tr>`
|
||||
for (let station of data){
|
||||
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 = () => {
|
||||
|
@ -30,16 +44,7 @@
|
|||
</head>
|
||||
<body>
|
||||
<h1>Transit schedule</h1>
|
||||
<table cellspacing="0">
|
||||
<!-- <thead>
|
||||
<tr>
|
||||
<th>line</th>
|
||||
<th>direction</th>
|
||||
<th>eta</th>
|
||||
</tr>
|
||||
</thead> -->
|
||||
<tbody id="etas"></tbody>
|
||||
</table>
|
||||
<main id="etas"></main>
|
||||
|
||||
<!-- <img id="qrcode" src="qrcode.png" alt="scan this QR code for the full site"> -->
|
||||
</body>
|
||||
|
|
|
@ -1,70 +1 @@
|
|||
@font-face {
|
||||
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;
|
||||
}
|
||||
@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}
|
||||
|
|
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