1
0
Fork 0

Simpler interface, initial data scripts

pull/1/head
Ambrose Chua 2020-10-30 03:34:38 +08:00
parent f514f6fc00
commit 79490bdb20
11 changed files with 480 additions and 63 deletions

3
.gitignore vendored
View File

@ -1 +1,4 @@
data/
js/data.json
datetime.link

20
Makefile Normal file
View File

@ -0,0 +1,20 @@
.PHONY: all
all: download
.PHONY: download
download: data/cities15000.txt data/countryInfo.txt data/admin1CodesASCII.txt
data/cities15000.txt:
mkdir -p data/
wget http://download.geonames.org/export/dump/cities15000.zip -O data/cities15000.zip
unzip data/cities15000.zip -d data/
$(RM) data/cities15000.zip
data/countryInfo.txt:
mkdir -p data/
wget http://download.geonames.org/export/dump/countryInfo.txt -O data/countryInfo.txt
data/admin1CodesASCII.txt:
mkdir -p data/
wget https://download.geonames.org/export/dump/admin1CodesASCII.txt -O data/admin1CodesASCII.txt

221
css/styles.css Normal file
View File

@ -0,0 +1,221 @@
*, *:before, *:after {
box-sizing: border-box;
}
html {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-feature-settings: "case", "tnum", "ss03", "cv09", "cv08", "cv10", "cv11";
line-height: 1.5;
}
@supports (font-variation-settings: normal) {
html {
font-family: 'Inter var', -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
}
@media (max-width: 400px) {
html {
font-size: 87.5%;
}
}
@media (min-width: 675px) {
html {
font-size: 112.5%;
}
}
@media (min-width: 1024px) {
html {
font-size: 125%;
}
}
body {
margin: 0;
}
main,
header,
footer {
display: block;
}
/* Layout */
html {
height: 100%;
}
body {
display: flex;
flex-direction: column;
min-height: 100%;
}
main,
footer {
margin: 0 auto;
width: 100%;
max-width: 40rem;
}
main {
flex-grow: 1;
}
footer {
padding: 2rem;
}
/* Essentials */
.list-inline {
list-style: none;
padding: 0;
}
.list-inline li {
display: inline-block;
}
.list-inline li:before {
content: "\2022";
padding: 0 0.25rem;
}
.list-inline li:first-child:before {
content: none;
}
.icon svg {
height: 1em;
}
.icon svg path {
fill: currentColor;
}
/* Specifics */
main {
margin-top: 1rem;
margin-bottom: 1rem;
}
footer {
text-align: right;
font-size: 0.75em;
opacity: 0.5;
}
footer ul {
margin: 0;
}
footer a {
color: inherit;
}
/* Theming */
body {
background: rgb(255, 255, 255);
background: color(display-p3 1 1 1);
color: rgb(0, 0, 0);
color: color(display-p3 0 0 0);
}
@media (prefers-color-scheme: dark) {
body {
background: rgb(0, 0, 0);
background: color(display-p3 0 0 0);
color: rgb(255, 255, 255);
color: color(display-p3 1 1 1);
}
}
body.theme-light {
background: rgb(255, 255, 255);
background: color(display-p3 1 1 1);
color: rgb(0, 0, 0);
color: color(display-p3 0 0 0);
}
body.theme-dark {
background: rgb(0, 0, 0);
background: color(display-p3 0 0 0);
color: rgb(255, 255, 255);
color: color(display-p3 1 1 1);
}
body:not(.theme-light):not(.theme-dark) .theme-toggle-dark,
body:not(.theme-light):not(.theme-dark) .theme-toggle-light {
display: none;
}
body.theme-light .theme-toggle-dark,
body.theme-light .theme-toggle-system {
display: none;
}
body.theme-dark .theme-toggle-light,
body.theme-dark .theme-toggle-system {
display: none;
}
/* Components */
d-zone {
margin-left: 2rem;
margin-right: 2rem;
display: flex;
flex-wrap: wrap;
align-items: center;
}
d-zone:not(:last-of-type):after {
content: '';
display: block;
height: 3px;
width: 100%;
border-radius: 2px;
background: rgb(127, 127, 127);
background: color(display-p3 0.5 0.5 0.5);
}
@media (max-width: 319px) {
d-zone {
display: block;
}
}
/* Left container */
d-zoneinfo {
flex: 1 1 0;
display: block;
margin-top: 1rem;
margin-bottom: 1rem;
}
d-zonename {
display: block;
font-weight: 600;
}
d-zoneoffset {
display: block;
font-size: 0.75em;
opacity: 0.5;
}
d-date {
display: block;
}
/* Right container */
d-zonefigure {
display: block;
margin-top: 1rem;
margin-bottom: 1rem;
margin-left: 0.5rem;
}
d-time {
display: block;
font-size: 2em;
}

View File

@ -1,3 +1,3 @@
package main
//go:generate go run scripts/bcp47timezone.go
//go:generate go run scripts/data.go

View File

@ -10,32 +10,25 @@ async function importDateTime() {
// Zone data
//
// This maps an IANA zone into readable strings with bcp47. Sadly, Intl doesn't provide such strings.
// This maps cities to IANA zones, and provides zone names
async function importZoneData() {
const zoneData = {};
const res = await fetch("/js/bcp47timezone.json");
async function importData() {
const res = await fetch("/js/data.json");
const data = await res.json();
for (const timezone of data) {
for (const alias of timezone.aliases) {
zoneData[alias] = timezone;
}
}
return zoneData;
// TODO: break apart the data
return data;
}
// Start
Promise.all([
importDateTime(),
importZoneData(),
importData(),
]).then(dependencies => {
start(...dependencies);
});
function start(DateTime, zoneData) {
function start(DateTime, data) {
// Datetime translation
//

51
js/theme.js Normal file
View File

@ -0,0 +1,51 @@
'use strict';
const OPTIONS = {
'dark': {
name: 'theme-dark',
},
'light': {
name: 'theme-light',
},
'system': {
name: '',
},
};
function setTheme(theme) {
for (let key in OPTIONS) {
const cls = OPTIONS[key];
if (cls.name) {
document.body.classList.remove(cls.name);
}
}
const cls = OPTIONS[theme];
if (cls === undefined) {
return;
}
if (cls.name) {
document.body.classList.add(cls.name);
}
}
function toggleTheme() {
let theme = localStorage.getItem('theme');
if (theme === 'dark') {
theme = 'light';
} else if (theme === 'light') {
theme = 'system';
} else if (theme === 'system') {
theme = 'dark';
} else {
// Default is system -> dark
theme = 'dark';
}
localStorage.setItem('theme', theme);
setTheme(theme);
}
const theme = localStorage.getItem('theme');
if (theme) {
setTheme(theme);
}

40
main.go
View File

@ -20,7 +20,7 @@ func main() {
server := &http.Server{
Addr: listen,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
WriteTimeout: 5 * time.Second,
}
tmpl, err = template.ParseGlob("templates/*")
@ -42,7 +42,7 @@ func main() {
func index(w http.ResponseWriter, req *http.Request) {
var err error
if req.Method != http.MethodGet {
if req.Method != http.MethodGet && req.Method != http.MethodHead {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
@ -50,21 +50,31 @@ func index(w http.ResponseWriter, req *http.Request) {
accept := req.Header.Get("Accept")
responseType := chooseResponseType(accept)
templateName := ""
switch responseType {
case responsePlain:
w.WriteHeader(http.StatusNotImplemented)
templateName = "index.txt"
case responseHTML:
indexTmpl := tmpl.Lookup("index.html")
if indexTmpl == nil {
log.Printf("Unable to find index template")
w.WriteHeader(http.StatusInternalServerError)
return
}
err = indexTmpl.Execute(w, nil)
if err != nil {
log.Printf("Error: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
templateName = "index.html"
case responseUnknown:
w.WriteHeader(http.StatusNotAcceptable)
return
}
t := tmpl.Lookup(templateName)
if t == nil {
log.Printf("Unable to find index template")
w.WriteHeader(http.StatusInternalServerError)
return
}
if req.Method == http.MethodHead {
return
}
err = t.Execute(w, nil)
if err != nil {
log.Printf("Error: %v", err)
// Usually, the following will fail
w.WriteHeader(http.StatusInternalServerError)
return
}
}

View File

@ -55,8 +55,9 @@ func sortedMimes(accept string) (sorted []mime) {
type responseType int
const (
responsePlain responseType = iota
responseHTML responseType = iota
responsePlain responseType = iota
responseHTML responseType = iota
responseUnknown responseType = iota
)
const (
@ -79,5 +80,5 @@ func chooseResponseType(accept string) responseType {
return responseHTML
}
}
return responseHTML
return responseUnknown
}

110
scripts/data.go Normal file
View File

@ -0,0 +1,110 @@
package main
import (
"encoding/csv"
"encoding/json"
"io"
"io/ioutil"
"log"
"os"
"strings"
)
func main() {
// Read CSV data
citiesFile, err := os.Open("data/cities15000.txt")
if err != nil {
log.Fatalf("Opening file failed: %v", err)
}
r := csv.NewReader(citiesFile)
r.Comma = '\t'
r.Comment = '#'
// Track collisions
collisions := make(map[string]bool)
// Pick out useful information
cities := make(map[string]City)
for {
record, err := r.Read()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("Unable to read CSV: %v", err)
}
key, city := CityFromRecord(record)
// TODO: Reimplement collision rewriter
// Remap collisions
if _, ok := collisions[key]; ok {
key = key + "_" + city.Admin1Code + "_" + city.CountryCode
}
// Check for collisions
if existing, ok := cities[key]; ok {
if existing.CountryCode == city.CountryCode {
log.Printf("Warning: Repeat entry with same country code for %s (please compare %s with %s)", key, city.Timezone, existing.Timezone)
} else if existing.Timezone == city.Timezone {
log.Printf("Warning: Repeat entry with same timezone for %s", key)
} else {
log.Printf("Warning: Collision entry found for %s. Rewriting (%s but there is %s)", key, city.CountryCode, existing.CountryCode)
cities[key+"_"+existing.Admin1Code+"_"+existing.CountryCode] = existing
delete(cities, "key")
collisions[key] = true
}
}
cities[key] = city
}
// Group data
data := Data{
Cities: cities,
}
// Encode JSON file
b, err := json.Marshal(data)
if err != nil {
log.Fatalf("Failed to encode: %v", err)
}
// Write JSON file
err = ioutil.WriteFile("js/data.json", b, 0644)
if err != nil {
log.Fatalf("Failed to write: %v", err)
}
}
type Data struct {
Cities map[string]City
}
type City struct {
Names []string `json:"n"`
Admin1Code string `json:"a"`
CountryCode string `json:"c"`
Timezone string `json:"t"`
}
func CityFromRecord(record []string) (string, City) {
name := normalizeName(record[2])
names := splitNames(record[3])
admin1Code := record[10]
countryCode := record[8]
timezone := record[17]
return name, City{
Names: names,
Admin1Code: admin1Code,
CountryCode: countryCode,
Timezone: timezone,
}
}
func normalizeName(name string) string {
return strings.ReplaceAll(name, " ", "_")
}
func splitNames(names string) []string {
return strings.Split(names, ",")
}

View File

@ -5,40 +5,42 @@
<title>datetime.link</title>
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<script src="/js/theme.js"></script>
<main id="app">
<datetime-zone>
<datetime-zoneinfo>
<datetime-zonename>India Standard Time (IST)</datetime-zonename>
<datetime-zoneoffset>(UTC +5:30)</datetime-zoneoffset>
</datetime-zoneinfo>
<datetime-datetime>
<datetime-date date="2020-06-02">2020-06-02</datetime-date>
<datetime-time time="14:00">14:00</datetime-time>
</datetime-datetime>
</datetime-zone>
<datetime-zone>
<datetime-zoneinfo>
<datetime-zonename>Singapore Time (SGT)</datetime-zonename>
<datetime-zoneoffset>(UTC +8)</datetime-zoneoffset>
</datetime-zoneinfo>
<datetime-datetime>
<datetime-date date="2020-06-02">2020-06-02</datetime-date>
<datetime-time time="11:30">11:30</datetime-time>
</datetime-datetime>
</datetime-zone>
<datetime-zone>
<datetime-zoneinfo>
<datetime-zonename>Singapore</datetime-zonename>
<datetime-zoneoffset>(UTC +8)</datetime-zoneoffset>
</datetime-zoneinfo>
<datetime-datetime>
<datetime-date date="2020-06-02">2020-06-02</datetime-date>
<datetime-time time="11:30">11:30</datetime-time>
</datetime-datetime>
</datetime-zone>
<d-zone>
<d-zoneinfo>
<d-zonename>India Standard Time</d-zonename>
<d-zoneoffset>UTC +5:30</d-zoneoffset>
<d-date date="2020-06-02">2020-06-02</d-date>
</d-zoneinfo>
<d-zonefigure>
<d-time time="14:00">14:00</d-time>
</d-zonefigure>
</d-zone>
<d-zone>
<d-zoneinfo>
<d-zonename>Singapore Time</d-zonename>
<d-zoneoffset>UTC +8:00</d-zoneoffset>
<d-date date="2020-06-02">2020-06-02</d-date>
</d-zoneinfo>
<d-zonefigure>
<d-time time="16:30">16:30</d-time>
</d-zonefigure>
</d-zone>
<d-zone>
<d-zoneinfo>
<d-zonename>New York, USA</d-zonename>
<d-zoneoffset>UTC -4:00</d-zoneoffset>
<d-date date="2020-06-02">2020-06-02</d-date>
</d-zoneinfo>
<d-zonefigure>
<d-time time="04:30">04:30</d-time>
</d-zonefigure>
</d-zone>
</main>
<footer>
@ -46,6 +48,11 @@
<!--
--><li><a href="https://github.com/serverwentdown/datetime.link" target="_blank">About datetime.link</a></li><!--
--><li><a href="https://github.com/serverwentdown/datetime.link/issues/new" target="_blank">Found a bug?</a></li><!--
--><li><a onclick="toggleTheme(); return false">
<span class="icon theme-toggle-system"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M8 256c0 136.966 111.033 248 248 248s248-111.034 248-248S392.966 8 256 8 8 119.033 8 256zm248 184V72c101.705 0 184 82.311 184 184 0 101.705-82.311 184-184 184z"/></svg></span> <span class="theme-toggle-name theme-toggle-system">System theme</span>
<span class="icon theme-toggle-dark"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M283.211 512c78.962 0 151.079-35.925 198.857-94.792 7.068-8.708-.639-21.43-11.562-19.35-124.203 23.654-238.262-71.576-238.262-196.954 0-72.222 38.662-138.635 101.498-174.394 9.686-5.512 7.25-20.197-3.756-22.23A258.156 258.156 0 0 0 283.211 0c-141.309 0-256 114.511-256 256 0 141.309 114.511 256 256 256z"/></svg></span> <span class="theme-toggle-name theme-toggle-dark">Dark theme</span>
<span class="icon theme-toggle-light"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M256 160c-52.9 0-96 43.1-96 96s43.1 96 96 96 96-43.1 96-96-43.1-96-96-96zm246.4 80.5l-94.7-47.3 33.5-100.4c4.5-13.6-8.4-26.5-21.9-21.9l-100.4 33.5-47.4-94.8c-6.4-12.8-24.6-12.8-31 0l-47.3 94.7L92.7 70.8c-13.6-4.5-26.5 8.4-21.9 21.9l33.5 100.4-94.7 47.4c-12.8 6.4-12.8 24.6 0 31l94.7 47.3-33.5 100.5c-4.5 13.6 8.4 26.5 21.9 21.9l100.4-33.5 47.3 94.7c6.4 12.8 24.6 12.8 31 0l47.3-94.7 100.4 33.5c13.6 4.5 26.5-8.4 21.9-21.9l-33.5-100.4 94.7-47.3c13-6.5 13-24.7.2-31.1zm-155.9 106c-49.9 49.9-131.1 49.9-181 0-49.9-49.9-49.9-131.1 0-181 49.9-49.9 131.1-49.9 181 0 49.9 49.9 49.9 131.1 0 181z"/></svg></span> <span class="theme-toggle-name theme-toggle-light">Light theme</span>
</a></li><!--
-->
</ul>
</footer>

1
templates/index.txt Normal file
View File

@ -0,0 +1 @@
TODO