feat: add title columns and migration tracking
This commit is contained in:
@@ -17,6 +17,56 @@ import (
|
|||||||
"malago/internal/templates"
|
"malago/internal/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func runMigrations(db *sql.DB) error {
|
||||||
|
// Create migration tracking table
|
||||||
|
_, err := db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS migration_version (
|
||||||
|
name TEXT PRIMARY KEY,
|
||||||
|
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
migrations := []string{
|
||||||
|
"migrations/001_init.sql",
|
||||||
|
"migrations/002_add_anime_titles.sql",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, migrationFile := range migrations {
|
||||||
|
// Check if migration already applied
|
||||||
|
var exists int
|
||||||
|
err := db.QueryRow("SELECT COUNT(*) FROM migration_version WHERE name = ?", migrationFile).Scan(&exists)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if exists > 0 {
|
||||||
|
log.Printf("migration %s already applied, skipping", migrationFile)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and execute migration
|
||||||
|
migrationSQL, err := os.ReadFile(migrationFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := db.Exec(string(migrationSQL)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as applied
|
||||||
|
_, err = db.Exec("INSERT INTO migration_version (name) VALUES (?)", migrationFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("migration %s applied successfully", migrationFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
dbFile := os.Getenv("DATABASE_FILE")
|
dbFile := os.Getenv("DATABASE_FILE")
|
||||||
if dbFile == "" {
|
if dbFile == "" {
|
||||||
@@ -29,12 +79,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
// Run migrations (assuming local dev setup, simplistic execution)
|
// Run migrations with tracking
|
||||||
migrationSQL, err := os.ReadFile("migrations/001_init.sql")
|
if err := runMigrations(db); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to read migrations: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := db.Exec(string(migrationSQL)); err != nil {
|
|
||||||
log.Fatalf("failed to run migrations: %v", err)
|
log.Fatalf("failed to run migrations: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
internal/database/helpers.go
Normal file
12
internal/database/helpers.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
// DisplayTitle returns the English title if available, otherwise Japanese, otherwise original
|
||||||
|
func (r GetUserWatchListRow) DisplayTitle() string {
|
||||||
|
if r.TitleEnglish.Valid && r.TitleEnglish.String != "" {
|
||||||
|
return r.TitleEnglish.String
|
||||||
|
}
|
||||||
|
if r.TitleJapanese.Valid && r.TitleJapanese.String != "" {
|
||||||
|
return r.TitleJapanese.String
|
||||||
|
}
|
||||||
|
return r.TitleOriginal
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,10 +18,12 @@ type Account struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Anime struct {
|
type Anime struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Title string `json:"title"`
|
TitleOriginal string `json:"title_original"`
|
||||||
ImageUrl string `json:"image_url"`
|
ImageUrl string `json:"image_url"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
TitleEnglish sql.NullString `json:"title_english"`
|
||||||
|
TitleJapanese sql.NullString `json:"title_japanese"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
|
|||||||
@@ -24,10 +24,12 @@ DELETE FROM session WHERE id = ?;
|
|||||||
DELETE FROM session WHERE user_id = ?;
|
DELETE FROM session WHERE user_id = ?;
|
||||||
|
|
||||||
-- name: UpsertAnime :one
|
-- name: UpsertAnime :one
|
||||||
INSERT INTO anime (id, title, image_url)
|
INSERT INTO anime (id, title_original, title_english, title_japanese, image_url)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
title = excluded.title,
|
title_original = excluded.title_original,
|
||||||
|
title_english = excluded.title_english,
|
||||||
|
title_japanese = excluded.title_japanese,
|
||||||
image_url = excluded.image_url
|
image_url = excluded.image_url
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
@@ -47,7 +49,12 @@ SELECT * FROM watch_list_entry
|
|||||||
WHERE user_id = ? AND anime_id = ? LIMIT 1;
|
WHERE user_id = ? AND anime_id = ? LIMIT 1;
|
||||||
|
|
||||||
-- name: GetUserWatchList :many
|
-- name: GetUserWatchList :many
|
||||||
SELECT e.*, a.title, a.image_url
|
SELECT
|
||||||
|
e.*,
|
||||||
|
a.title_original,
|
||||||
|
a.title_english,
|
||||||
|
a.title_japanese,
|
||||||
|
a.image_url
|
||||||
FROM watch_list_entry e
|
FROM watch_list_entry e
|
||||||
JOIN anime a ON e.anime_id = a.id
|
JOIN anime a ON e.anime_id = a.id
|
||||||
WHERE e.user_id = ?
|
WHERE e.user_id = ?
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -92,7 +93,7 @@ func (q *Queries) DeleteWatchListEntry(ctx context.Context, arg DeleteWatchListE
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getAnime = `-- name: GetAnime :one
|
const getAnime = `-- name: GetAnime :one
|
||||||
SELECT id, title, image_url, created_at FROM anime WHERE id = ? LIMIT 1
|
SELECT id, title_original, image_url, created_at, title_english, title_japanese FROM anime WHERE id = ? LIMIT 1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetAnime(ctx context.Context, id int64) (Anime, error) {
|
func (q *Queries) GetAnime(ctx context.Context, id int64) (Anime, error) {
|
||||||
@@ -100,9 +101,11 @@ func (q *Queries) GetAnime(ctx context.Context, id int64) (Anime, error) {
|
|||||||
var i Anime
|
var i Anime
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Title,
|
&i.TitleOriginal,
|
||||||
&i.ImageUrl,
|
&i.ImageUrl,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
|
&i.TitleEnglish,
|
||||||
|
&i.TitleJapanese,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -156,7 +159,12 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getUserWatchList = `-- name: GetUserWatchList :many
|
const getUserWatchList = `-- name: GetUserWatchList :many
|
||||||
SELECT e.id, e.user_id, e.anime_id, e.status, e.created_at, e.updated_at, a.title, a.image_url
|
SELECT
|
||||||
|
e.id, e.user_id, e.anime_id, e.status, e.created_at, e.updated_at,
|
||||||
|
a.title_original,
|
||||||
|
a.title_english,
|
||||||
|
a.title_japanese,
|
||||||
|
a.image_url
|
||||||
FROM watch_list_entry e
|
FROM watch_list_entry e
|
||||||
JOIN anime a ON e.anime_id = a.id
|
JOIN anime a ON e.anime_id = a.id
|
||||||
WHERE e.user_id = ?
|
WHERE e.user_id = ?
|
||||||
@@ -164,14 +172,16 @@ ORDER BY e.updated_at DESC
|
|||||||
`
|
`
|
||||||
|
|
||||||
type GetUserWatchListRow struct {
|
type GetUserWatchListRow struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
AnimeID int64 `json:"anime_id"`
|
AnimeID int64 `json:"anime_id"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Title string `json:"title"`
|
TitleOriginal string `json:"title_original"`
|
||||||
ImageUrl string `json:"image_url"`
|
TitleEnglish sql.NullString `json:"title_english"`
|
||||||
|
TitleJapanese sql.NullString `json:"title_japanese"`
|
||||||
|
ImageUrl string `json:"image_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetUserWatchList(ctx context.Context, userID string) ([]GetUserWatchListRow, error) {
|
func (q *Queries) GetUserWatchList(ctx context.Context, userID string) ([]GetUserWatchListRow, error) {
|
||||||
@@ -190,7 +200,9 @@ func (q *Queries) GetUserWatchList(ctx context.Context, userID string) ([]GetUse
|
|||||||
&i.Status,
|
&i.Status,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.Title,
|
&i.TitleOriginal,
|
||||||
|
&i.TitleEnglish,
|
||||||
|
&i.TitleJapanese,
|
||||||
&i.ImageUrl,
|
&i.ImageUrl,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -231,28 +243,40 @@ func (q *Queries) GetWatchListEntry(ctx context.Context, arg GetWatchListEntryPa
|
|||||||
}
|
}
|
||||||
|
|
||||||
const upsertAnime = `-- name: UpsertAnime :one
|
const upsertAnime = `-- name: UpsertAnime :one
|
||||||
INSERT INTO anime (id, title, image_url)
|
INSERT INTO anime (id, title_original, title_english, title_japanese, image_url)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
title = excluded.title,
|
title_original = excluded.title_original,
|
||||||
|
title_english = excluded.title_english,
|
||||||
|
title_japanese = excluded.title_japanese,
|
||||||
image_url = excluded.image_url
|
image_url = excluded.image_url
|
||||||
RETURNING id, title, image_url, created_at
|
RETURNING id, title_original, image_url, created_at, title_english, title_japanese
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpsertAnimeParams struct {
|
type UpsertAnimeParams struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Title string `json:"title"`
|
TitleOriginal string `json:"title_original"`
|
||||||
ImageUrl string `json:"image_url"`
|
TitleEnglish sql.NullString `json:"title_english"`
|
||||||
|
TitleJapanese sql.NullString `json:"title_japanese"`
|
||||||
|
ImageUrl string `json:"image_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) UpsertAnime(ctx context.Context, arg UpsertAnimeParams) (Anime, error) {
|
func (q *Queries) UpsertAnime(ctx context.Context, arg UpsertAnimeParams) (Anime, error) {
|
||||||
row := q.db.QueryRowContext(ctx, upsertAnime, arg.ID, arg.Title, arg.ImageUrl)
|
row := q.db.QueryRowContext(ctx, upsertAnime,
|
||||||
|
arg.ID,
|
||||||
|
arg.TitleOriginal,
|
||||||
|
arg.TitleEnglish,
|
||||||
|
arg.TitleJapanese,
|
||||||
|
arg.ImageUrl,
|
||||||
|
)
|
||||||
var i Anime
|
var i Anime
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Title,
|
&i.TitleOriginal,
|
||||||
&i.ImageUrl,
|
&i.ImageUrl,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
|
&i.TitleEnglish,
|
||||||
|
&i.TitleJapanese,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -42,6 +43,8 @@ func (h *WatchlistHandler) HandleUpdateWatchlist(w http.ResponseWriter, r *http.
|
|||||||
|
|
||||||
animeIDStr := r.FormValue("anime_id")
|
animeIDStr := r.FormValue("anime_id")
|
||||||
animeTitle := r.FormValue("anime_title")
|
animeTitle := r.FormValue("anime_title")
|
||||||
|
animeTitleEnglish := r.FormValue("anime_title_english")
|
||||||
|
animeTitleJapanese := r.FormValue("anime_title_japanese")
|
||||||
animeImage := r.FormValue("anime_image")
|
animeImage := r.FormValue("anime_image")
|
||||||
status := r.FormValue("status")
|
status := r.FormValue("status")
|
||||||
|
|
||||||
@@ -53,9 +56,11 @@ func (h *WatchlistHandler) HandleUpdateWatchlist(w http.ResponseWriter, r *http.
|
|||||||
|
|
||||||
// Ensure the anime exists in our local DB first (foreign key constraint)
|
// Ensure the anime exists in our local DB first (foreign key constraint)
|
||||||
_, err = h.db.UpsertAnime(r.Context(), database.UpsertAnimeParams{
|
_, err = h.db.UpsertAnime(r.Context(), database.UpsertAnimeParams{
|
||||||
ID: animeID,
|
ID: animeID,
|
||||||
Title: animeTitle,
|
TitleOriginal: animeTitle,
|
||||||
ImageUrl: animeImage,
|
TitleEnglish: sql.NullString{String: animeTitleEnglish, Valid: animeTitleEnglish != ""},
|
||||||
|
TitleJapanese: sql.NullString{String: animeTitleJapanese, Valid: animeTitleJapanese != ""},
|
||||||
|
ImageUrl: animeImage,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("failed to save anime reference: %v", err), http.StatusInternalServerError)
|
http.Error(w, fmt.Sprintf("failed to save anime reference: %v", err), http.StatusInternalServerError)
|
||||||
@@ -75,7 +80,16 @@ func (h *WatchlistHandler) HandleUpdateWatchlist(w http.ResponseWriter, r *http.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
templates.WatchlistDropdown(int(animeID), animeTitle, animeImage, status).Render(r.Context(), w)
|
// Determine display title (prefer English)
|
||||||
|
displayTitle := animeTitleEnglish
|
||||||
|
if displayTitle == "" {
|
||||||
|
displayTitle = animeTitleJapanese
|
||||||
|
}
|
||||||
|
if displayTitle == "" {
|
||||||
|
displayTitle = animeTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
templates.WatchlistDropdown(int(animeID), displayTitle, animeImage, status).Render(r.Context(), w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *WatchlistHandler) HandleDeleteWatchlist(w http.ResponseWriter, r *http.Request) {
|
func (h *WatchlistHandler) HandleDeleteWatchlist(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -121,8 +135,18 @@ func (h *WatchlistHandler) HandleDeleteWatchlist(w http.ResponseWriter, r *http.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine display title for dropdown (prefer English)
|
||||||
|
displayTitle := ""
|
||||||
|
if anime.TitleEnglish.Valid && anime.TitleEnglish.String != "" {
|
||||||
|
displayTitle = anime.TitleEnglish.String
|
||||||
|
} else if anime.TitleJapanese.Valid && anime.TitleJapanese.String != "" {
|
||||||
|
displayTitle = anime.TitleJapanese.String
|
||||||
|
} else {
|
||||||
|
displayTitle = anime.TitleOriginal
|
||||||
|
}
|
||||||
|
|
||||||
// Otherwise return updated dropdown for anime page
|
// Otherwise return updated dropdown for anime page
|
||||||
templates.WatchlistDropdown(int(animeID), anime.Title, anime.ImageUrl, "").Render(r.Context(), w)
|
templates.WatchlistDropdown(int(animeID), displayTitle, anime.ImageUrl, "").Render(r.Context(), w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *WatchlistHandler) HandleGetWatchlist(w http.ResponseWriter, r *http.Request) {
|
func (h *WatchlistHandler) HandleGetWatchlist(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -206,7 +230,7 @@ func (h *WatchlistHandler) HandleExportWatchlist(w http.ResponseWriter, r *http.
|
|||||||
for i, entry := range entries {
|
for i, entry := range entries {
|
||||||
export.Entries[i] = WatchlistExportEntry{
|
export.Entries[i] = WatchlistExportEntry{
|
||||||
AnimeID: entry.AnimeID,
|
AnimeID: entry.AnimeID,
|
||||||
Title: entry.Title,
|
Title: entry.DisplayTitle(),
|
||||||
ImageURL: entry.ImageUrl,
|
ImageURL: entry.ImageUrl,
|
||||||
Status: entry.Status,
|
Status: entry.Status,
|
||||||
UpdatedAt: entry.UpdatedAt.Format(time.RFC3339),
|
UpdatedAt: entry.UpdatedAt.Format(time.RFC3339),
|
||||||
@@ -251,11 +275,13 @@ func (h *WatchlistHandler) HandleImportWatchlist(w http.ResponseWriter, r *http.
|
|||||||
|
|
||||||
imported := 0
|
imported := 0
|
||||||
for _, entry := range export.Entries {
|
for _, entry := range export.Entries {
|
||||||
// Upsert anime
|
// Upsert anime - store title as original (we don't know which type it is from export)
|
||||||
_, err := h.db.UpsertAnime(r.Context(), database.UpsertAnimeParams{
|
_, err := h.db.UpsertAnime(r.Context(), database.UpsertAnimeParams{
|
||||||
ID: entry.AnimeID,
|
ID: entry.AnimeID,
|
||||||
Title: entry.Title,
|
TitleOriginal: entry.Title,
|
||||||
ImageUrl: entry.ImageURL,
|
TitleEnglish: sql.NullString{},
|
||||||
|
TitleJapanese: sql.NullString{},
|
||||||
|
ImageUrl: entry.ImageURL,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ templ statusOption(anime jikan.Anime, status string, currentStatus string) {
|
|||||||
<button
|
<button
|
||||||
class={ "dropdown-item", templ.KV("active", status == currentStatus) }
|
class={ "dropdown-item", templ.KV("active", status == currentStatus) }
|
||||||
hx-post="/api/watchlist"
|
hx-post="/api/watchlist"
|
||||||
hx-vals={ fmt.Sprintf(`{"anime_id": "%d", "anime_title": "%s", "anime_image": "%s", "status": "%s"}`, anime.MalID, anime.DisplayTitle(), anime.ImageURL(), status) }
|
hx-vals={ fmt.Sprintf(`{"anime_id": "%d", "anime_title": "%s", "anime_title_english": "%s", "anime_title_japanese": "%s", "anime_image": "%s", "status": "%s"}`, anime.MalID, anime.Title, anime.TitleEnglish, anime.TitleJapanese, anime.ImageURL(), status) }
|
||||||
hx-target="#watchlist-dropdown"
|
hx-target="#watchlist-dropdown"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -700,9 +700,9 @@ func statusOption(anime jikan.Anime, status string, currentStatus string) templ.
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var34 string
|
var templ_7745c5c3_Var34 string
|
||||||
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(`{"anime_id": "%d", "anime_title": "%s", "anime_image": "%s", "status": "%s"}`, anime.MalID, anime.DisplayTitle(), anime.ImageURL(), status))
|
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(`{"anime_id": "%d", "anime_title": "%s", "anime_title_english": "%s", "anime_title_japanese": "%s", "anime_image": "%s", "status": "%s"}`, anime.MalID, anime.Title, anime.TitleEnglish, anime.TitleJapanese, anime.ImageURL(), status))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/anime.templ`, Line: 197, Col: 164}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/anime.templ`, Line: 197, Col: 255}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|||||||
@@ -49,12 +49,12 @@ templ Watchlist(entries []database.GetUserWatchListRow, layout string, currentSt
|
|||||||
<div class="catalog-item watchlist-item" id={ fmt.Sprintf("watchlist-entry-%d", entry.AnimeID) }>
|
<div class="catalog-item watchlist-item" id={ fmt.Sprintf("watchlist-entry-%d", entry.AnimeID) }>
|
||||||
<a href={ templ.URL(fmt.Sprintf("/anime/%d", entry.AnimeID)) }>
|
<a href={ templ.URL(fmt.Sprintf("/anime/%d", entry.AnimeID)) }>
|
||||||
if entry.ImageUrl != "" {
|
if entry.ImageUrl != "" {
|
||||||
<img src={ entry.ImageUrl } alt={ entry.Title } class="catalog-thumb" loading="lazy" />
|
<img src={ entry.ImageUrl } alt={ entry.DisplayTitle() } class="catalog-thumb" loading="lazy" />
|
||||||
} else {
|
} else {
|
||||||
<div class="no-image">no image</div>
|
<div class="no-image">no image</div>
|
||||||
}
|
}
|
||||||
</a>
|
</a>
|
||||||
<div class="catalog-title">{ entry.Title }</div>
|
<div class="catalog-title">{ entry.DisplayTitle() }</div>
|
||||||
<div class="watchlist-status">{ entry.Status }</div>
|
<div class="watchlist-status">{ entry.Status }</div>
|
||||||
<button
|
<button
|
||||||
class="remove-btn"
|
class="remove-btn"
|
||||||
@@ -80,12 +80,12 @@ templ Watchlist(entries []database.GetUserWatchListRow, layout string, currentSt
|
|||||||
<tr id={ fmt.Sprintf("watchlist-entry-%d", entry.AnimeID) }>
|
<tr id={ fmt.Sprintf("watchlist-entry-%d", entry.AnimeID) }>
|
||||||
<td>
|
<td>
|
||||||
<a href={ templ.SafeURL(fmt.Sprintf("/anime/%d", entry.AnimeID)) }>
|
<a href={ templ.SafeURL(fmt.Sprintf("/anime/%d", entry.AnimeID)) }>
|
||||||
<img src={ entry.ImageUrl } alt={ entry.Title } class="thumb" loading="lazy"/>
|
<img src={ entry.ImageUrl } alt={ entry.DisplayTitle() } class="thumb" loading="lazy"/>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="title-cell">
|
<td class="title-cell">
|
||||||
<a href={ templ.SafeURL(fmt.Sprintf("/anime/%d", entry.AnimeID)) }>
|
<a href={ templ.SafeURL(fmt.Sprintf("/anime/%d", entry.AnimeID)) }>
|
||||||
{ entry.Title }
|
{ entry.DisplayTitle() }
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="status-cell">{ entry.Status }</td>
|
<td class="status-cell">{ entry.Status }</td>
|
||||||
|
|||||||
@@ -406,9 +406,9 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var30 string
|
var templ_7745c5c3_Var30 string
|
||||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Title)
|
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(entry.DisplayTitle())
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 52, Col: 54}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 52, Col: 63}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -429,9 +429,9 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var31 string
|
var templ_7745c5c3_Var31 string
|
||||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Title)
|
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(entry.DisplayTitle())
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 57, Col: 47}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 57, Col: 56}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -535,9 +535,9 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var38 string
|
var templ_7745c5c3_Var38 string
|
||||||
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Title)
|
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(entry.DisplayTitle())
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 83, Col: 55}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 83, Col: 64}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -561,9 +561,9 @@ func Watchlist(entries []database.GetUserWatchListRow, layout string, currentSta
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var40 string
|
var templ_7745c5c3_Var40 string
|
||||||
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Title)
|
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(entry.DisplayTitle())
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 88, Col: 23}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/watchlist.templ`, Line: 88, Col: 32}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|||||||
6
migrations/002_add_anime_titles.sql
Normal file
6
migrations/002_add_anime_titles.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
-- Add English and Japanese title columns to anime table
|
||||||
|
ALTER TABLE anime ADD COLUMN title_english TEXT;
|
||||||
|
ALTER TABLE anime ADD COLUMN title_japanese TEXT;
|
||||||
|
|
||||||
|
-- Rename existing title to title_original for clarity
|
||||||
|
ALTER TABLE anime RENAME COLUMN title TO title_original;
|
||||||
Reference in New Issue
Block a user