add newsletter
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"job": "none"
|
||||
"job": "none",
|
||||
"emails": [
|
||||
"zipfriis@gmail.com"
|
||||
]
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
type Config struct {
|
||||
Job string `json:"job"`
|
||||
Emails []string `json:"emails"`
|
||||
}
|
||||
|
||||
func loadConfig() (*Config, error) {
|
||||
@@ -24,6 +25,14 @@ func loadConfig() (*Config, error) {
|
||||
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 ----------
|
||||
|
||||
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 /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) {
|
||||
r.ParseForm()
|
||||
|
||||
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")
|
||||
http.HandleFunc("POST /newsletter/subscribe", func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -271,6 +271,51 @@
|
||||
</div>
|
||||
<br>
|
||||
</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">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title" id="interest">
|
||||
@@ -484,6 +529,26 @@ function openOverlay() {
|
||||
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 src="/static/scripts/posteranimation.js"></script>
|
||||
|
||||
+2
-2
@@ -202,7 +202,7 @@ h1 span{color:var(--accent);}
|
||||
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-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-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;}
|
||||
@@ -272,7 +272,7 @@ footer{
|
||||
|
||||
.tag-dropped {
|
||||
background: var(--surface2);
|
||||
color: var(--amber);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user