add newsletter

This commit is contained in:
samantha42
2026-05-30 18:15:20 +02:00
parent 681f916787
commit 8d4e523b3e
6 changed files with 104 additions and 17 deletions
+4 -1
View File
@@ -1,3 +1,6 @@
{ {
"job": "none" "job": "none",
"emails": [
"zipfriis@gmail.com"
]
} }
+31 -12
View File
@@ -13,6 +13,7 @@ import (
type Config struct { type Config struct {
Job string `json:"job"` Job string `json:"job"`
Emails []string `json:"emails"`
} }
func loadConfig() (*Config, error) { func loadConfig() (*Config, error) {
@@ -24,6 +25,14 @@ func loadConfig() (*Config, error) {
return &c, json.Unmarshal(data, &c) return &c, json.Unmarshal(data, &c)
} }
func saveConfig(c *Config) error {
data, err := json.MarshalIndent(c, "", " ")
if err != nil {
return err
}
return os.WriteFile("data.json", data, 0644)
}
// ---------- Main ---------- // ---------- Main ----------
func main() { func main() {
@@ -49,19 +58,29 @@ func main() {
http.HandleFunc("GET /favicon.ico", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/icon.png") }) http.HandleFunc("GET /favicon.ico", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/icon.png") })
http.HandleFunc("GET /login", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/login.html") }) http.HandleFunc("GET /login", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/login.html") })
http.HandleFunc("POST /login", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("POST /newsletter/subscribe", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm() var req struct {
Email string `json:"email"`
password := r.FormValue("password")
totp := r.FormValue("totp")
if os.Getenv("password") == password && os.Getenv("auth") == totp {
fmt.Fprintf(w, "logged in")
http.Redirect(w, r, "/admin", http.StatusAccepted)
} else {
w.WriteHeader(http.StatusAccepted)
fmt.Fprintf(w, "password or auth is wrong")
} }
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
double := false
conf, err := loadConfig()
for _, e := range conf.Emails {
if e == req.Email {
double = true
}
}
if !double {
conf.Emails = append(conf.Emails, req.Email)
}
saveConfig(conf)
}) })
http.HandleFunc("GET /status", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("GET /status", func(w http.ResponseWriter, r *http.Request) {
BIN
View File
Binary file not shown.
+65
View File
@@ -271,6 +271,51 @@
</div> </div>
<br> <br>
</section> </section>
<section id="newsletter">
<div class="page-header">
<h2 class="page-title">Equity Research · Newsletter</h2>
</div>
<div class="exp-card">
<div class="exp-header">
<span class="exp-title">When the numbers say something worth saying.</span>
</div>
<p class="exp-body">
Not a scheduled publication. Sent when there is a specific, well-reasoned case to make — a mispricing, a narrative the market has wrong, or an implied expectation that doesn't survive contact with the filings.
<br><br>
Fundamental analysis. SEC filings. No macro noise.
</p>
<div style="display:flex;flex-wrap:wrap;gap:1rem;margin:1.25rem 0;">
<div style="flex:1;min-width:140px;">
<div class="exp-title" style="margin-bottom:6px;">// thesis</div>
<p class="exp-body">A specific position on a specific company. Long or short. With a reason.</p>
</div>
<div style="flex:1;min-width:140px;">
<div class="exp-title" style="margin-bottom:6px;">// filings-first</div>
<p class="exp-body">10-K, 10-Q, earnings. The numbers, not the press release.</p>
</div>
</div>
<div id="nl-form" style="display:flex;flex-wrap:wrap;gap:8px;margin-top:1rem;">
<input
id="nl-email"
type="email"
placeholder="your@email.com"
autocomplete="email"
style="font-family:var(--ff-mono);font-size:0.75rem;background:transparent;border:0.5px solid var(--border);color:var(--text);padding:0 12px;height:36px;border-radius:3px;outline:none;min-width:200px;flex:1;"
/>
<button
onclick="nlSubmit()"
class="navigate"
style="height:36px;padding:0 16px;cursor:pointer;white-space:nowrap;"
>subscribe →</button>
</div>
<p id="nl-note" style="font-family:var(--ff-mono);font-size:0.65rem;color:var(--muted);margin-top:8px;">// no schedule. no spam. unsubscribe anytime.</p>
<p id="nl-success" style="display:none;font-family:var(--ff-mono);font-size:0.75rem;color:var(--accent, #5DCAA5);margin-top:8px;">// subscribed. you'll hear from me when something is worth writing about.</p>
</div>
</section>
<section id="interests"> <section id="interests">
<div class="page-header"> <div class="page-header">
<h2 class="page-title" id="interest"> <h2 class="page-title" id="interest">
@@ -484,6 +529,26 @@ function openOverlay() {
button.textContent = "close ↑" button.textContent = "close ↑"
} }
} }
function nlSubmit() {
const input = document.getElementById('nl-email');
const val = input.value.trim();
if (!val || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)) {
input.style.borderColor = 'red';
input.focus();
setTimeout(() => { input.style.borderColor = ''; }, 1600);
return;
}
console.log(val)
// TODO: POST to your subscriber endpoint (Listmonk, Buttondown, etc.)
fetch('/newsletter/subscribe', { method:'POST', body: JSON.stringify({email:val}), headers:{'Content-Type':'application/json'} })
document.getElementById('nl-form').style.display = 'none';
document.getElementById('nl-note').style.display = 'none';
document.getElementById('nl-success').style.display = 'block';
}
document.getElementById('nl-email').addEventListener('keydown', e => {
if (e.key === 'Enter') nlSubmit();
});
</script> </script>
<script src="/static/scripts/posteranimation.js"></script> <script src="/static/scripts/posteranimation.js"></script>
+2 -2
View File
@@ -202,7 +202,7 @@ h1 span{color:var(--accent);}
transition:border-color 0.2s,box-shadow 0.2s; transition:border-color 0.2s,box-shadow 0.2s;
} }
.edu-card:hover{border-color:var(--border2);box-shadow:0 2px 12px rgba(0,0,0,0.06);} .edu-card:hover{border-color:var(--border2);box-shadow:0 2px 12px rgba(0,0,0,0.06);}
.edu-year{font-family:var(--ff-mono);font-size:0.7rem;color:var(--amber);padding-top:2px;line-height:1.6;} .edu-year{font-family:var(--ff-mono);font-size:0.7rem;color:var(--green);padding-top:2px;line-height:1.6;}
.edu-degree{font-size:0.9rem;font-weight:500;color:var(--text);} .edu-degree{font-size:0.9rem;font-weight:500;color:var(--text);}
.edu-inst{font-family:var(--ff-mono);font-size:0.75rem;color:var(--muted);margin-top:2px;} .edu-inst{font-family:var(--ff-mono);font-size:0.75rem;color:var(--muted);margin-top:2px;}
.edu-detail{font-size:0.82rem;color:var(--dim);margin-top:4px;} .edu-detail{font-size:0.82rem;color:var(--dim);margin-top:4px;}
@@ -272,7 +272,7 @@ footer{
.tag-dropped { .tag-dropped {
background: var(--surface2); background: var(--surface2);
color: var(--amber); color: var(--muted);
} }
+1 -1
View File
File diff suppressed because one or more lines are too long