add web service
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.vakhrushev.me/av/remembos/internal/memory"
|
||||
)
|
||||
|
||||
//go:embed templates/*.html
|
||||
var templateFS embed.FS
|
||||
|
||||
var templates = template.Must(template.ParseFS(templateFS, "templates/*.html"))
|
||||
|
||||
type memoryData struct {
|
||||
DateFormatted string
|
||||
AgoText string
|
||||
Content string
|
||||
Tags []string
|
||||
Tier int
|
||||
ShowCount int
|
||||
}
|
||||
|
||||
type errorData struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
service *memory.Service
|
||||
logger *slog.Logger
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
func NewHandler(service *memory.Service, logger *slog.Logger) *Handler {
|
||||
h := &Handler{
|
||||
service: service,
|
||||
logger: logger,
|
||||
mux: http.NewServeMux(),
|
||||
}
|
||||
h.mux.HandleFunc("GET /", h.handleMemory)
|
||||
h.mux.HandleFunc("GET /health", h.handleHealth)
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (h *Handler) handleMemory(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
mem, err := h.service.GetTodayMemory(r.Context())
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get memory", "error", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
templates.ExecuteTemplate(w, "error.html", errorData{
|
||||
Message: "Не удалось загрузить воспоминание",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if mem == nil {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
templates.ExecuteTemplate(w, "error.html", errorData{
|
||||
Message: "Нет заметок для воспоминания",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
data := memoryData{
|
||||
DateFormatted: formatDate(mem.Date),
|
||||
AgoText: agoText(mem.Date),
|
||||
Content: mem.Memo.Content,
|
||||
Tags: mem.Memo.Tags,
|
||||
Tier: mem.Tier,
|
||||
ShowCount: mem.ShowCount,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := templates.ExecuteTemplate(w, "memory.html", data); err != nil {
|
||||
h.logger.Error("template render failed", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) handleHealth(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(w, "ok")
|
||||
}
|
||||
|
||||
var months = [...]string{
|
||||
1: "января", 2: "февраля", 3: "марта",
|
||||
4: "апреля", 5: "мая", 6: "июня",
|
||||
7: "июля", 8: "августа", 9: "сентября",
|
||||
10: "октября", 11: "ноября", 12: "декабря",
|
||||
}
|
||||
|
||||
func formatDate(t time.Time) string {
|
||||
return fmt.Sprintf("%d %s %d", t.Day(), months[t.Month()], t.Year())
|
||||
}
|
||||
|
||||
func agoText(t time.Time) string {
|
||||
now := time.Now()
|
||||
years := now.Year() - t.Year()
|
||||
monthsDiff := int(now.Month()) - int(t.Month())
|
||||
if monthsDiff < 0 {
|
||||
years--
|
||||
monthsDiff += 12
|
||||
}
|
||||
|
||||
if years > 0 {
|
||||
return fmt.Sprintf("%s назад", pluralYears(years))
|
||||
}
|
||||
if monthsDiff > 0 {
|
||||
return fmt.Sprintf("%s назад", pluralMonths(monthsDiff))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func pluralYears(n int) string {
|
||||
mod10 := n % 10
|
||||
mod100 := n % 100
|
||||
switch {
|
||||
case mod100 >= 11 && mod100 <= 14:
|
||||
return fmt.Sprintf("%d лет", n)
|
||||
case mod10 == 1:
|
||||
return fmt.Sprintf("%d год", n)
|
||||
case mod10 >= 2 && mod10 <= 4:
|
||||
return fmt.Sprintf("%d года", n)
|
||||
default:
|
||||
return fmt.Sprintf("%d лет", n)
|
||||
}
|
||||
}
|
||||
|
||||
func pluralMonths(n int) string {
|
||||
mod10 := n % 10
|
||||
mod100 := n % 100
|
||||
switch {
|
||||
case mod100 >= 11 && mod100 <= 14:
|
||||
return fmt.Sprintf("%d месяцев", n)
|
||||
case mod10 == 1:
|
||||
return fmt.Sprintf("%d месяц", n)
|
||||
case mod10 >= 2 && mod10 <= 4:
|
||||
return fmt.Sprintf("%d месяца", n)
|
||||
default:
|
||||
return fmt.Sprintf("%d месяцев", n)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Remembos</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
padding: 2rem 1rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.message {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="message">{{.Message}}</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Воспоминание</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
padding: 2rem 1rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.container {
|
||||
max-width: 640px;
|
||||
width: 100%;
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.date {
|
||||
font-size: 1.1rem;
|
||||
color: #666;
|
||||
}
|
||||
.ago {
|
||||
font-size: 0.9rem;
|
||||
color: #999;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
.content {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.tags {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.tag {
|
||||
background: #e8f0fe;
|
||||
color: #1a73e8;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.meta {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.8rem;
|
||||
color: #aaa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="date">{{.DateFormatted}}</div>
|
||||
{{if .AgoText}}<div class="ago">{{.AgoText}}</div>{{end}}
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="content">{{.Content}}</div>
|
||||
{{if .Tags}}
|
||||
<div class="tags">
|
||||
{{range .Tags}}<span class="tag">#{{.}}</span>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="meta">Tier {{.Tier}} · показов: {{.ShowCount}}</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user