refactored view package

This commit is contained in:
2026-02-09 19:30:47 +11:00
parent 24733098b4
commit d01485d139
47 changed files with 653 additions and 490 deletions

View File

@@ -0,0 +1,11 @@
package adminview
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
templ DashboardLayout() {
@baseview.Layout("Admin") {
<div>
{ children... }
</div>
}
}

View File

@@ -0,0 +1,9 @@
package adminview
import "git.haelnorr.com/h/oslstats/internal/db"
templ DashboardPage(users *db.List[db.User]) {
@DashboardLayout() {
@UserList(users)
}
}

View File

@@ -1,4 +1,4 @@
package admin
package adminview
import "git.haelnorr.com/h/oslstats/internal/db"

View File

@@ -1,6 +1,6 @@
package form
package authview
templ RegisterForm(username string) {
templ RegisterFormForm(username string) {
<form
hx-post="/register"
hx-swap="none"

View File

@@ -1,11 +1,10 @@
package page
package authview
import "git.haelnorr.com/h/oslstats/internal/view/layout"
import "git.haelnorr.com/h/oslstats/internal/view/component/form"
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
// Returns the login page
templ Register(username string) {
@layout.Global("Register") {
templ RegisterPage(username string) {
@baseview.Layout("Register") {
<div class="max-w-100 mx-auto px-2">
<div class="mt-7 bg-mantle border border-surface1 rounded-xl">
<div class="p-4 sm:p-7">
@@ -20,7 +19,7 @@ templ Register(username string) {
</p>
</div>
<div class="mt-5">
@form.RegisterForm(username)
@RegisterFormForm(username)
</div>
</div>
</div>

View File

@@ -1,16 +1,15 @@
package page
package baseview
import "git.haelnorr.com/h/oslstats/internal/view/layout"
import "strconv"
// Original Error template (keep for backwards compatibility where needed)
templ Error(code int, err string, message string) {
@ErrorWithDetails(code, err, message, "")
templ ErrorPage(code int, err string, message string) {
@ErrorPageWithDetails(code, err, message, "")
}
// Enhanced Error template with optional details section
templ ErrorWithDetails(code int, err string, message string, details string) {
@layout.Global(err) {
templ ErrorPageWithDetails(code int, err string, message string, details string) {
@Layout(err) {
<div class="grid mt-24 left-0 right-0 top-0 bottom-0 place-content-center bg-base px-4">
<div class="text-center max-w-2xl mx-auto">
<h1 class="text-9xl text-text">{ strconv.Itoa(code) }</h1>

View File

@@ -0,0 +1,128 @@
package baseview
type FooterItem struct {
Name string
Href string
}
// Specify the links to show in the footer
func getFooterItems() []FooterItem {
return []FooterItem{
{Name: "About", Href: "/about"},
}
}
// Returns the template fragment for the Footer
templ Footer() {
<footer class="bg-mantle mt-10">
<div class="relative mx-auto max-w-screen-xl px-4 py-8 sm:px-6 lg:px-8">
@backToTopButton()
<div class="lg:flex lg:items-end lg:justify-between">
@footerBranding()
@footerLinks(getFooterItems())
</div>
<div class="lg:flex lg:items-end lg:justify-between">
@footerCopyright()
@themeSelector()
</div>
</div>
</footer>
}
templ backToTopButton() {
<div class="absolute end-4 top-4 sm:end-6 lg:end-8">
<a
class="inline-block rounded-full bg-teal p-2 text-crust
shadow-sm transition hover:bg-teal/75"
href="#main-content"
>
<span class="sr-only">Back to top</span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293
3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4
4a1 1 0 010 1.414z"
clip-rule="evenodd"
></path>
</svg>
</a>
</div>
}
templ footerBranding() {
<div>
<div class="flex justify-center text-text lg:justify-start">
<span class="text-2xl">OSL Stats</span>
</div>
<p class="mx-auto max-w-md text-center leading-relaxed text-subtext0">
placeholder text
</p>
</div>
}
templ footerLinks(items []FooterItem) {
<ul
class="mt-12 flex flex-wrap justify-center gap-6 md:gap-8
lg:mt-0 lg:justify-end lg:gap-12"
>
for _, item := range items {
<li>
<a
class="transition hover:text-subtext1"
href={ templ.SafeURL(item.Href) }
>
{ item.Name }
</a>
</li>
}
</ul>
}
templ footerCopyright() {
<div>
<p class="mt-4 text-center text-sm text-overlay0">
by Haelnorr | placeholder text
</p>
</div>
}
templ themeSelector() {
<div>
<div class="mt-2 text-center">
<label for="theme-select" class="hidden lg:inline">Theme</label>
<select
name="ThemeSelect"
id="theme-select"
class="mt-1.5 inline rounded-lg bg-surface0 p-2 w-fit"
x-model="theme"
>
<template
x-for="themeopt in [
'dark',
'light',
'system',
]"
>
<option
x-text="displayThemeName(themeopt)"
:value="themeopt"
:selected="theme === themeopt"
></option>
</template>
</select>
<script>
const displayThemeName = (value) => {
if (value === "dark") return "Dark (Mocha)";
if (value === "light") return "Light (Latte)";
if (value === "system") return "System";
};
</script>
</div>
</div>
}

View File

@@ -1,13 +1,10 @@
package layout
package baseview
import "git.haelnorr.com/h/oslstats/internal/view/component/popup"
import "git.haelnorr.com/h/oslstats/internal/view/component/nav"
import "git.haelnorr.com/h/oslstats/internal/view/component/footer"
import "git.haelnorr.com/h/oslstats/internal/view/popup"
import "git.haelnorr.com/h/oslstats/internal/contexts"
// Global page layout. Includes HTML document settings, header tags
// navbar and footer
templ Global(title string) {
// Global base layout for all pages
templ Layout(title string) {
{{ devInfo := contexts.DevMode(ctx) }}
<!DOCTYPE html>
<html
@@ -44,11 +41,11 @@ templ Global(title string) {
id="main-content"
class="flex flex-col h-screen justify-between"
>
@nav.Navbar()
@Navbar()
<div id="page-content" class="mb-auto md:px-5 md:pt-5">
{ children... }
</div>
@footer.Footer()
@Footer()
</div>
</body>
</html>

View File

@@ -0,0 +1,233 @@
package baseview
import (
"context"
"git.haelnorr.com/h/oslstats/internal/contexts"
"git.haelnorr.com/h/oslstats/internal/db"
)
type NavItem struct {
Name string
Href string
}
type ProfileItem struct {
Name string
Href string
}
// Main navigation items (centralized)
func getNavItems() []NavItem {
return []NavItem{
{Name: "Seasons", Href: "/seasons"},
}
}
// Profile dropdown items (context-aware for admin)
func getProfileItems(ctx context.Context) []ProfileItem {
items := []ProfileItem{
{Name: "Profile", Href: "/profile"},
{Name: "Account", Href: "/account"},
}
cache := contexts.Permissions(ctx)
if cache != nil && cache.Roles["admin"] {
items = append(items, ProfileItem{
Name: "Admin Panel",
Href: "/admin",
})
}
return items
}
// Main navbar component
templ Navbar() {
{{ navItems := getNavItems() }}
{{ user := db.CurrentUser(ctx) }}
{{ profileItems := getProfileItems(ctx) }}
<div x-data="{ open: false }">
<header class="bg-crust">
<div class="mx-auto flex h-16 max-w-7xl items-center gap-8 px-4 sm:px-6 lg:px-8">
<!-- Logo -->
<a class="block" href="/">
<span class="text-3xl font-bold transition hover:text-green">
OSL Stats
</span>
</a>
<div class="flex flex-1 items-center justify-end sm:justify-between">
<!-- Desktop nav links -->
@desktopNav(navItems)
<!-- User menu / Login button -->
@userMenu(user, profileItems)
</div>
</div>
</header>
<!-- Mobile side nav -->
@mobileNav(navItems, user)
</div>
}
// Desktop navigation (private helper)
templ desktopNav(navItems []NavItem) {
<nav aria-label="Global" class="hidden sm:block">
<ul class="flex items-center gap-6 text-xl">
for _, item := range navItems {
<li>
<a
class="text-subtext1 hover:text-green transition"
href={ templ.SafeURL(item.Href) }
>
{ item.Name }
</a>
</li>
}
</ul>
</nav>
}
// User menu section (private helper)
templ userMenu(user *db.User, profileItems []ProfileItem) {
<div class="flex items-center gap-2">
<div class="sm:flex sm:gap-2">
if user != nil {
@profileDropdown(user, profileItems)
} else {
@loginButton()
}
</div>
@mobileMenuButton()
</div>
}
// Profile dropdown (private helper)
templ profileDropdown(user *db.User, items []ProfileItem) {
<div x-data="{ isActive: false }" class="relative">
<div
class="inline-flex items-center overflow-hidden rounded-lg
bg-sapphire hover:bg-sapphire/75 transition"
>
<button
x-on:click="isActive = !isActive"
class="h-full py-2 px-4 text-mantle hover:cursor-pointer"
>
<span class="sr-only">Profile</span>
{ user.Username }
</button>
</div>
<div
class="absolute end-0 z-10 mt-2 w-36 divide-y divide-surface2
rounded-lg border border-surface1 bg-surface0 shadow-lg"
role="menu"
x-cloak
x-transition
x-show="isActive"
x-on:click.away="isActive = false"
x-on:keydown.escape.window="isActive = false"
>
<!-- Profile links -->
<div class="p-2">
for _, item := range items {
<a
href={ templ.SafeURL(item.Href) }
class="block rounded-lg px-4 py-2 text-md hover:bg-crust"
role="menuitem"
>
{ item.Name }
</a>
}
</div>
<!-- Logout -->
<div class="p-2">
<form hx-post="/logout">
<button
type="submit"
class="flex w-full items-center gap-2 rounded-lg px-4 py-2
text-md text-red hover:bg-red/25 hover:cursor-pointer"
role="menuitem"
@click="isActive=false"
>
Logout
</button>
</form>
</div>
</div>
</div>
}
// Login button (private helper)
templ loginButton() {
<button
class="hidden rounded-lg px-4 py-2 sm:block hover:cursor-pointer
bg-green hover:bg-green/75 text-mantle transition"
hx-post="/login"
hx-swap="none"
>
Login
</button>
}
// Mobile menu toggle (private helper)
templ mobileMenuButton() {
<button
@click="open = !open"
class="block rounded-lg p-2.5 sm:hidden transition
bg-surface0 text-subtext0 hover:text-overlay2/75"
>
<span class="sr-only">Toggle menu</span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4 6h16M4 12h16M4 18h16"
></path>
</svg>
</button>
}
// Mobile navigation drawer (private helper)
templ mobileNav(navItems []NavItem, user *db.User) {
<div
x-show="open"
x-transition
class="absolute w-full bg-mantle sm:hidden z-10"
>
<div class="px-4 py-6">
<ul class="space-y-1">
for _, item := range navItems {
<li>
<a
href={ templ.SafeURL(item.Href) }
class="block rounded-lg px-4 py-2 text-lg
bg-surface0 text-text transition hover:bg-surface2"
>
{ item.Name }
</a>
</li>
}
</ul>
</div>
if user == nil {
<div class="px-4 pb-6">
<ul class="space-y-1">
<li class="flex justify-center items-center gap-2">
<a
class="w-26 px-4 py-2 rounded-lg bg-green text-mantle
transition hover:bg-green/75 text-center"
href="/login"
>
Login
</a>
</li>
</ul>
</div>
}
</div>
}

View File

@@ -1,116 +0,0 @@
package footer
type FooterItem struct {
name string
href string
}
// Specify the links to show in the footer
func getFooterItems() []FooterItem {
return []FooterItem{
{
name: "About",
href: "/about",
},
}
}
// Returns the template fragment for the Footer
templ Footer() {
<footer class="bg-mantle mt-10">
<div
class="relative mx-auto max-w-screen-xl px-4 py-8 sm:px-6 lg:px-8"
>
<div class="absolute end-4 top-4 sm:end-6 lg:end-8">
<a
class="inline-block rounded-full bg-teal p-2 text-crust
shadow-sm transition hover:bg-teal/75"
href="#main-content"
>
<span class="sr-only">Back to top</span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293
3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4
4a1 1 0 010 1.414z"
clip-rule="evenodd"
></path>
</svg>
</a>
</div>
<div class="lg:flex lg:items-end lg:justify-between">
<div>
<div class="flex justify-center text-text lg:justify-start">
// TODO: logo/branding here
<span class="text-2xl">OSL Stats</span>
</div>
<p
class="mx-auto max-w-md text-center leading-relaxed
text-subtext0"
>placeholder text</p>
</div>
<ul
class="mt-12 flex flex-wrap justify-center gap-6 md:gap-8
lg:mt-0 lg:justify-end lg:gap-12"
>
for _, item := range getFooterItems() {
<li>
<a
class="transition hover:text-subtext1"
href={ templ.SafeURL(item.href) }
>{ item.name }</a>
</li>
}
</ul>
</div>
<div class="lg:flex lg:items-end lg:justify-between">
<div>
<p class="mt-4 text-center text-sm text-overlay0">
by Haelnorr | placeholder text
</p>
</div>
<div>
<div class="mt-2 text-center">
<label
for="theme-select"
class="hidden lg:inline"
>Theme</label>
<select
name="ThemeSelect"
id="theme-select"
class="mt-1.5 inline rounded-lg bg-surface0 p-2 w-fit"
x-model="theme"
>
<template
x-for="themeopt in [
'dark',
'light',
'system',
]"
>
<option
x-text="displayThemeName(themeopt)"
:value="themeopt"
:selected="theme === themeopt"
></option>
</template>
</select>
<script>
const displayThemeName = (value) => {
if (value === "dark") return "Dark (Mocha)";
if (value === "light") return "Light (Latte)";
if (value === "system") return "System";
};
</script>
</div>
</div>
</div>
</div>
</footer>
}

View File

@@ -1,41 +0,0 @@
package nav
type NavItem struct {
name string // Label to display
href string // Link reference
}
// Return the list of navbar links
func getNavItems() []NavItem {
return []NavItem{
{
name: "Seasons",
href: "/seasons",
},
}
}
// Returns the navbar template fragment
templ Navbar() {
{{ navItems := getNavItems() }}
<div x-data="{ open: false }">
<header class="bg-crust">
<div
class="mx-auto flex h-16 max-w-7xl items-center gap-8
px-4 sm:px-6 lg:px-8"
>
<a class="block" href="/">
<!-- logo here -->
<span class="text-3xl font-bold transition hover:text-green">
OSL Stats
</span>
</a>
<div class="flex flex-1 items-center justify-end sm:justify-between">
@navLeft(navItems)
@navRight()
</div>
</div>
</header>
@sideNav(navItems)
</div>
}

View File

@@ -1,19 +0,0 @@
package nav
// Returns the left portion of the navbar
templ navLeft(navItems []NavItem) {
<nav aria-label="Global" class="hidden sm:block">
<ul class="flex items-center gap-6 text-xl">
for _, item := range navItems {
<li>
<a
class="text-subtext1 hover:text-green transition"
href={ templ.SafeURL(item.href) }
>
{ item.name }
</a>
</li>
}
</ul>
</nav>
}

View File

@@ -1,131 +0,0 @@
package nav
import (
"context"
"git.haelnorr.com/h/oslstats/internal/contexts"
"git.haelnorr.com/h/oslstats/internal/db"
)
type ProfileItem struct {
name string // Label to display
href string // Link reference
}
// Return the list of profile links
func getProfileItems(ctx context.Context) []ProfileItem {
items := []ProfileItem{
{
name: "Profile",
href: "/profile",
},
{
name: "Account",
href: "/account",
},
}
// Add admin link if user has admin role
cache := contexts.Permissions(ctx)
if cache != nil && cache.Roles["admin"] {
items = append(items, ProfileItem{
name: "Admin Panel",
href: "/admin",
})
}
return items
}
// Returns the right portion of the navbar
templ navRight() {
{{ user := db.CurrentUser(ctx) }}
{{ items := getProfileItems(ctx) }}
<div class="flex items-center gap-2">
<div class="sm:flex sm:gap-2">
if user != nil {
<div x-data="{ isActive: false }" class="relative">
<div
class="inline-flex items-center overflow-hidden
rounded-lg bg-sapphire hover:bg-sapphire/75 transition"
>
<button
x-on:click="isActive = !isActive"
class="h-full py-2 px-4 text-mantle hover:cursor-pointer"
>
<span class="sr-only">Profile</span>
{ user.Username }
</button>
</div>
<div
class="absolute end-0 z-10 mt-2 w-36 divide-y
divide-surface2 rounded-lg border border-surface1
bg-surface0 shadow-lg"
role="menu"
x-cloak
x-transition
x-show="isActive"
x-on:click.away="isActive = false"
x-on:keydown.escape.window="isActive = false"
>
<div class="p-2">
for _, item := range items {
<a
href={ templ.SafeURL(item.href) }
class="block rounded-lg px-4 py-2 text-md
hover:bg-crust"
role="menuitem"
>
{ item.name }
</a>
}
</div>
<div class="p-2">
<form hx-post="/logout">
<button
type="submit"
class="flex w-full items-center gap-2
rounded-lg px-4 py-2 text-md text-red
hover:bg-red/25 hover:cursor-pointer"
role="menuitem"
@click="isActive=false"
>
Logout
</button>
</form>
</div>
</div>
</div>
} else {
<button
class="hidden rounded-lg px-4 py-2 sm:block hover:cursor-pointer
bg-green hover:bg-green/75 text-mantle transition"
hx-post="/login"
hx-swap="none"
>
Login
</button>
}
</div>
<button
@click="open = !open"
class="block rounded-lg p-2.5 sm:hidden transition
bg-surface0 text-subtext0 hover:text-overlay2/75"
>
<span class="sr-only">Toggle menu</span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="size-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4 6h16M4 12h16M4 18h16"
></path>
</svg>
</button>
</div>
}

View File

@@ -1,45 +0,0 @@
package nav
import "git.haelnorr.com/h/oslstats/internal/db"
// Returns the mobile version of the navbar thats only visible when activated
templ sideNav(navItems []NavItem) {
{{ user := db.CurrentUser(ctx) }}
<div
x-show="open"
x-transition
class="absolute w-full bg-mantle sm:hidden z-10"
>
<div class="px-4 py-6">
<ul class="space-y-1">
for _, item := range navItems {
<li>
<a
href={ templ.SafeURL(item.href) }
class="block rounded-lg px-4 py-2 text-lg
bg-surface0 text-text transition hover:bg-surface2"
>
{ item.name }
</a>
</li>
}
</ul>
</div>
if user == nil {
<div class="px-4 pb-6">
<ul class="space-y-1">
<li class="flex justify-center items-center gap-2">
<a
class="w-26 px-4 py-2 rounded-lg
bg-green text-mantle transition hover:bg-green/75
text-center"
href="/login"
>
Login
</a>
</li>
</ul>
</div>
}
</div>
}

View File

@@ -1,10 +1,10 @@
package page
package homeview
import "git.haelnorr.com/h/oslstats/internal/view/layout"
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
// Page content for the index page
templ Index() {
@layout.Global("OSL Stats") {
templ IndexPage() {
@baseview.Layout("OSL Stats") {
<div class="text-center mt-25">
<div class="text-4xl lg:text-6xl">OSL Stats</div>
<div>Placeholder text</div>

View File

@@ -1,8 +0,0 @@
package layout
templ AdminDashboard() {
@Global("Admin")
<div>
{ children... }
</div>
}

View File

@@ -1,10 +0,0 @@
package page
import "git.haelnorr.com/h/oslstats/internal/view/layout"
import "git.haelnorr.com/h/oslstats/internal/view/component/admin"
import "git.haelnorr.com/h/oslstats/internal/db"
templ AdminDashboard(users *db.List[db.User]) {
@layout.AdminDashboard()
@admin.UserList(users)
}

View File

@@ -1,13 +1,12 @@
package page
package seasonsview
import "git.haelnorr.com/h/oslstats/internal/db"
import "git.haelnorr.com/h/oslstats/internal/view/layout"
import seasoncomp "git.haelnorr.com/h/oslstats/internal/view/component/season"
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
import "time"
import "strconv"
templ SeasonPage(season *db.Season) {
@layout.Global(season.Name) {
templ DetailPage(season *db.Season) {
@baseview.Layout(season.Name) {
<div class="max-w-screen-2xl mx-auto px-4 py-8">
@SeasonDetails(season)
</div>
@@ -125,7 +124,7 @@ templ SeasonDetails(season *db.Season) {
<div class="bg-surface0 border border-surface1 rounded-lg p-6">
<h2 class="text-2xl font-bold text-text mb-4">Status</h2>
<div class="flex items-center gap-3">
@seasoncomp.StatusBadge(season, false, false)
@StatusBadge(season, false, false)
</div>
</div>
</div>

View File

@@ -1,16 +1,15 @@
package page
package seasonsview
import "git.haelnorr.com/h/oslstats/internal/db"
import "git.haelnorr.com/h/oslstats/internal/view/layout"
import "git.haelnorr.com/h/oslstats/internal/view/component/pagination"
import "git.haelnorr.com/h/oslstats/internal/view/component/sort"
import "git.haelnorr.com/h/oslstats/internal/view/component/season"
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
import "git.haelnorr.com/h/oslstats/internal/view/pagination"
import "git.haelnorr.com/h/oslstats/internal/view/sort"
import "fmt"
import "time"
import "github.com/uptrace/bun"
templ SeasonsPage(seasons *db.List[db.Season]) {
@layout.Global("Seasons") {
templ ListPage(seasons *db.List[db.Season]) {
@baseview.Layout("Seasons") {
<div class="max-w-screen-2xl mx-auto px-2">
@SeasonsList(seasons)
</div>
@@ -84,7 +83,7 @@ templ SeasonsList(seasons *db.List[db.Season]) {
<!-- Header: Name + Status Badge -->
<div class="flex justify-between items-start mb-3">
<h3 class="text-xl font-bold text-text">{ s.Name }</h3>
@season.StatusBadge(s, true, true)
@StatusBadge(s, true, true)
</div>
<!-- Info Row: Short Name + Start Date -->
<div class="flex items-center gap-3 text-sm">

View File

@@ -1,8 +1,8 @@
package form
package seasonsview
import "git.haelnorr.com/h/oslstats/internal/view/component/datepicker"
import "git.haelnorr.com/h/oslstats/internal/view/datepicker"
templ NewSeason() {
templ NewForm() {
<form
hx-post="/seasons/new"
hx-swap="none"

View File

@@ -1,10 +1,9 @@
package page
package seasonsview
import "git.haelnorr.com/h/oslstats/internal/view/component/form"
import "git.haelnorr.com/h/oslstats/internal/view/layout"
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
templ NewSeason() {
@layout.Global("New Season") {
templ NewPage() {
@baseview.Layout("New Season") {
<div class="max-w-screen-lg mx-auto px-4 py-8">
<div class="bg-mantle border border-surface1 rounded-xl">
<div class="p-6 sm:p-8">
@@ -15,7 +14,7 @@ templ NewSeason() {
</p>
</div>
<div class="max-w-md mx-auto">
@form.NewSeason()
@NewForm()
</div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
package season
package seasonsview
import "git.haelnorr.com/h/oslstats/internal/db"
import "time"

View File

@@ -1,10 +1,10 @@
package page
package testview
import "git.haelnorr.com/h/oslstats/internal/view/layout"
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
// Page content for the test notification page
templ Test() {
@layout.Global("Notification Test") {
templ NotificationTestPage() {
@baseview.Layout("Notification Test") {
<div class="flex items-center justify-center min-h-[calc(100vh-200px)]">
<div class="w-full max-w-md px-4">
<!-- Title -->