feat: add carpet
This commit is contained in:
+109
-6
@@ -26,15 +26,16 @@ import (
|
||||
var tplFS embed.FS
|
||||
|
||||
type Handler struct {
|
||||
db *sql.DB
|
||||
eco *economy.Service
|
||||
tpl *template.Template
|
||||
cardsDir string // پوشهی دیسکیِ آپلودِ اسکینها (همان مسیری که static از آن سرو میکند)
|
||||
db *sql.DB
|
||||
eco *economy.Service
|
||||
tpl *template.Template
|
||||
cardsDir string // پوشهی دیسکیِ آپلودِ اسکینها (همان مسیری که static از آن سرو میکند)
|
||||
carpetsDir string // پوشهی دیسکیِ آپلودِ تصاویرِ فرش
|
||||
}
|
||||
|
||||
func New(db *sql.DB, eco *economy.Service, cardsDir string) *Handler {
|
||||
func New(db *sql.DB, eco *economy.Service, cardsDir, carpetsDir string) *Handler {
|
||||
tpl := template.Must(template.ParseFS(tplFS, "templates/*.html"))
|
||||
return &Handler{db: db, eco: eco, tpl: tpl, cardsDir: cardsDir}
|
||||
return &Handler{db: db, eco: eco, tpl: tpl, cardsDir: cardsDir, carpetsDir: carpetsDir}
|
||||
}
|
||||
|
||||
// Routes زیرروترِ /admin را با Basic Auth برمیگرداند.
|
||||
@@ -51,10 +52,15 @@ func (h *Handler) Routes(user, pass string) http.Handler {
|
||||
r.Post("/users/coins", h.adjustCoins)
|
||||
r.Post("/users/grant", h.grantToUser)
|
||||
r.Post("/season/reset", h.resetSeason)
|
||||
r.Get("/carpets", h.carpetsAdmin)
|
||||
r.Post("/carpets/add", h.carpetAdd)
|
||||
r.Post("/carpets/delete", h.carpetDelete)
|
||||
r.Post("/carpets/{id}/upload", h.carpetUpload)
|
||||
r.Get("/chat", h.chatAdmin)
|
||||
r.Post("/chat/pack/add", h.chatPackAdd)
|
||||
r.Post("/chat/pack/delete", h.chatPackDelete)
|
||||
r.Post("/chat/message/add", h.chatMessageAdd)
|
||||
r.Post("/chat/message", h.chatMessageUpdate)
|
||||
r.Post("/chat/message/delete", h.chatMessageDelete)
|
||||
return r
|
||||
}
|
||||
@@ -443,6 +449,86 @@ func (h *Handler) grantToUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// rows یک کوئری را به []map[string]any تبدیل میکند (برای رندرِ عمومیِ جدولها).
|
||||
// --- مدیریتِ فرشها ---
|
||||
|
||||
func (h *Handler) carpetsAdmin(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
carpets := h.rows(ctx,
|
||||
`SELECT id,title,price_coins,vip,sort,enabled FROM carpets ORDER BY sort`,
|
||||
"id", "title", "price_coins", "vip", "sort", "enabled")
|
||||
h.render(w, "carpets.html", map[string]any{
|
||||
"Nav": "carpets",
|
||||
"Carpets": carpets,
|
||||
"Saved": r.URL.Query().Get("saved") == "1",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) carpetAdd(w http.ResponseWriter, r *http.Request) {
|
||||
_ = r.ParseForm()
|
||||
id := r.FormValue("id")
|
||||
if id == "" || strings.ContainsAny(id, "/\\.") {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
num := func(k string) int { v, _ := strconv.Atoi(r.FormValue(k)); return v }
|
||||
vip := 0
|
||||
if r.FormValue("vip") == "on" {
|
||||
vip = 1
|
||||
}
|
||||
_, err := h.db.ExecContext(r.Context(),
|
||||
`INSERT OR REPLACE INTO carpets (id,title,price_coins,vip,sort,enabled) VALUES (?,?,?,?,?,1)`,
|
||||
id, r.FormValue("title"), num("price_coins"), vip, num("sort"))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/carpets?saved=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (h *Handler) carpetDelete(w http.ResponseWriter, r *http.Request) {
|
||||
_ = r.ParseForm()
|
||||
_, _ = h.db.ExecContext(r.Context(), `DELETE FROM carpets WHERE id=?`, r.FormValue("id"))
|
||||
http.Redirect(w, r, "/admin/carpets?saved=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// carpetUpload یک تصویرِ jpg/png را برای یک فرش آپلود و با نامِ <id>.jpg ذخیره میکند.
|
||||
func (h *Handler) carpetUpload(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
if id == "" || strings.ContainsAny(id, "/\\.") {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if h.carpetsDir == "" {
|
||||
http.Error(w, "CARPETS_DIR تنظیم نشده است", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := r.ParseMultipartForm(16 << 20); err != nil {
|
||||
http.Error(w, "bad upload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
file, _, err := r.FormFile("image")
|
||||
if err != nil {
|
||||
http.Error(w, "image لازم است", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
if err := os.MkdirAll(h.carpetsDir, 0o755); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
out, err := os.Create(filepath.Join(h.carpetsDir, id+".jpg"))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
if _, err := io.Copy(out, io.LimitReader(file, 16<<20)); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/carpets?saved=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// --- مدیریتِ بستههای چت و پیامها ---
|
||||
|
||||
func (h *Handler) chatAdmin(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -533,6 +619,23 @@ func (h *Handler) chatMessageAdd(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/admin/chat?saved=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (h *Handler) chatMessageUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
_ = r.ParseForm()
|
||||
id, _ := strconv.Atoi(r.FormValue("id"))
|
||||
body := r.FormValue("body")
|
||||
if id == 0 || body == "" {
|
||||
http.Redirect(w, r, "/admin/chat", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
sort, _ := strconv.Atoi(r.FormValue("sort"))
|
||||
if _, err := h.db.ExecContext(r.Context(),
|
||||
`UPDATE chat_messages SET body=?, sort=? WHERE id=?`, body, sort, id); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/chat?saved=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (h *Handler) chatMessageDelete(w http.ResponseWriter, r *http.Request) {
|
||||
_ = r.ParseForm()
|
||||
id, _ := strconv.Atoi(r.FormValue("id"))
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<!doctype html>
|
||||
<html dir="rtl" lang="fa">
|
||||
{{template "head" .}}
|
||||
<body>
|
||||
{{template "nav" .}}
|
||||
<div class="wrap">
|
||||
<h1>فرشها</h1>
|
||||
{{if .Saved}}<div class="saved">انجام شد ✓</div>{{end}}
|
||||
|
||||
<p style="color:#9aa;font-size:13px">
|
||||
هر فرش بهجای میزِ بازی استفاده میشود. تصویرِ هر فرش را بهصورتِ jpg آپلود کنید
|
||||
(با نامِ شناسه ذخیره میشود). شناسهٔ <code>classic</code> یعنی میزِ پیشفرضِ بدونِ تصویر.
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<tr><th>شناسه</th><th>عنوان</th><th>قیمت (سکه)</th><th>VIP رایگان</th><th>ترتیب</th><th>پیشنمایش</th><th>تصویر</th><th></th></tr>
|
||||
{{range .Carpets}}
|
||||
<tr><form method="post" action="/admin/carpets/add">
|
||||
<td>{{.id}}<input type="hidden" name="id" value="{{.id}}"></td>
|
||||
<td><input name="title" value="{{.title}}"></td>
|
||||
<td><input name="price_coins" value="{{.price_coins}}" style="width:80px"></td>
|
||||
<td><input type="checkbox" name="vip" {{if .vip}}checked{{end}}></td>
|
||||
<td><input name="sort" value="{{.sort}}" style="width:50px"></td>
|
||||
<td><img src="/carpets/{{.id}}.jpg" alt="—" style="height:48px;border-radius:4px" onerror="this.style.display='none'"></td>
|
||||
<td>
|
||||
<form method="post" action="/admin/carpets/{{.id}}/upload" enctype="multipart/form-data" style="display:flex;gap:4px">
|
||||
<input type="file" name="image" accept="image/*" required>
|
||||
<button>آپلود</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<button>ذخیره</button>
|
||||
<button formaction="/admin/carpets/delete" formnovalidate class="del">حذف</button>
|
||||
</td>
|
||||
</form></tr>
|
||||
{{end}}
|
||||
<tr class="addrow"><form method="post" action="/admin/carpets/add">
|
||||
<td><input name="id" placeholder="شناسه" required></td>
|
||||
<td><input name="title" placeholder="عنوان"></td>
|
||||
<td><input name="price_coins" value="0" style="width:80px"></td>
|
||||
<td><input type="checkbox" name="vip"></td>
|
||||
<td><input name="sort" value="0" style="width:50px"></td>
|
||||
<td colspan="2"></td>
|
||||
<td><button>افزودن فرش</button></td>
|
||||
</form></tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -50,12 +50,12 @@
|
||||
<table>
|
||||
<tr><th>متن / شکلک</th><th>ترتیب</th><th></th></tr>
|
||||
{{range .messages}}
|
||||
<tr><form method="post" action="/admin/chat/message/delete">
|
||||
<td>{{.body}}</td>
|
||||
<td>{{.sort}}</td>
|
||||
<tr><form method="post" action="/admin/chat/message">
|
||||
<td><input type="hidden" name="id" value="{{.id}}"><input name="body" value="{{.body}}" style="width:100%"></td>
|
||||
<td><input name="sort" value="{{.sort}}" style="width:50px"></td>
|
||||
<td>
|
||||
<input type="hidden" name="id" value="{{.id}}">
|
||||
<button class="del">حذف</button>
|
||||
<button>ذخیره</button>
|
||||
<button formaction="/admin/chat/message/delete" formnovalidate class="del">حذف</button>
|
||||
</td>
|
||||
</form></tr>
|
||||
{{end}}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<nav>
|
||||
<a href="/admin" class="{{if eq .Nav "dashboard"}}active{{end}}">داشبورد</a>
|
||||
<a href="/admin/shop" class="{{if eq .Nav "shop"}}active{{end}}">فروشگاه</a>
|
||||
<a href="/admin/carpets" class="{{if eq .Nav "carpets"}}active{{end}}">فرشها</a>
|
||||
<a href="/admin/chat" class="{{if eq .Nav "chat"}}active{{end}}">چت</a>
|
||||
<a href="/admin/users" class="{{if eq .Nav "users"}}active{{end}}">کاربران</a>
|
||||
</nav>
|
||||
|
||||
Reference in New Issue
Block a user