|
|
|
@@ -16,6 +16,7 @@ import (
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/yuin/goldmark"
|
|
|
|
|
"github.com/yuin/goldmark/extension"
|
|
|
|
|
"github.com/yuin/goldmark-highlighting"
|
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
|
)
|
|
|
|
@@ -83,9 +84,12 @@ var (
|
|
|
|
|
|
|
|
|
|
func newMarkdownRenderer() goldmark.Markdown {
|
|
|
|
|
return goldmark.New(
|
|
|
|
|
goldmark.WithExtensions(highlighting.NewHighlighting(
|
|
|
|
|
highlighting.WithStyle("github"),
|
|
|
|
|
)),
|
|
|
|
|
goldmark.WithExtensions(
|
|
|
|
|
extension.Table,
|
|
|
|
|
highlighting.NewHighlighting(
|
|
|
|
|
highlighting.WithStyle("github"),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -174,6 +178,7 @@ func loadData() error {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getNavBar(r *http.Request) string {
|
|
|
|
|
themeToggle := `<button id="theme-toggle" aria-label="Toggle dark mode">🌓</button>`
|
|
|
|
|
if isAuthenticated(r) {
|
|
|
|
|
username := ""
|
|
|
|
|
userID := getCurrentUserID(r)
|
|
|
|
@@ -183,9 +188,9 @@ func getNavBar(r *http.Request) string {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return fmt.Sprintf(`<nav><a href="/home">home</a> | <a href="/">list</a> | <a href="/add">add</a> | <a href="/edit">edit</a> | <span class="logged-in">Logged in as %s</span> | <a href="/logout">logout</a></nav>`, username)
|
|
|
|
|
return fmt.Sprintf(`<nav><a href="/home">home</a> | <a href="/">list</a> | <a href="/add">add</a> | <a href="/edit">edit</a> | <span class="logged-in">Logged in as %s</span> | <a href="/logout">logout</a> | %s</nav>`, username, themeToggle)
|
|
|
|
|
}
|
|
|
|
|
return `<nav><a href="/home">home</a> | <a href="/">list</a> | <a href="/add">add</a> | <a href="/edit">edit</a> | <a href="/login">login</a> | <a href="/register">register</a></nav>`
|
|
|
|
|
return `<nav><a href="/home">home</a> | <a href="/">list</a> | <a href="/add">add</a> | <a href="/edit">edit</a> | <a href="/login">login</a> | <a href="/register">register</a> | ` + themeToggle + `</nav>`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Session helpers - all access sessions map under dataMu lock
|
|
|
|
@@ -450,43 +455,113 @@ func main() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const css = `<style>
|
|
|
|
|
:root{
|
|
|
|
|
--bg-color:#fafafa;
|
|
|
|
|
--text-color:#333;
|
|
|
|
|
--heading-color:#2c3e50;
|
|
|
|
|
--nav-bg:#2c3e50;
|
|
|
|
|
--nav-text:#ecf0f1;
|
|
|
|
|
--link-color:#3498db;
|
|
|
|
|
--code-bg:#f6f8fa;
|
|
|
|
|
--code-text:#333;
|
|
|
|
|
--input-border:#ddd;
|
|
|
|
|
--button-bg:#3498db;
|
|
|
|
|
--delete-bg:#e74c3c;
|
|
|
|
|
--blockquote-border:#3498db;
|
|
|
|
|
--blockquote-text:#555;
|
|
|
|
|
--private-color:#95a5a6;
|
|
|
|
|
--table-border:#ddd;
|
|
|
|
|
--table-bg:#fafafa;
|
|
|
|
|
--table-header-bg:#f0f0f0;
|
|
|
|
|
--table-row-alt:#f5f5f5;
|
|
|
|
|
}
|
|
|
|
|
.dark{
|
|
|
|
|
--bg-color:#1e1e1e;
|
|
|
|
|
--table-border:#444;
|
|
|
|
|
--table-bg:#2a2a2a;
|
|
|
|
|
--table-header-bg:#333;
|
|
|
|
|
--table-row-alt:#252525;
|
|
|
|
|
--text-color:#e0e0e0;
|
|
|
|
|
--heading-color:#e0e0e0;
|
|
|
|
|
--nav-bg:#2c3e50;
|
|
|
|
|
--nav-text:#ffffff;
|
|
|
|
|
--link-color:#58a6ff;
|
|
|
|
|
--code-bg:#1a1a1a;
|
|
|
|
|
--code-text:#ffffff;
|
|
|
|
|
--input-border:#444;
|
|
|
|
|
--button-bg:#0d6efd;
|
|
|
|
|
--delete-bg:#da3633;
|
|
|
|
|
--blockquote-border:#58a6ff;
|
|
|
|
|
--blockquote-text:#aaa;
|
|
|
|
|
--private-color:#8892b0;
|
|
|
|
|
}
|
|
|
|
|
*:before,*:after{box-sizing:border-box}
|
|
|
|
|
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;line-height:1.6;color:#333;background:#fafafa}
|
|
|
|
|
nav{background:#2c3e50;padding:.5rem;position:sticky;top:0;z-index:100}
|
|
|
|
|
nav a{color:#ecf0f1;text-decoration:none;margin:0 .5rem;padding:.25rem .5rem;border-radius:4px;transition:background .2s}
|
|
|
|
|
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;line-height:1.6;color:var(--text-color);background:var(--bg-color);transition:background .3s,color .3s}
|
|
|
|
|
nav{background:var(--nav-bg);padding:.5rem;position:sticky;top:0;z-index:100}
|
|
|
|
|
nav a{color:var(--nav-text);text-decoration:none;margin:0 .5rem;padding:.25rem .5rem;border-radius:4px;transition:background .2s}
|
|
|
|
|
nav a:hover{background:rgba(255,255,255,.1)}
|
|
|
|
|
nav .logged-in{color:#ecf0f1;font-weight:600;margin:0 .5rem;padding:.25rem .5rem}
|
|
|
|
|
nav .logged-in{color:var(--nav-text);font-weight:600;margin:0 .5rem;padding:.25rem .5rem}
|
|
|
|
|
main{padding:1rem;max-width:800px;margin:0 auto}
|
|
|
|
|
h1,h2{color:#2c3e50;margin-top:0}
|
|
|
|
|
h1,h2{color:var(--heading-color);margin-top:0}
|
|
|
|
|
ul,ol{padding-left:1.5rem}
|
|
|
|
|
li{margin:.5rem 0}
|
|
|
|
|
form{margin:1rem 0}
|
|
|
|
|
label{display:block;margin:.5rem 0 .25rem;font-weight:600}
|
|
|
|
|
input[type="text"],input[type="password"]{width:100%;padding:.5rem;font-size:1rem;border:1px solid #ddd;border-radius:4px;font-family:inherit}
|
|
|
|
|
textarea{width:100%;padding:.75rem;font-size:1rem;border:1px solid #ddd;border-radius:4px;font-family:inherit;min-height:400px}
|
|
|
|
|
label{display:block;margin:.5rem 0 .25rem;font-weight:600;color:var(--text-color)}
|
|
|
|
|
input[type="text"],input[type="password"]{width:100%;padding:.5rem;font-size:1rem;border:1px solid var(--input-border);border-radius:4px;font-family:inherit;background:var(--bg-color);color:var(--text-color)}
|
|
|
|
|
textarea{width:100%;padding:.75rem;font-size:1rem;border:1px solid var(--input-border);border-radius:4px;font-family:inherit;min-height:400px;background:var(--bg-color);color:var(--text-color)}
|
|
|
|
|
@media(min-height:700px){textarea{min-height:500px}}
|
|
|
|
|
button{background:#3498db;color:#fff;border:none;padding:.5rem 1rem;border-radius:4px;cursor:pointer;font-size:1rem;transition:background .2s}
|
|
|
|
|
button:hover{background:#2980b9}
|
|
|
|
|
.delete-btn{background:#e74c3c;color:#fff;padding:.1rem .3rem;font-size:.7rem;border-radius:3px;cursor:pointer;transition:background .2s}
|
|
|
|
|
.delete-btn:hover{background:#c0392b}
|
|
|
|
|
.private-indicator{color:#95a5a6;font-size:.8rem;vertical-align:middle}
|
|
|
|
|
.markdown-body{line-height:1.7}
|
|
|
|
|
.markdown-body h1,.markdown-body h2,.markdown-body h3{margin-top:1.5rem}
|
|
|
|
|
button{background:var(--button-bg);color:#fff;border:none;padding:.5rem 1rem;border-radius:4px;cursor:pointer;font-size:1rem;transition:background .2s}
|
|
|
|
|
button:hover{background:var(--link-color)}
|
|
|
|
|
.delete-btn{background:var(--delete-bg);color:#fff;padding:.1rem .3rem;font-size:.7rem;border-radius:3px;cursor:pointer;transition:background .2s}
|
|
|
|
|
.delete-btn:hover{background:var(--delete-bg)}
|
|
|
|
|
.private-indicator{color:var(--private-color);font-size:.8rem;vertical-align:middle}
|
|
|
|
|
.markdown-body{line-height:1.7;color:var(--text-color)}
|
|
|
|
|
.markdown-body h1,.markdown-body h2,.markdown-body h3{margin-top:1.5rem;color:var(--heading-color)}
|
|
|
|
|
.markdown-body p{margin:.75rem 0}
|
|
|
|
|
.markdown-body pre{background:#f6f8fa;padding:1rem;border-radius:4px;overflow-x:auto}
|
|
|
|
|
.markdown-body code{background:#f6f8fa;padding:.1rem .3rem;border-radius:3px;font-family:monospace;font-size:.9rem}
|
|
|
|
|
.markdown-body blockquote{border-left:3px solid #3498db;padding-left:1rem;margin-left:0;color:#555}
|
|
|
|
|
.markdown-body pre{background:var(--code-bg);padding:1rem;border-radius:4px;overflow-x:auto;color:var(--code-text)}
|
|
|
|
|
.markdown-body code{background:var(--code-bg);padding:.1rem .3rem;border-radius:3px;font-family:monospace;font-size:.9rem;color:var(--code-text)}
|
|
|
|
|
.markdown-body blockquote{border-left:3px solid var(--blockquote-border);padding-left:1rem;margin-left:0;color:var(--blockquote-text)}
|
|
|
|
|
.markdown-body table{border-collapse:collapse;width:100%;margin:1rem 0;background:var(--table-bg);border:1px solid var(--table-border)}
|
|
|
|
|
.markdown-body th{padding:.75rem;text-align:left;border:1px solid var(--table-border);background:var(--table-header-bg);color:var(--heading-color);font-weight:600}
|
|
|
|
|
.markdown-body td{padding:.75rem;text-align:left;border:1px solid var(--table-border);color:var(--text-color)}
|
|
|
|
|
.markdown-body tr:nth-child(even) td{background:var(--table-row-alt)}
|
|
|
|
|
.markdown-body tr:nth-child(odd) td{background:var(--table-bg)}
|
|
|
|
|
.markdown-body pre code{background:transparent;padding:0;font-size:inherit}
|
|
|
|
|
@media(max-width:600px){nav{padding:.5rem;flex-wrap:wrap;display:flex;gap:.5rem}nav a{padding:.25rem .5rem;font-size:.9rem}main{padding:.75rem}}.edit-link{color:#7f8c8d;font-size:.8rem;text-decoration:none}
|
|
|
|
|
.edit-link:hover{color:#3498db}
|
|
|
|
|
</style><meta name="viewport" content="width=device-width,initial-scale=1">`
|
|
|
|
|
.dark .chroma{color:var(--code-text)!important;background-color:var(--code-bg)!important}
|
|
|
|
|
.dark .chroma .lnt{color:var(--private-color)!important}
|
|
|
|
|
.dark .chroma .ln{color:var(--private-color)!important}
|
|
|
|
|
.dark pre{background-color:var(--code-bg)!important}
|
|
|
|
|
@media(max-width:600px){nav{padding:.5rem;flex-wrap:wrap;display:flex;gap:.5rem}nav a{padding:.25rem .5rem;font-size:.9rem}main{padding:.75rem}}
|
|
|
|
|
.edit-link{color:var(--private-color);font-size:.8rem;text-decoration:none}
|
|
|
|
|
.edit-link:hover{color:var(--link-color)}
|
|
|
|
|
#theme-toggle{background:none;border:none;cursor:pointer;padding:.25rem .4rem;color:var(--nav-text);font-size:1.1rem;border-radius:4px}
|
|
|
|
|
#theme-toggle:hover{background:rgba(255,255,255,.1)}
|
|
|
|
|
</style>
|
|
|
|
|
<script>
|
|
|
|
|
document.addEventListener('DOMContentLoaded',function(){
|
|
|
|
|
const html = document.documentElement;
|
|
|
|
|
const saved = localStorage.getItem('theme');
|
|
|
|
|
const prefersDark = window.matchMedia('(prefers-color-scheme:dark)').matches;
|
|
|
|
|
if(saved === 'dark' || (!saved && prefersDark)){
|
|
|
|
|
html.classList.add('dark');
|
|
|
|
|
}
|
|
|
|
|
const toggle = document.getElementById('theme-toggle');
|
|
|
|
|
if(toggle){
|
|
|
|
|
toggle.addEventListener('click',function(){
|
|
|
|
|
html.classList.toggle('dark');
|
|
|
|
|
localStorage.setItem('theme', html.classList.contains('dark') ? 'dark' : 'light');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">`
|
|
|
|
|
|
|
|
|
|
const chromaCSS = `<style>
|
|
|
|
|
.lntd{vertical-align:top;padding:0;margin:0;border:0}.lntable-busy{opacity:.5}.chroma{color:#383a42;background-color:#f6f8fa}.chroma .x{}.chroma .err{color:#a61717;background-color:#e3d2d2}.chroma .lnt{color:#383a42}.chroma .lntd{vertical-align:top;padding:0;margin:0;border:0}.chroma .lntable-busy{opacity:.5}.chroma .hl{display:block;width:100%;background-color:#ffdd40}.chroma .lnt{color:#383a42}.chroma .ln{color:#383a42}.chroma .k{color:#000;font-weight:bold}.chroma .kc{color:#000;font-weight:bold}.chroma .kd{color:#000;font-weight:bold}.chroma .kn{color:#000;font-weight:bold}.chroma .kp{color:#000;font-weight:bold}.chroma .kr{color:#000;font-weight:bold}.chroma .kt{color:#445588;font-weight:bold}.chroma .na{color:#008080}.chroma .nb{color:#008080}.chroma .nc{color:#445588;font-weight:bold}.chroma .no{color:#008080}.chroma .nd{color:#3c5d5b;font-weight:bold}.chroma .ni{color:#800080;font-weight:bold}.chroma .ne{color:#800080;font-weight:bold}.chroma .nf{color:#008080}.chroma .nl{color:#008080}.chroma .nn{color:#005580}.chroma .nx{color:#008080}.chroma .nt{color:#000;font-weight:bold}.chroma .vc{color:#008080}.chroma .vg{color:#008080}.chroma .vi{color:#008080}.chroma .nv{color:#008080}.chroma .va{color:#008080}.chroma .vd{color:#008080}.chroma .ve{color:#008080}.chroma .lntd{vertical-align:top;padding:0;margin:0;border:0}.chroma .s{color:#d14}.chroma .sa{color:#d14}.chroma .sb{color:#d14}.chroma .sc{color:#d14}.chroma .dl{color:#d14}.chroma .sd{color:#d14}.chroma .s2{color:#d14}.chroma .se{color:#d14}.chroma .sh{color:#d14}.chroma .si{color:#d14}.chroma .sx{color:#d14}.chroma .sr{color:#d14}.chroma .s1{color:#d14}.chroma .ss{color:#d14}.chroma .m{color:#009999}.chroma .mb{color:#009999}.chroma .mf{color:#009999}.chroma .mh{color:#009999}.chroma .mi{color:#009999}.chroma .mo{color:#009999}.chroma .il{color:#009999}.chroma .c{color:#999988;font-style:italic}.chroma .ch{color:#999988;font-style:italic}.chroma .cm{color:#999988;font-style:italic}.chroma .c1{color:#999988;font-style:italic}.chroma .cs{color:#999988;font-style:italic}.chroma .cp{color:#999988;font-style:italic}.chroma .cpf{color:#999988;font-style:italic}.chroma .gd{color:#000;font-style:italic}.chroma .ge{font-style:italic}.chroma .gr{color:#ff0000}.chroma .gh{color:#999999}.chroma .gi{color:#000;font-style:italic}.chroma .gs{font-weight:bold}.chroma .gu{color:#000;font-style:italic}.chroma .gp{color:#000;font-style:italic}.chroma .go{color:#888888}.chroma .gt{color:#0044dd}.chroma .gd{color:#000;font-style:italic}.chroma .ge{font-style:italic}.chroma .gc{color:#999988;font-style:italic}.chroma .gb{color:#000;font-weight:bold}.chroma .ga{color:#000;font-style:italic}.chroma .gn{color:#000;font-weight:bold}.chroma .gm{color:#000;font-style:italic}.chroma .gp{color:#000;font-style:italic}.chroma .gs{font-weight:bold}
|
|
|
|
|
</style>`
|
|
|
|
|
|
|
|
|
|
func renderPage(w http.ResponseWriter, r *http.Request, title, body string) {
|
|
|
|
|
fmt.Fprintf(w, "<html><head><title>%s</title>%s%s</head><body>%s<main>%s</main></body></html>", title, css, chromaCSS, getNavBar(r), body)
|
|
|
|
|
fmt.Fprintf(w, "<html><head><title>%s</title>%s%s</head><body>%s<main>%s</main></body></html>", title, chromaCSS, css, getNavBar(r), body)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func listHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|