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:
+137
-3
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user