diff --git a/docs/TODO.md b/docs/TODO.md new file mode 100644 index 0000000..6ea11d8 --- /dev/null +++ b/docs/TODO.md @@ -0,0 +1,13 @@ +# Goondex TODO / DONE + +## TODO +- [ ] Implement bulk studio import (`./goondex import all-studios`) with the same pagination/resume flow as the performer importer. +- [ ] Implement bulk scene import (`./goondex import all-scenes`) and wire the CLI/UI to the new data set. +- [ ] Build a movie ingest path (TPDB and/or Adult Empire) that feeds the `movies` tables and populates the movies pages. +- [ ] Align the web stack on a single CSS pipeline (deprecate legacy `style.css`, keep goondex + scoped component files). +- [ ] Add lightweight UI validation (lint/smoke tests) for navigation, modals, and search to catch regressions early. + +## DONE +- [x] Split card styling into per-context files (base, performers, studios, scenes) and updated listing templates to use them. +- [x] Created shared task lists (`docs/TODO.md`, `docs/WEB_TODO.md`) to keep engineering and web work in sync. +- [x] Adult Empire scraper + TPDB merge support for performers (see `SESSION_SUMMARY_v0.1.0-dev4.md`). diff --git a/docs/WEB_TODO.md b/docs/WEB_TODO.md new file mode 100644 index 0000000..c196fb1 --- /dev/null +++ b/docs/WEB_TODO.md @@ -0,0 +1,11 @@ +# Web TODO / DONE + +## TODO +- [ ] Split remaining shared CSS (hero, navbar, search, stats, forms, modals) into scoped files and retire inline styles in templates. +- [ ] Migrate detail pages to the same CSS pipeline as listings (drop `style.css` in favor of scoped goondex files). +- [ ] Audit cards for movies once movie data lands and add a dedicated `cards-movie.css`. +- [ ] Add a short usage note for the GX component set (which classes/components we rely on and where). + +## DONE +- [x] Added per-context card styles (`cards/card-base.css`, `cards/cards-performer.css`, `cards/cards-studio.css`, `cards/cards-scene.css`) and wired listing templates. +- [x] Cleaned up CSS imports in `internal/web/static/css/goondex.css` to reference real files only. diff --git a/internal/web/server.go b/internal/web/server.go index e62e376..2d82ae4 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -6,11 +6,11 @@ import ( "encoding/json" "fmt" "html/template" + "io/fs" "net/http" "os" "strconv" "time" - "io/fs" "git.leaktechnologies.dev/stu/Goondex/internal/db" import_service "git.leaktechnologies.dev/stu/Goondex/internal/import" @@ -59,10 +59,10 @@ func (s *Server) Start() error { mux.Handle( "/static/", - http.StripPrefix( - "/static/", - http.FileServer(http.FS(staticFS)), - ), + http.StripPrefix( + "/static/", + http.FileServer(http.FS(staticFS)), + ), ) // ============================================================================ @@ -127,6 +127,8 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) { movies, _ := movieStore.Search("") data := map[string]interface{}{ + "PageTitle": "Dashboard", + "ActivePage": "dashboard", "PerformerCount": len(performers), "StudioCount": len(studios), "SceneCount": len(scenes), @@ -218,10 +220,12 @@ func (s *Server) handlePerformerList(w http.ResponseWriter, r *http.Request) { } data := map[string]interface{}{ - "Performers": performersWithCounts, - "Query": query, - "Nationalities": nationalities, - "Genders": genders, + "PageTitle": "Performers", + "ActivePage": "performers", + "Performers": performersWithCounts, + "Query": query, + "Nationalities": nationalities, + "Genders": genders, "SelectedNationality": nationalityFilter, "SelectedGender": genderFilter, } @@ -317,9 +321,12 @@ func (s *Server) handlePerformerDetail(w http.ResponseWriter, r *http.Request) { scenes, _ := sceneStore.GetByPerformer(id) data := map[string]interface{}{ - "Performer": performer, - "SceneCount": sceneCount, - "Scenes": scenes, + "PageTitle": fmt.Sprintf("Performer: %s", performer.Name), + "ActivePage": "performers", + "Stylesheets": []string{"/static/css/style.css", "/static/css/goondex.css"}, + "Performer": performer, + "SceneCount": sceneCount, + "Scenes": scenes, } s.templates.ExecuteTemplate(w, "performer_detail.html", data) @@ -344,12 +351,14 @@ func (s *Server) handleStudioList(w http.ResponseWriter, r *http.Request) { for _, st := range studios { count, _ := store.GetSceneCount(st.ID) studiosWithCounts = append(studiosWithCounts, - StudioWithCount{Studio: st, SceneCount: count}) + StudioWithCount{Studio: st, SceneCount: count}) } data := map[string]interface{}{ - "Studios": studiosWithCounts, - "Query": query, + "PageTitle": "Studios", + "ActivePage": "studios", + "Studios": studiosWithCounts, + "Query": query, } s.templates.ExecuteTemplate(w, "studios.html", data) @@ -373,8 +382,11 @@ func (s *Server) handleStudioDetail(w http.ResponseWriter, r *http.Request) { sceneCount, _ := store.GetSceneCount(id) data := map[string]interface{}{ - "Studio": studio, - "SceneCount": sceneCount, + "PageTitle": fmt.Sprintf("Studio: %s", studio.Name), + "ActivePage": "studios", + "Stylesheets": []string{"/static/css/style.css", "/static/css/goondex.css"}, + "Studio": studio, + "SceneCount": sceneCount, } s.templates.ExecuteTemplate(w, "studio_detail.html", data) @@ -408,12 +420,14 @@ func (s *Server) handleSceneList(w http.ResponseWriter, r *http.Request) { } scenesWithStudios = append(scenesWithStudios, - SceneWithStudio{Scene: sc, StudioName: studioName}) + SceneWithStudio{Scene: sc, StudioName: studioName}) } data := map[string]interface{}{ - "Scenes": scenesWithStudios, - "Query": query, + "PageTitle": "Scenes", + "ActivePage": "scenes", + "Scenes": scenesWithStudios, + "Query": query, } s.templates.ExecuteTemplate(w, "scenes.html", data) @@ -448,11 +462,14 @@ func (s *Server) handleSceneDetail(w http.ResponseWriter, r *http.Request) { } data := map[string]interface{}{ - "Scene": scene, - "Performers": performers, - "Tags": tags, - "Movies": movies, - "StudioName": studioName, + "PageTitle": fmt.Sprintf("Scene: %s", scene.Title), + "ActivePage": "scenes", + "Stylesheets": []string{"/static/css/style.css", "/static/css/goondex.css"}, + "Scene": scene, + "Performers": performers, + "Tags": tags, + "Movies": movies, + "StudioName": studioName, } s.templates.ExecuteTemplate(w, "scene_detail.html", data) @@ -492,8 +509,10 @@ func (s *Server) handleMovieList(w http.ResponseWriter, r *http.Request) { } data := map[string]interface{}{ - "Movies": moviesWithDetails, - "Query": query, + "PageTitle": "Movies", + "ActivePage": "movies", + "Movies": moviesWithDetails, + "Query": query, } s.templates.ExecuteTemplate(w, "movies.html", data) @@ -526,6 +545,8 @@ func (s *Server) handleMovieDetail(w http.ResponseWriter, r *http.Request) { } data := map[string]interface{}{ + "PageTitle": fmt.Sprintf("Movie: %s", movie.Title), + "ActivePage": "movies", "Movie": movie, "Scenes": scenes, "StudioName": studioName, @@ -594,7 +615,7 @@ func (s *Server) handleAPIImportPerformer(w http.ResponseWriter, r *http.Request json.NewEncoder(w).Encode(APIResponse{ Success: true, Message: fmt.Sprintf("Imported %d performer(s)", imported), - Data: map[string]int{"imported": imported, "found": len(performers)}, + Data: map[string]int{"imported": imported, "found": len(performers)}, }) } @@ -644,7 +665,7 @@ func (s *Server) handleAPIImportStudio(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(APIResponse{ Success: true, Message: fmt.Sprintf("Imported %d studio(s)", imported), - Data: map[string]int{"imported": imported, "found": len(studios)}, + Data: map[string]int{"imported": imported, "found": len(studios)}, }) } @@ -737,7 +758,7 @@ func (s *Server) handleAPIImportScene(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(APIResponse{ Success: true, Message: fmt.Sprintf("Imported %d scene(s)", imported), - Data: map[string]int{"imported": imported, "found": len(scenes)}, + Data: map[string]int{"imported": imported, "found": len(scenes)}, }) } @@ -856,7 +877,7 @@ func (s *Server) handleAPIBulkImportPerformers(w http.ResponseWriter, r *http.Re json.NewEncoder(w).Encode(APIResponse{ Success: true, Message: fmt.Sprintf("Imported %d/%d performers", result.Imported, result.Total), - Data: result, + Data: result, }) } @@ -886,7 +907,7 @@ func (s *Server) handleAPIBulkImportStudios(w http.ResponseWriter, r *http.Reque json.NewEncoder(w).Encode(APIResponse{ Success: true, Message: fmt.Sprintf("Imported %d/%d studios", result.Imported, result.Total), - Data: result, + Data: result, }) } @@ -916,7 +937,7 @@ func (s *Server) handleAPIBulkImportScenes(w http.ResponseWriter, r *http.Reques json.NewEncoder(w).Encode(APIResponse{ Success: true, Message: fmt.Sprintf("Imported %d/%d scenes", result.Imported, result.Total), - Data: result, + Data: result, }) } @@ -1084,6 +1105,6 @@ func (s *Server) handleAPIGlobalSearch(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(APIResponse{ Success: true, Message: fmt.Sprintf("Found %d results", results["total"]), - Data: results, + Data: results, }) } diff --git a/internal/web/static/css/cards/card-base.css b/internal/web/static/css/cards/card-base.css new file mode 100644 index 0000000..f0a0bad --- /dev/null +++ b/internal/web/static/css/cards/card-base.css @@ -0,0 +1,103 @@ +/* + * GOONDEX CARD BASE + * Shared grid + card shell used by performer/studio/scene listings. + */ + +:root { + --gx-card-thumb-ratio: 3 / 4; + --gx-card-min-width: 250px; +} + +.gx-card-grid { + display: grid; + gap: 1.6rem; + padding: 1rem 0; + grid-template-columns: repeat(auto-fill, minmax(var(--gx-card-min-width), 1fr)); +} + +.gx-card { + background: var(--color-bg-card); + border: 1px solid var(--color-border-soft); + border-radius: var(--radius-soft); + overflow: hidden; + + box-shadow: var(--shadow-elevated); + transition: + transform var(--transition), + box-shadow var(--transition), + border-color var(--transition); + + cursor: pointer; + position: relative; +} + +.gx-card:hover { + transform: translateY(-4px); + border-color: var(--color-brand); + box-shadow: + 0 0 18px rgba(255, 79, 163, 0.28), + 0 6px 24px rgba(0, 0, 0, 0.55); +} + +.gx-card-thumb { + width: 100%; + aspect-ratio: var(--gx-card-thumb-ratio); + background-size: cover; + background-position: center; + filter: brightness(0.92); + transition: filter var(--transition-fast); +} + +.gx-card:hover .gx-card-thumb { + filter: brightness(1); +} + +.gx-card-body { + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.gx-card-title { + font-size: 1.1rem; + font-weight: 600; + + background: linear-gradient(135deg, var(--color-text-primary), var(--color-header)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.gx-card-meta { + font-size: 0.85rem; + color: var(--color-text-secondary); + opacity: 0.9; +} + +.gx-card-tags { + margin-top: 0.7rem; + display: flex; + flex-wrap: wrap; + gap: 0.4rem; +} + +.gx-card-tag { + padding: 0.2rem 0.55rem; + font-size: 0.75rem; + border-radius: var(--radius); + background: rgba(255, 79, 163, 0.08); + color: var(--color-brand); + border: 1px solid rgba(255, 79, 163, 0.25); + text-transform: uppercase; + letter-spacing: 0.03em; +} + +@media (max-width: 550px) { + .gx-card-grid { + grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); + } + + .gx-card-title { + font-size: 1rem; + } +} diff --git a/internal/web/static/css/cards/cards-movie.css b/internal/web/static/css/cards/cards-movie.css new file mode 100644 index 0000000..0882d95 --- /dev/null +++ b/internal/web/static/css/cards/cards-movie.css @@ -0,0 +1,16 @@ +/* + * GOONDEX MOVIE CARDS + * Poster-focused layout for movie listings. + */ + +.movie-card-grid { + --gx-card-thumb-ratio: 2 / 3; +} + +.movie-card .gx-card-meta + .gx-card-meta { + margin-top: 0.25rem; +} + +.movie-card .gx-card-tags { + margin-top: 0.6rem; +} diff --git a/internal/web/static/css/cards/cards-performer.css b/internal/web/static/css/cards/cards-performer.css new file mode 100644 index 0000000..b5e5fe9 --- /dev/null +++ b/internal/web/static/css/cards/cards-performer.css @@ -0,0 +1,16 @@ +/* + * GOONDEX PERFORMER CARDS + * Portrait-focused layout for performer listings. + */ + +.performer-card-grid { + --gx-card-thumb-ratio: 3 / 4; +} + +.performer-card .gx-card-meta + .gx-card-meta { + margin-top: 0.3rem; +} + +.performer-card .gx-card-tags { + margin-top: 0.6rem; +} diff --git a/internal/web/static/css/cards/cards-scene.css b/internal/web/static/css/cards/cards-scene.css new file mode 100644 index 0000000..e8361ec --- /dev/null +++ b/internal/web/static/css/cards/cards-scene.css @@ -0,0 +1,16 @@ +/* + * GOONDEX SCENE CARDS + * Landscape-focused layout for scene listings. + */ + +.scene-card-grid { + --gx-card-thumb-ratio: 16 / 9; +} + +.scene-card .gx-card-meta + .gx-card-meta { + margin-top: 0.25rem; +} + +.scene-card .gx-card-tags { + margin-top: 0.6rem; +} diff --git a/internal/web/static/css/cards/cards-studio.css b/internal/web/static/css/cards/cards-studio.css new file mode 100644 index 0000000..afeaaa7 --- /dev/null +++ b/internal/web/static/css/cards/cards-studio.css @@ -0,0 +1,23 @@ +/* + * GOONDEX STUDIO CARDS + * Studio listings with compact description support. + */ + +.studio-card-grid { + --gx-card-thumb-ratio: 3 / 4; +} + +.studio-card .studio-card-description { + margin-top: 0.5rem; + font-size: 0.85rem; + color: var(--color-text-secondary); + opacity: 0.85; + line-height: 1.4; + max-height: 2.8rem; + overflow: hidden; + text-overflow: ellipsis; +} + +.studio-card .gx-card-tags { + margin-top: 0.6rem; +} diff --git a/internal/web/static/css/goondex.css b/internal/web/static/css/goondex.css index 9b2a5e1..ed06e39 100644 --- a/internal/web/static/css/goondex.css +++ b/internal/web/static/css/goondex.css @@ -5,7 +5,6 @@ /* ===== GX COMPONENT LIBRARY ================================================= */ @import 'gx/GX_Button.css'; -@import 'gx/GX_CardGrid.css'; @import 'gx/GX_Checkbox.css'; @import 'gx/GX_Input.css'; @import 'gx/GX_Loader.css'; @@ -15,18 +14,18 @@ /* ===== LAYOUT & STRUCTURE =================================================== */ @import 'layout.css'; -@import 'navbar.css'; -@import 'sidepanels.css'; -/* ===== PAGE-LEVEL COMPONENTS ================================================ */ -@import 'hero.css'; -@import 'stats.css'; +/* ===== CORE COMPONENTS ===================================================== */ @import 'forms.css'; @import 'buttons.css'; @import 'components.css'; +/* ===== CARDS (SCOPED BY CONTEXT) =========================================== */ +@import 'cards/card-base.css'; +@import 'cards/cards-performer.css'; +@import 'cards/cards-studio.css'; +@import 'cards/cards-scene.css'; +@import 'cards/cards-movie.css'; + /* ===== GLOBAL PAGE STYLES =================================================== */ @import 'pages.css'; - -/* ===== RESPONSIVE OVERRIDES (MOBILE/TABLET/HALF-SCREEN) ===================== */ -@import 'responsive.css'; diff --git a/internal/web/static/css/gx/GX_CardGrid.css b/internal/web/static/css/gx/GX_CardGrid.css index b62155b..8b54b1f 100644 --- a/internal/web/static/css/gx/GX_CardGrid.css +++ b/internal/web/static/css/gx/GX_CardGrid.css @@ -1,103 +1,11 @@ /* - * GX CARD GRID — Performer / Studio / Scene cards - * Dark luxury aesthetic, Flamingo Pink medium glow, responsive columns + * GX CARD GRID (COMPAT) + * This file now re-exports the scoped card styles. + * Prefer importing /static/css/cards/*.css directly. */ -/* WRAPPER */ -.gx-card-grid { - display: grid; - gap: 1.6rem; - padding: 1rem 0; - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); -} - -/* CARD */ -.gx-card { - background: var(--color-bg-card); - border: 1px solid var(--color-border-soft); - border-radius: var(--radius-soft); - overflow: hidden; - - box-shadow: var(--shadow-elevated); - transition: transform var(--transition), - box-shadow var(--transition), - border-color var(--transition); - - cursor: pointer; - position: relative; -} - -/* HOVER EFFECT */ -.gx-card:hover { - transform: translateY(-4px); - border-color: var(--color-brand); - box-shadow: 0 0 18px rgba(255, 79, 163, 0.28), - 0 6px 24px rgba(0, 0, 0, 0.55); -} - -/* THUMBNAIL */ -.gx-card-thumb { - width: 100%; - aspect-ratio: 3 / 4; - background-size: cover; - background-position: center; - filter: brightness(0.92); - transition: filter var(--transition-fast); -} - -.gx-card:hover .gx-card-thumb { - filter: brightness(1); -} - -/* CONTENT */ -.gx-card-body { - padding: 1rem; -} - -/* TITLE */ -.gx-card-title { - font-size: 1.1rem; - font-weight: 600; - margin-bottom: 0.35rem; - - background: linear-gradient(135deg, var(--color-text-primary), var(--color-header)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; -} - -/* SMALL META (scene count, category, etc.) */ -.gx-card-meta { - font-size: 0.85rem; - color: var(--color-text-secondary); - opacity: 0.9; -} - -/* TAGS inside cards (optional) */ -.gx-card-tags { - margin-top: 0.8rem; - display: flex; - flex-wrap: wrap; - gap: 0.4rem; -} - -.gx-card-tag { - padding: 0.2rem 0.55rem; - font-size: 0.75rem; - border-radius: var(--radius); - background: rgba(255, 79, 163, 0.08); - color: var(--color-brand); - border: 1px solid rgba(255, 79, 163, 0.25); - text-transform: uppercase; - letter-spacing: 0.03em; -} - -/* MOBILE OPTIMISATION */ -@media (max-width: 550px) { - .gx-card-grid { - grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); - } - - .gx-card-title { - font-size: 1rem; - } -} +@import '../cards/card-base.css'; +@import '../cards/cards-performer.css'; +@import '../cards/cards-studio.css'; +@import '../cards/cards-scene.css'; +@import '../cards/cards-movie.css'; diff --git a/internal/web/static/css/gx/GX_CardGrid.html b/internal/web/static/css/gx/GX_CardGrid.html index de8ce1a..0bff653 100644 --- a/internal/web/static/css/gx/GX_CardGrid.html +++ b/internal/web/static/css/gx/GX_CardGrid.html @@ -1,7 +1,7 @@ -
+
{{range .Performers}} -
+
diff --git a/internal/web/static/css/layout.css b/internal/web/static/css/layout.css index b1f9e3b..f081c01 100644 --- a/internal/web/static/css/layout.css +++ b/internal/web/static/css/layout.css @@ -80,6 +80,20 @@ body { justify-content: space-between; } +/* Bootstrap navbar controls */ +.navbar .navbar-toggler { + border-color: var(--color-border-soft); + padding: 0.35rem 0.5rem; +} + +.navbar .navbar-toggler:focus { + box-shadow: none; +} + +.navbar .navbar-toggler-icon { + filter: invert(1); +} + /* Logo image control */ .logo-img { height: 42px; @@ -264,4 +278,17 @@ body { .stats-grid { grid-template-columns: 1fr; } + + .navbar .collapse { + padding-top: 0.75rem; + } + + .nav-links { + flex-direction: column; + gap: 0.75rem; + } + + .nav-links .nav-link { + padding: 0.35rem 0; + } } diff --git a/internal/web/templates/dashboard.html b/internal/web/templates/dashboard.html index 9790a81..d826077 100644 --- a/internal/web/templates/dashboard.html +++ b/internal/web/templates/dashboard.html @@ -1,10 +1,7 @@ - - - Goondex - Dashboard - + {{template "html-head" .}} + {{template "html-head" .}} - + {{template "navbar" .}}
{{if .Scenes}} -
+
{{range .Scenes}} -
+
@@ -51,12 +31,12 @@ {{end}} {{if .StudioName}} -
+
🏢 {{.StudioName}}
{{end}} -
+
{{if .Scene.Code}} {{.Scene.Code}} {{end}} @@ -79,5 +59,6 @@
{{end}}
+ {{template "html-scripts" .}} diff --git a/internal/web/templates/studio_detail.html b/internal/web/templates/studio_detail.html index 4d93107..900252b 100644 --- a/internal/web/templates/studio_detail.html +++ b/internal/web/templates/studio_detail.html @@ -1,24 +1,10 @@ - - - {{.Studio.Name}} - Goondex - + {{template "html-head" .}} - + {{template "navbar" .}}
+ {{template "html-scripts" .}} diff --git a/internal/web/templates/studios.html b/internal/web/templates/studios.html index dcbb282..4cad2d0 100644 --- a/internal/web/templates/studios.html +++ b/internal/web/templates/studios.html @@ -1,24 +1,10 @@ - - - Studios - Goondex - + {{template "html-head" .}} - + {{template "navbar" .}}
{{if .Studios}} -
+
{{range .Studios}} -
+
@@ -42,13 +28,11 @@
{{.SceneCount}} scenes
{{if .Studio.Description}} -
- {{.Studio.Description}} -
+
{{.Studio.Description}}
{{end}} {{if .Studio.Source}} -
+
{{.Studio.Source}}
{{end}} @@ -67,5 +51,6 @@
{{end}}
+ {{template "html-scripts" .}}