refactor: unify /coffee → /support

- Rename coffee.astro → support.astro (broader scope: ko-fi, crypto, wallets)
- Update all /coffee links → /support across website + extension pages
- Update nav, footer, hero, and all 5 extension .md files
- TabFM coffee banner already points to /support
This commit is contained in:
Jose Juan Moñino
2026-07-10 19:13:20 +02:00
parent 0bc8ac75cd
commit 903e51cf5b
12 changed files with 790 additions and 87 deletions
+137 -3
View File
@@ -46,7 +46,7 @@ func main() {
}
defer db.Close()
// Create table if not exists
// Create tables if not exists
schema := `
CREATE TABLE IF NOT EXISTS feedback (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -54,9 +54,19 @@ func main() {
reason TEXT DEFAULT '',
text TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);`
);
CREATE TABLE IF NOT EXISTS ratings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ext TEXT NOT NULL,
rating INTEGER NOT NULL CHECK(rating >= 1 AND rating <= 5),
ip_hash TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_ratings_ext ON ratings(ext);
CREATE INDEX IF NOT EXISTS idx_ratings_ip ON ratings(ip_hash);
`
if _, err := db.Exec(schema); err != nil {
log.Fatalf("failed to create table: %v", err)
log.Fatalf("failed to create tables: %v", err)
}
mux := http.NewServeMux()
@@ -154,6 +164,130 @@ func main() {
json.NewEncoder(w).Encode(stats)
})
// POST /rate — submit a star rating (1-5) for an extension
mux.HandleFunc("/rate", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.WriteHeader(http.StatusOK)
return
}
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Access-Control-Allow-Origin", "*")
var req struct {
Ext string `json:"ext"`
Rating int `json:"rating"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
req.Ext = strings.TrimSpace(req.Ext)
if req.Ext == "" {
http.Error(w, "ext is required", http.StatusBadRequest)
return
}
if req.Rating < 1 || req.Rating > 5 {
http.Error(w, "rating must be 1-5", http.StatusBadRequest)
return
}
// Hash the IP for dedup (one vote per IP per extension)
ip := r.RemoteAddr
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
ip = strings.Split(forwarded, ",")[0]
}
ipHash := fmt.Sprintf("%x", ip)[:16] // simple hash, enough for dedup
// Check if already voted
var existing int
err := db.QueryRow("SELECT rating FROM ratings WHERE ext = ? AND ip_hash = ?", req.Ext, ipHash).Scan(&existing)
if err == nil {
// Update existing vote
_, err = db.Exec("UPDATE ratings SET rating = ?, created_at = CURRENT_TIMESTAMP WHERE ext = ? AND ip_hash = ?", req.Rating, req.Ext, ipHash)
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
} else {
// Insert new vote
_, err = db.Exec("INSERT INTO ratings (ext, rating, ip_hash) VALUES (?, ?, ?)", req.Ext, req.Rating, ipHash)
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
}
log.Printf("rating received: ext=%s rating=%d", req.Ext, req.Rating)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
})
// GET /ratings?ext=<slug> — get aggregate rating for an extension
mux.HandleFunc("/ratings", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/json")
ext := strings.TrimSpace(r.URL.Query().Get("ext"))
if ext == "" {
// Return all ratings
rows, err := db.Query(`
SELECT ext, AVG(rating) as avg, COUNT(*) as count
FROM ratings
GROUP BY ext
`)
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
defer rows.Close()
type RatingSummary struct {
Ext string `json:"ext"`
Average float64 `json:"average"`
Count int `json:"count"`
}
var all []RatingSummary
for rows.Next() {
var s RatingSummary
if err := rows.Scan(&s.Ext, &s.Average, &s.Count); err != nil {
continue
}
all = append(all, s)
}
json.NewEncoder(w).Encode(all)
return
}
// Single extension
var avg float64
var count int
err := db.QueryRow("SELECT COALESCE(AVG(rating), 0), COUNT(*) FROM ratings WHERE ext = ?", ext).Scan(&avg, &count)
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"ext": ext,
"average": avg,
"count": count,
})
})
// Health check
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)