Simpler interface, initial data scripts
parent
f514f6fc00
commit
79490bdb20
|
@ -1 +1,4 @@
|
|||
data/
|
||||
js/data.json
|
||||
|
||||
datetime.link
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
package main
|
||||
|
||||
//go:generate go run scripts/bcp47timezone.go
|
||||
//go:generate go run scripts/data.go
|
||||
|
|
|
@ -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
|
||||
//
|
||||
|
|
|
@ -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
40
main.go
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
7
mime.go
7
mime.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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, ",")
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
TODO
|
Loading…
Reference in New Issue