first commit

This commit is contained in:
Matthias Hinrichs
2025-07-05 03:10:41 +02:00
commit 9b7bdcbc53
39 changed files with 5109 additions and 0 deletions
@@ -0,0 +1,78 @@
package components
import "portfolio-tracker/internal/model"
templ PageLayout(authenticated bool, username string, title string, content templ.Component, portfolios []model.Portfolio) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>{ title }</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/core@1.3.2/dist/css/tabler.min.css"/>
<style>
html, body {
height: 100%;
width: 100%;
}
body {
min-height: 100vh;
width: 100vw;
overflow-x: hidden;
}
.container-xl, .container-fluid {
width: 100% !important;
max-width: 100% !important;
padding-left: 24px;
padding-right: 24px;
}
.page-wrapper {
margin-left: 220px;
width: auto;
}
.card {
width: 100%;
}
.dropdown-search {
position: relative;
}
.dropdown-menu-search {
display: none;
position: absolute;
top: 100%;
left: 0;
width: 100%;
z-index: 1000;
background: #fff;
border: 1px solid #ddd;
border-radius: 0 0 .25rem .25rem;
max-height: 300px;
overflow-y: auto;
}
.dropdown-menu-search.show {
display: block;
}
.dropdown-item-search {
display: block;
padding: .5rem 1rem;
cursor: pointer;
}
.dropdown-item-search:hover {
background: #f1f3f4;
}
</style>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
@Search()
<div class="page">
@Navigation(authenticated, portfolios)
<div class="page-wrapper">
@content
</div>
</div>
@SearchJS()
<script src="https://cdn.jsdelivr.net/npm/@tabler/core@1.3.2/dist/js/tabler.min.js"></script>
</body>
</html>
}
@@ -0,0 +1,87 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.778
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "portfolio-tracker/internal/model"
func PageLayout(authenticated bool, username string, title string, content templ.Component, portfolios []model.Portfolio) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/components/layout.templ`, Line: 11, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</title><link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@tabler/core@1.3.2/dist/css/tabler.min.css\"><style>\n\t\t\thtml, body {\n\t\t\t\theight: 100%;\n\t\t\t\twidth: 100%;\n\t\t\t}\n\t\t\tbody {\n\t\t\t\tmin-height: 100vh;\n\t\t\t\twidth: 100vw;\n\t\t\t\toverflow-x: hidden;\n\t\t\t}\n\t\t\t.container-xl, .container-fluid {\n\t\t\t\twidth: 100% !important;\n\t\t\t\tmax-width: 100% !important;\n\t\t\t\tpadding-left: 24px;\n\t\t\t\tpadding-right: 24px;\n\t\t\t}\n\t\t\t.page-wrapper {\n\t\t\t\tmargin-left: 220px;\n\t\t\t\twidth: auto;\n\t\t\t}\n\t\t\t.card {\n\t\t\t\twidth: 100%;\n\t\t\t}\n\t\t\t.dropdown-search {\n\t\t\t\tposition: relative;\n\t\t\t}\n\t\t\t.dropdown-menu-search {\n\t\t\t\tdisplay: none;\n\t\t\t\tposition: absolute;\n\t\t\t\ttop: 100%;\n\t\t\t\tleft: 0;\n\t\t\t\twidth: 100%;\n\t\t\t\tz-index: 1000;\n\t\t\t\tbackground: #fff;\n\t\t\t\tborder: 1px solid #ddd;\n\t\t\t\tborder-radius: 0 0 .25rem .25rem;\n\t\t\t\tmax-height: 300px;\n\t\t\t\toverflow-y: auto;\n\t\t\t}\n\t\t\t.dropdown-menu-search.show {\n\t\t\t\tdisplay: block;\n\t\t\t}\n\t\t\t.dropdown-item-search {\n\t\t\t\tdisplay: block;\n\t\t\t\tpadding: .5rem 1rem;\n\t\t\t\tcursor: pointer;\n\t\t\t}\n\t\t\t.dropdown-item-search:hover {\n\t\t\t\tbackground: #f1f3f4;\n\t\t\t}\n\t\t</style></head><body><script src=\"https://cdn.jsdelivr.net/npm/apexcharts\"></script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Search().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"page\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Navigation(authenticated, portfolios).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"page-wrapper\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = content.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = SearchJS().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<script src=\"https://cdn.jsdelivr.net/npm/@tabler/core@1.3.2/dist/js/tabler.min.js\"></script></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate
@@ -0,0 +1,192 @@
package components
import (
"fmt"
"portfolio-tracker/internal/model"
)
templ Navigation(authenticated bool, portfolios []model.Portfolio) {
<aside class="navbar navbar-vertical navbar-expand-lg" data-bs-theme="dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">
<span class="navbar-brand-text">Portfolio Tracker</span>
</a>
<div class="collapse navbar-collapse show">
<ul class="navbar-nav pt-lg-3">
<li class="nav-item">
<a class="nav-link active" href="/">
<span class="nav-link-icon">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<rect x="3" y="12" width="6" height="8" rx="1"></rect>
<rect x="9" y="8" width="6" height="12" rx="1"></rect>
<rect x="15" y="4" width="6" height="16" rx="1"></rect>
</svg>
</span>
<span class="nav-link-title">Dashboard</span>
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="portfolioDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<span class="nav-link-icon">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M9 5H7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2V7a2 2 0 0 0 -2 -2h-2"></path>
<path d="M9 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z"></path>
<path d="M9 12l2 2l4 -4"></path>
</svg>
</span>
<span class="nav-link-title">Portfolio</span>
</a>
<ul class="dropdown-menu" aria-labelledby="portfolioDropdown">
if len(portfolios) > 0 {
for _, portfolio := range portfolios {
<li><a class="dropdown-item" href={ templ.URL("/portfolio/" + templ.EscapeString(fmt.Sprintf("%d", portfolio.ID))) }>{ portfolio.Name }</a></li>
}
<li><hr class="dropdown-divider"/></li>
} else {
<li><span class="dropdown-item-text text-muted">Keine Portfolios vorhanden</span></li>
<li><hr class="dropdown-divider"/></li>
}
<li>
<a class="dropdown-item" href="/portfolio/new" data-bs-toggle="modal" data-bs-target="#createPortfolioModal">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-sm me-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 5l0 14"></path>
<path d="M5 12l14 0"></path>
</svg>
Neues Portfolio erstellen
</a>
</li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<span class="nav-link-icon">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 21l18 0"></path>
<path d="M3 10l18 0"></path>
<path d="M5 6l7 -3l7 3"></path>
<path d="M4 10l0 11"></path>
<path d="M20 10l0 11"></path>
<path d="M8 14l0 3"></path>
<path d="M12 14l0 3"></path>
<path d="M16 14l0 3"></path>
</svg>
</span>
<span class="nav-link-title">Wertpapiere</span>
</a>
</li>
<!-- Weitere Menüpunkte hier -->
</ul>
<div id="login_buttons" class="mt-4" style="padding-left:10px; padding-bottom:10px;">
if authenticated == true {
<form method="post" action="/user/logout">
<button type="submit" class="btn btn-danger w-100">Logout</button>
</form>
} else {
<button type="button" class="btn btn-outline-primary w-100 mb-2" data-bs-toggle="modal" data-bs-target="#loginModal">Login</button>
<button type="button" class="btn btn-primary w-100" data-bs-toggle="modal" data-bs-target="#registerModal">Registrieren</button>
}
</div>
</div>
</div>
</aside>
<!-- Create Portfolio Modal -->
<div class="modal modal-blur fade" id="createPortfolioModal" tabindex="-1" aria-labelledby="createPortfolioModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createPortfolioModalLabel">Neues Portfolio erstellen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<form method="post" action="/portfolio/create">
<div class="modal-body">
<div class="mb-3">
<label for="portfolio-name" class="form-label">Portfolio Name</label>
<input type="text" class="form-control" id="portfolio-name" name="name" placeholder="z.B. Mein Hauptportfolio" required/>
</div>
<div class="mb-3">
<label for="portfolio-currency" class="form-label">Basiswährung</label>
<select class="form-select" id="portfolio-currency" name="base_currency" required>
<option value="">Wählen Sie eine Währung</option>
<option value="EUR">EUR - Euro</option>
<option value="USD">USD - US Dollar</option>
<option value="GBP">GBP - Britisches Pfund</option>
<option value="CHF">CHF - Schweizer Franken</option>
<option value="JPY">JPY - Japanischer Yen</option>
</select>
</div>
<div class="mb-3">
<label for="portfolio-description" class="form-label">Beschreibung (optional)</label>
<textarea class="form-control" id="portfolio-description" name="description" rows="3" placeholder="Beschreibung des Portfolios..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">Portfolio erstellen</button>
</div>
</form>
</div>
</div>
</div>
<!-- Login Modal -->
<div class="modal modal-blur fade" id="loginModal" tabindex="-1" aria-labelledby="loginModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="loginModalLabel">Anmelden</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<form method="post" action="/user/login">
<div class="modal-body">
<div class="mb-3">
<label for="login-username" class="form-label">Benutzername</label>
<input type="text" class="form-control" id="login-username" name="username" required/>
</div>
<div class="mb-3">
<label for="login-password" class="form-label">Passwort</label>
<input type="password" class="form-control" id="login-password" name="password" required/>
</div>
</div>
<div class="modal-footer d-flex justify-content-between align-items-center">
<a href="#" class="text-secondary small" data-bs-dismiss="modal" style="text-decoration:none;">Abbrechen</a>
<button type="submit" class="btn btn-primary">Anmelden</button>
</div>
</form>
</div>
</div>
</div>
<!-- Register Modal -->
<div class="modal modal-blur fade" id="registerModal" tabindex="-1" aria-labelledby="registerModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="registerModalLabel">Registrieren</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<form method="post" action="/user/register">
<div class="modal-body">
<div class="mb-3">
<label for="register-username" class="form-label">Benutzername</label>
<input type="text" class="form-control" id="register-username" name="username" required/>
</div>
<div class="mb-3">
<label for="register-email" class="form-label">E-Mail</label>
<input type="email" class="form-control" id="register-email" name="email" required/>
</div>
<div class="mb-3">
<label for="register-password" class="form-label">Passwort</label>
<input type="password" class="form-control" id="register-password" name="password" required/>
</div>
</div>
<div class="modal-footer d-flex justify-content-between align-items-center">
<a href="#" class="text-secondary small" data-bs-dismiss="modal" style="text-decoration:none;">Abbrechen</a>
<button type="submit" class="btn btn-primary">Registrieren</button>
</div>
</form>
</div>
</div>
</div>
}
@@ -0,0 +1,103 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.778
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"fmt"
"portfolio-tracker/internal/model"
)
func Navigation(authenticated bool, portfolios []model.Portfolio) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<aside class=\"navbar navbar-vertical navbar-expand-lg\" data-bs-theme=\"dark\"><div class=\"container-fluid\"><a class=\"navbar-brand\" href=\"#\"><span class=\"navbar-brand-text\">Portfolio Tracker</span></a><div class=\"collapse navbar-collapse show\"><ul class=\"navbar-nav pt-lg-3\"><li class=\"nav-item\"><a class=\"nav-link active\" href=\"/\"><span class=\"nav-link-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path> <rect x=\"3\" y=\"12\" width=\"6\" height=\"8\" rx=\"1\"></rect> <rect x=\"9\" y=\"8\" width=\"6\" height=\"12\" rx=\"1\"></rect> <rect x=\"15\" y=\"4\" width=\"6\" height=\"16\" rx=\"1\"></rect></svg></span> <span class=\"nav-link-title\">Dashboard</span></a></li><li class=\"nav-item dropdown\"><a class=\"nav-link dropdown-toggle\" href=\"#\" id=\"portfolioDropdown\" role=\"button\" data-bs-toggle=\"dropdown\" aria-expanded=\"false\"><span class=\"nav-link-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path> <path d=\"M9 5H7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2V7a2 2 0 0 0 -2 -2h-2\"></path> <path d=\"M9 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z\"></path> <path d=\"M9 12l2 2l4 -4\"></path></svg></span> <span class=\"nav-link-title\">Portfolio</span></a><ul class=\"dropdown-menu\" aria-labelledby=\"portfolioDropdown\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(portfolios) > 0 {
for _, portfolio := range portfolios {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><a class=\"dropdown-item\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 templ.SafeURL = templ.URL("/portfolio/" + templ.EscapeString(fmt.Sprintf("%d", portfolio.ID)))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var2)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(portfolio.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/components/navigation.templ`, Line: 44, Col: 142}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <li><hr class=\"dropdown-divider\"></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><span class=\"dropdown-item-text text-muted\">Keine Portfolios vorhanden</span></li><li><hr class=\"dropdown-divider\"></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><a class=\"dropdown-item\" href=\"/portfolio/new\" data-bs-toggle=\"modal\" data-bs-target=\"#createPortfolioModal\"><svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon icon-sm me-2\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path> <path d=\"M12 5l0 14\"></path> <path d=\"M5 12l14 0\"></path></svg> Neues Portfolio erstellen</a></li></ul></li><li class=\"nav-item\"><a class=\"nav-link\" href=\"#\"><span class=\"nav-link-icon\"><svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"></path> <path d=\"M3 21l18 0\"></path> <path d=\"M3 10l18 0\"></path> <path d=\"M5 6l7 -3l7 3\"></path> <path d=\"M4 10l0 11\"></path> <path d=\"M20 10l0 11\"></path> <path d=\"M8 14l0 3\"></path> <path d=\"M12 14l0 3\"></path> <path d=\"M16 14l0 3\"></path></svg></span> <span class=\"nav-link-title\">Wertpapiere</span></a></li><!-- Weitere Menüpunkte hier --></ul><div id=\"login_buttons\" class=\"mt-4\" style=\"padding-left:10px; padding-bottom:10px;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if authenticated == true {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<form method=\"post\" action=\"/user/logout\"><button type=\"submit\" class=\"btn btn-danger w-100\">Logout</button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<button type=\"button\" class=\"btn btn-outline-primary w-100 mb-2\" data-bs-toggle=\"modal\" data-bs-target=\"#loginModal\">Login</button> <button type=\"button\" class=\"btn btn-primary w-100\" data-bs-toggle=\"modal\" data-bs-target=\"#registerModal\">Registrieren</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div></div></aside><!-- Create Portfolio Modal --><div class=\"modal modal-blur fade\" id=\"createPortfolioModal\" tabindex=\"-1\" aria-labelledby=\"createPortfolioModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"createPortfolioModalLabel\">Neues Portfolio erstellen</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Schließen\"></button></div><form method=\"post\" action=\"/portfolio/create\"><div class=\"modal-body\"><div class=\"mb-3\"><label for=\"portfolio-name\" class=\"form-label\">Portfolio Name</label> <input type=\"text\" class=\"form-control\" id=\"portfolio-name\" name=\"name\" placeholder=\"z.B. Mein Hauptportfolio\" required></div><div class=\"mb-3\"><label for=\"portfolio-currency\" class=\"form-label\">Basiswährung</label> <select class=\"form-select\" id=\"portfolio-currency\" name=\"base_currency\" required><option value=\"\">Wählen Sie eine Währung</option> <option value=\"EUR\">EUR - Euro</option> <option value=\"USD\">USD - US Dollar</option> <option value=\"GBP\">GBP - Britisches Pfund</option> <option value=\"CHF\">CHF - Schweizer Franken</option> <option value=\"JPY\">JPY - Japanischer Yen</option></select></div><div class=\"mb-3\"><label for=\"portfolio-description\" class=\"form-label\">Beschreibung (optional)</label> <textarea class=\"form-control\" id=\"portfolio-description\" name=\"description\" rows=\"3\" placeholder=\"Beschreibung des Portfolios...\"></textarea></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Abbrechen</button> <button type=\"submit\" class=\"btn btn-primary\">Portfolio erstellen</button></div></form></div></div></div><!-- Login Modal --><div class=\"modal modal-blur fade\" id=\"loginModal\" tabindex=\"-1\" aria-labelledby=\"loginModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"loginModalLabel\">Anmelden</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Schließen\"></button></div><form method=\"post\" action=\"/user/login\"><div class=\"modal-body\"><div class=\"mb-3\"><label for=\"login-username\" class=\"form-label\">Benutzername</label> <input type=\"text\" class=\"form-control\" id=\"login-username\" name=\"username\" required></div><div class=\"mb-3\"><label for=\"login-password\" class=\"form-label\">Passwort</label> <input type=\"password\" class=\"form-control\" id=\"login-password\" name=\"password\" required></div></div><div class=\"modal-footer d-flex justify-content-between align-items-center\"><a href=\"#\" class=\"text-secondary small\" data-bs-dismiss=\"modal\" style=\"text-decoration:none;\">Abbrechen</a> <button type=\"submit\" class=\"btn btn-primary\">Anmelden</button></div></form></div></div></div><!-- Register Modal --><div class=\"modal modal-blur fade\" id=\"registerModal\" tabindex=\"-1\" aria-labelledby=\"registerModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"registerModalLabel\">Registrieren</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Schließen\"></button></div><form method=\"post\" action=\"/user/register\"><div class=\"modal-body\"><div class=\"mb-3\"><label for=\"register-username\" class=\"form-label\">Benutzername</label> <input type=\"text\" class=\"form-control\" id=\"register-username\" name=\"username\" required></div><div class=\"mb-3\"><label for=\"register-email\" class=\"form-label\">E-Mail</label> <input type=\"email\" class=\"form-control\" id=\"register-email\" name=\"email\" required></div><div class=\"mb-3\"><label for=\"register-password\" class=\"form-label\">Passwort</label> <input type=\"password\" class=\"form-control\" id=\"register-password\" name=\"password\" required></div></div><div class=\"modal-footer d-flex justify-content-between align-items-center\"><a href=\"#\" class=\"text-secondary small\" data-bs-dismiss=\"modal\" style=\"text-decoration:none;\">Abbrechen</a> <button type=\"submit\" class=\"btn btn-primary\">Registrieren</button></div></form></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate
@@ -0,0 +1,19 @@
package components
templ Search() {
<div class="navbar navbar-expand-lg navbar-light bg-white" style="margin-left:220px;">
<div class="container-fluid">
<form class="d-flex dropdown-search" id="stock-search-form" autocomplete="off" style="width:100%;">
<span class="input-group-text" style="background:transparent; border:none; padding-right:0;">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icon-tabler-search" viewBox="0 0 24 24">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</span>
<input class="form-control me-2" type="search" placeholder="WKN, ISIN, Name ..." aria-label="Search" id="stock-search-input" autocomplete="off" style="margin-left:-1px;"/>
<button class="btn btn-primary" type="submit">Suchen</button>
<div class="dropdown-menu-search" id="search-dropdown"></div>
</form>
</div>
</div>
}
@@ -0,0 +1,40 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.778
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func Search() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"navbar navbar-expand-lg navbar-light bg-white\" style=\"margin-left:220px;\"><div class=\"container-fluid\"><form class=\"d-flex dropdown-search\" id=\"stock-search-form\" autocomplete=\"off\" style=\"width:100%;\"><span class=\"input-group-text\" style=\"background:transparent; border:none; padding-right:0;\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"icon icon-tabler icon-tabler-search\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"8\"></circle> <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line></svg></span> <input class=\"form-control me-2\" type=\"search\" placeholder=\"WKN, ISIN, Name ...\" aria-label=\"Search\" id=\"stock-search-input\" autocomplete=\"off\" style=\"margin-left:-1px;\"> <button class=\"btn btn-primary\" type=\"submit\">Suchen</button><div class=\"dropdown-menu-search\" id=\"search-dropdown\"></div></form></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate
@@ -0,0 +1,89 @@
package components
templ SearchJS() {
<script>
const input = document.getElementById('stock-search-input');
const dropdown = document.getElementById('search-dropdown');
let debounceTimeout;
let selectedIndex = -1;
input.addEventListener('input', function () {
selectedIndex = -1;
const query = input.value.trim();
clearTimeout(debounceTimeout);
if (query.length < 2) {
dropdown.classList.remove('show');
dropdown.innerHTML = '';
return;
}
debounceTimeout = setTimeout(() => {
fetch('/api/stocksearch?q=' + encodeURIComponent(query))
.then(res => res.json())
.then(data => {
if (data.quotes && data.quotes.length > 0) {
dropdown.innerHTML = data.quotes.map(item =>
`<a class="dropdown-item-search" href="/details?stock=${encodeURIComponent(item.symbol)}">${item.longname || item.shortname || item.symbol} (${item.symbol})</a>`
).join('');
dropdown.classList.add('show');
} else {
dropdown.innerHTML = '<div class="dropdown-item-search">Keine Ergebnisse</div>';
dropdown.classList.add('show');
}
});
}, 300);
});
input.addEventListener('keydown', function (e) {
const items = Array.from(dropdown.querySelectorAll('.dropdown-item-search'));
if (!dropdown.classList.contains('show') || items.length === 0) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedIndex = (selectedIndex + 1) % items.length;
updateDropdownSelection(items);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedIndex = (selectedIndex - 1 + items.length) % items.length;
updateDropdownSelection(items);
} else if (e.key === 'Enter') {
if (selectedIndex >= 0 && selectedIndex < items.length) {
e.preventDefault();
window.location.href = items[selectedIndex].getAttribute('href');
}
}
});
function updateDropdownSelection(items) {
items.forEach((item, idx) => {
if (idx === selectedIndex) {
item.classList.add('active');
item.scrollIntoView({ block: 'nearest' });
} else {
item.classList.remove('active');
}
});
}
document.addEventListener('click', function (e) {
if (!input.contains(e.target) && !dropdown.contains(e.target)) {
dropdown.classList.remove('show');
}
});
document.getElementById('stock-search-form').addEventListener('submit', function(e) {
e.preventDefault();
const items = Array.from(dropdown.querySelectorAll('.dropdown-item-search'));
// Wenn ein Eintrag ausgewählt ist, nimm diesen
if (selectedIndex >= 0 && selectedIndex < items.length) {
window.location.href = items[selectedIndex].getAttribute('href');
return;
}
// Sonst nimm das erste Ergebnis, falls vorhanden
if (items.length > 0) {
window.location.href = items[0].getAttribute('href');
return;
}
// Sonst nichts tun
});
</script>
}
@@ -0,0 +1,40 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.778
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func SearchJS() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<script>\n const input = document.getElementById('stock-search-input');\n const dropdown = document.getElementById('search-dropdown');\n let debounceTimeout;\n let selectedIndex = -1;\n\n input.addEventListener('input', function () {\n selectedIndex = -1;\n const query = input.value.trim();\n clearTimeout(debounceTimeout);\n if (query.length < 2) {\n dropdown.classList.remove('show');\n dropdown.innerHTML = '';\n return;\n }\n debounceTimeout = setTimeout(() => {\n fetch('/api/stocksearch?q=' + encodeURIComponent(query))\n .then(res => res.json())\n .then(data => {\n if (data.quotes && data.quotes.length > 0) {\n dropdown.innerHTML = data.quotes.map(item =>\n `<a class=\"dropdown-item-search\" href=\"/details?stock=${encodeURIComponent(item.symbol)}\">${item.longname || item.shortname || item.symbol} (${item.symbol})</a>`\n ).join('');\n dropdown.classList.add('show');\n } else {\n dropdown.innerHTML = '<div class=\"dropdown-item-search\">Keine Ergebnisse</div>';\n dropdown.classList.add('show');\n }\n });\n }, 300);\n });\n\n input.addEventListener('keydown', function (e) {\n const items = Array.from(dropdown.querySelectorAll('.dropdown-item-search'));\n if (!dropdown.classList.contains('show') || items.length === 0) return;\n\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n selectedIndex = (selectedIndex + 1) % items.length;\n updateDropdownSelection(items);\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n selectedIndex = (selectedIndex - 1 + items.length) % items.length;\n updateDropdownSelection(items);\n } else if (e.key === 'Enter') {\n if (selectedIndex >= 0 && selectedIndex < items.length) {\n e.preventDefault();\n window.location.href = items[selectedIndex].getAttribute('href');\n }\n }\n });\n\n function updateDropdownSelection(items) {\n items.forEach((item, idx) => {\n if (idx === selectedIndex) {\n item.classList.add('active');\n item.scrollIntoView({ block: 'nearest' });\n } else {\n item.classList.remove('active');\n }\n });\n }\n\n document.addEventListener('click', function (e) {\n if (!input.contains(e.target) && !dropdown.contains(e.target)) {\n dropdown.classList.remove('show');\n }\n });\n\n document.getElementById('stock-search-form').addEventListener('submit', function(e) {\n e.preventDefault();\n const items = Array.from(dropdown.querySelectorAll('.dropdown-item-search'));\n // Wenn ein Eintrag ausgewählt ist, nimm diesen\n if (selectedIndex >= 0 && selectedIndex < items.length) {\n window.location.href = items[selectedIndex].getAttribute('href');\n return;\n }\n // Sonst nimm das erste Ergebnis, falls vorhanden\n if (items.length > 0) {\n window.location.href = items[0].getAttribute('href');\n return;\n }\n // Sonst nichts tun\n });\n </script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate