add newsletter
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"job": "none"
|
"job": "none",
|
||||||
|
"emails": [
|
||||||
|
"zipfriis@gmail.com"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,8 @@ 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) {
|
||||||
|
|||||||
@@ -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
@@ -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
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user