first commit
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"tankstopp/internal/currency"
|
||||
"tankstopp/internal/models"
|
||||
)
|
||||
|
||||
templ FormGroup(label, hint string) {
|
||||
<div class="mb-3">
|
||||
if label != "" {
|
||||
<label class="form-label">
|
||||
{ label }
|
||||
</label>
|
||||
}
|
||||
{ children... }
|
||||
if hint != "" {
|
||||
<div class="form-hint">{ hint }</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
templ Input(name, inputType, placeholder, value string, required bool) {
|
||||
<input
|
||||
type={ inputType }
|
||||
class="form-control"
|
||||
name={ name }
|
||||
id={ name }
|
||||
placeholder={ placeholder }
|
||||
value={ value }
|
||||
if required {
|
||||
required
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
||||
templ NumberInput(name, placeholder string, value float64, step string, min float64, required bool) {
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
name={ name }
|
||||
id={ name }
|
||||
placeholder={ placeholder }
|
||||
value={ fmt.Sprintf("%.2f", value) }
|
||||
step={ step }
|
||||
min={ fmt.Sprintf("%.2f", min) }
|
||||
if required {
|
||||
required
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
||||
templ DateInput(name, value string, required bool) {
|
||||
<input
|
||||
type="date"
|
||||
class="form-control"
|
||||
name={ name }
|
||||
id={ name }
|
||||
value={ value }
|
||||
if required {
|
||||
required
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
||||
templ TextArea(name, placeholder, value string, rows int) {
|
||||
<textarea
|
||||
class="form-control"
|
||||
name={ name }
|
||||
id={ name }
|
||||
rows={ fmt.Sprintf("%d", rows) }
|
||||
placeholder={ placeholder }
|
||||
>{ value }</textarea>
|
||||
}
|
||||
|
||||
templ Select(name string, required bool) {
|
||||
<select
|
||||
class="form-select"
|
||||
name={ name }
|
||||
id={ name }
|
||||
if required {
|
||||
required
|
||||
}
|
||||
>
|
||||
{ children... }
|
||||
</select>
|
||||
}
|
||||
|
||||
templ Option(value, text string, selected bool) {
|
||||
<option
|
||||
value={ value }
|
||||
if selected {
|
||||
selected
|
||||
}
|
||||
>{ text }</option>
|
||||
}
|
||||
|
||||
templ CurrencySelect(name, selectedCurrency string, currencies []currency.Currency) {
|
||||
@Select(name, false) {
|
||||
@Option("", "Select currency...", selectedCurrency == "")
|
||||
for _, curr := range currencies {
|
||||
@Option(curr.Code, fmt.Sprintf("%s %s - %s", curr.Symbol, curr.Code, curr.Name), curr.Code == selectedCurrency)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
templ VehicleSelect(name string, selectedVehicleID uint, vehicles []models.Vehicle, required bool) {
|
||||
@Select(name, required) {
|
||||
@Option("", "Select vehicle...", selectedVehicleID == 0)
|
||||
for _, vehicle := range vehicles {
|
||||
if vehicle.LicensePlate != "" {
|
||||
@Option(fmt.Sprintf("%d", vehicle.ID), fmt.Sprintf("%s (%s)", vehicle.Name, vehicle.LicensePlate), vehicle.ID == selectedVehicleID)
|
||||
} else {
|
||||
@Option(fmt.Sprintf("%d", vehicle.ID), vehicle.Name, vehicle.ID == selectedVehicleID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
templ FuelTypeSelect(name, selectedFuelType string, required bool) {
|
||||
@Select(name, required) {
|
||||
@Option("", "Select fuel type...", selectedFuelType == "")
|
||||
@Option("Super E5", "Super E5", selectedFuelType == "Super E5")
|
||||
@Option("Super E10", "Super E10", selectedFuelType == "Super E10")
|
||||
@Option("Super Plus", "Super Plus", selectedFuelType == "Super Plus")
|
||||
@Option("Diesel", "Diesel", selectedFuelType == "Diesel")
|
||||
@Option("Premium Diesel", "Premium Diesel", selectedFuelType == "Premium Diesel")
|
||||
@Option("LPG", "LPG", selectedFuelType == "LPG")
|
||||
@Option("CNG", "CNG", selectedFuelType == "CNG")
|
||||
@Option("Electric", "Electric", selectedFuelType == "Electric")
|
||||
@Option("Hybrid", "Hybrid (Mixed)", selectedFuelType == "Hybrid")
|
||||
}
|
||||
}
|
||||
|
||||
templ InputGroup(prefix, suffix string) {
|
||||
<div class="input-group">
|
||||
if prefix != "" {
|
||||
<span class="input-group-text" id={ prefix + "-addon" }>{ prefix }</span>
|
||||
}
|
||||
{ children... }
|
||||
if suffix != "" {
|
||||
<span class="input-group-text" id={ suffix + "-addon" }>{ suffix }</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
templ PasswordInput(name, placeholder string, required bool) {
|
||||
<div class="input-group input-group-flat">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
name={ name }
|
||||
placeholder={ placeholder }
|
||||
autocomplete="off"
|
||||
if required {
|
||||
required
|
||||
}
|
||||
/>
|
||||
<span class="input-group-text">
|
||||
<a href="#" class="link-secondary" onclick="togglePassword(this)" title="Show password" data-target={ name }>
|
||||
@Icon("eye", 24)
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ Switch(name, label string, checked bool) {
|
||||
<label class="form-check form-switch">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
name={ name }
|
||||
if checked {
|
||||
checked
|
||||
}
|
||||
/>
|
||||
<span class="form-check-label">{ label }</span>
|
||||
</label>
|
||||
}
|
||||
|
||||
templ FormButtons(cancelHref, submitText, submitIcon string) {
|
||||
<div class="card-footer bg-transparent mt-auto">
|
||||
<div class="btn-list justify-content-end">
|
||||
<a href={ templ.SafeURL(cancelHref) } class="btn">
|
||||
@Icon("arrow-left", 24)
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary ms-auto">
|
||||
if submitIcon != "" {
|
||||
@Icon(submitIcon, 24)
|
||||
}
|
||||
{ submitText }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ Form(method, action string) {
|
||||
<form method={ method } action={ action }>
|
||||
{ children... }
|
||||
</form>
|
||||
}
|
||||
|
||||
templ FormRow() {
|
||||
<div class="row">
|
||||
{ children... }
|
||||
</div>
|
||||
}
|
||||
|
||||
templ FormCol(size string) {
|
||||
<div class={ "col-md-" + size }>
|
||||
{ children... }
|
||||
</div>
|
||||
}
|
||||
|
||||
templ DeleteButton(action, itemName string) {
|
||||
<form method="POST" action={ action } style="display: inline;" onsubmit="return confirmDelete(this)" data-item={ itemName }>
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
@Icon("trash", 24)
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
|
||||
templ EditButton(href string) {
|
||||
<a href={ templ.SafeURL(href) } class="btn btn-sm btn-outline-primary">
|
||||
@Icon("edit", 24)
|
||||
Edit
|
||||
</a>
|
||||
}
|
||||
|
||||
templ ButtonGroup() {
|
||||
<div class="btn-list">
|
||||
{ children... }
|
||||
</div>
|
||||
}
|
||||
|
||||
templ PrimaryButton(text string, icon string) {
|
||||
<button type="submit" class="btn btn-primary">
|
||||
if icon != "" {
|
||||
@Icon(icon, 24)
|
||||
}
|
||||
{ text }
|
||||
</button>
|
||||
}
|
||||
|
||||
templ SecondaryButton(href, text string, icon string) {
|
||||
<a href={ templ.SafeURL(href) } class="btn">
|
||||
if icon != "" {
|
||||
@Icon(icon, 24)
|
||||
}
|
||||
{ text }
|
||||
</a>
|
||||
}
|
||||
|
||||
templ InputWithIcon(name, inputType, placeholder, value string, icon string, required bool) {
|
||||
@FormGroup("", "") {
|
||||
if icon != "" {
|
||||
@Icon(icon, 24)
|
||||
}
|
||||
@Input(name, inputType, placeholder, value, required)
|
||||
}
|
||||
}
|
||||
|
||||
templ CurrencyInputGroup(name string, value float64, currencySymbol string, step string) {
|
||||
@InputGroup(currencySymbol, "") {
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
name={ name }
|
||||
id={ name }
|
||||
step={ step }
|
||||
min="0"
|
||||
value={ fmt.Sprintf("%.2f", value) }
|
||||
placeholder="0.00"
|
||||
required
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
templ RefreshButton() {
|
||||
<button class="btn btn-outline-secondary" type="button">
|
||||
@Icon("refresh", 24)
|
||||
</button>
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,200 @@
|
||||
package components
|
||||
|
||||
import "fmt"
|
||||
|
||||
templ Icon(name string, size int) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon"
|
||||
width={ fmt.Sprintf("%d", size) }
|
||||
height={ fmt.Sprintf("%d", size) }
|
||||
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>
|
||||
switch name {
|
||||
case "fuel":
|
||||
<path d="M14 11h1a2 2 0 0 1 2 2v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1 -1v-2a6 6 0 0 0 -6 -6h-1m-4 0a2 2 0 0 1 -2 -2v-4a2 2 0 0 1 2 -2h6a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-6a2 2 0 0 1 -2 -2v-6z"></path>
|
||||
case "plus":
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
case "home":
|
||||
<polyline points="5,12 3,12 12,3 21,12 19,12"></polyline>
|
||||
<path d="m5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7"></path>
|
||||
<path d="m9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6"></path>
|
||||
case "car":
|
||||
<circle cx="7" cy="17" r="2"></circle>
|
||||
<circle cx="17" cy="17" r="2"></circle>
|
||||
<path d="M5 17h-2v-6l2 -5h9l4 5h1a2 2 0 0 1 2 2v4h-2m-4 0h-6m-6 -6h15m-6 0v-5"></path>
|
||||
case "chart-bar":
|
||||
<path d="M3 12m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v6a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"></path>
|
||||
<path d="M9 8m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"></path>
|
||||
<path d="M15 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"></path>
|
||||
case "settings":
|
||||
<path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"></path>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
case "logout":
|
||||
<path d="M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2"></path>
|
||||
<path d="M20 12h-13l3 -3m0 6l-3 -3"></path>
|
||||
case "check":
|
||||
<path d="M5 12l5 5l10 -10"></path>
|
||||
case "alert-circle":
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
case "alert-triangle":
|
||||
<path d="M12 9v2m0 4v.01"></path>
|
||||
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
|
||||
case "info-circle":
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||
<polyline points="11,12 12,12 12,16 13,16"></polyline>
|
||||
case "calendar":
|
||||
<rect x="4" y="5" width="16" height="16" rx="2"></rect>
|
||||
<line x1="16" y1="3" x2="16" y2="7"></line>
|
||||
<line x1="8" y1="3" x2="8" y2="7"></line>
|
||||
<line x1="4" y1="11" x2="20" y2="11"></line>
|
||||
case "location":
|
||||
<circle cx="12" cy="11" r="3"></circle>
|
||||
<path d="M17.657 16.657l-4.243 4.243a2 2 0 0 1 -2.827 0l-4.244 -4.243a8 8 0 1 1 11.314 0z"></path>
|
||||
case "edit":
|
||||
<path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"></path>
|
||||
<path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z"></path>
|
||||
<path d="M16 5l3 3"></path>
|
||||
case "trash":
|
||||
<path d="M4 7l16 0"></path>
|
||||
<path d="M10 11l0 6"></path>
|
||||
<path d="M14 11l0 6"></path>
|
||||
<path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12"></path>
|
||||
<path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3"></path>
|
||||
case "currency":
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M3 12h6m6 0h6"></path>
|
||||
case "save":
|
||||
<path d="M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2"></path>
|
||||
<circle cx="12" cy="14" r="2"></circle>
|
||||
<polyline points="14,4 14,8 8,8 8,4"></polyline>
|
||||
case "arrow-left":
|
||||
<path d="M5 12l14 -7"></path>
|
||||
<path d="M5 12l14 7"></path>
|
||||
case "clock":
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
<polyline points="12,7 12,12 15,15"></polyline>
|
||||
case "gas-station":
|
||||
<path d="M6.8 11a6 6 0 1 0 10.396 0l-.436 -2.183a4 4 0 1 0 -9.564 0l-.396 2.183z"></path>
|
||||
<path d="M6 14h12a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-2a2 2 0 0 1 2 -2z"></path>
|
||||
case "user":
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
<path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path>
|
||||
case "lock":
|
||||
<rect x="5" y="11" width="14" height="10" rx="2"></rect>
|
||||
<circle cx="12" cy="16" r="1"></circle>
|
||||
<path d="M8 11v-4a4 4 0 0 1 8 0v4"></path>
|
||||
case "eye":
|
||||
<circle cx="12" cy="12" r="2"></circle>
|
||||
<path d="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7"></path>
|
||||
case "eye-off":
|
||||
<line x1="3" y1="3" x2="21" y2="21"></line>
|
||||
<path d="M10.584 10.587a2 2 0 0 0 2.828 2.83"></path>
|
||||
<path d="M9.363 5.365a9.466 9.466 0 0 1 2.637 -.365c4 0 7.333 2.333 10 7c-.778 1.361 -1.612 2.524 -2.503 3.488m-2.14 1.861c-1.631 1.1 -3.415 1.651 -5.357 1.651c-4 0 -7.333 -2.333 -10 -7c1.369 -2.395 2.913 -4.175 4.632 -5.341"></path>
|
||||
case "gauge":
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
<path d="M14.8 9a2 2 0 0 0 -1.8 -1h-2a2 2 0 1 0 0 4h2a2 2 0 1 1 0 4h-2a2 2 0 0 1 -1.8 -1"></path>
|
||||
<path d="M12 6v2m0 8v2"></path>
|
||||
case "notes":
|
||||
<path d="M8 2v4"></path>
|
||||
<path d="M16 2v4"></path>
|
||||
<rect x="3" y="4" width="18" height="18" rx="2"></rect>
|
||||
<path d="M3 10h18"></path>
|
||||
<path d="M8 14h.01"></path>
|
||||
<path d="M12 14h.01"></path>
|
||||
<path d="M16 14h.01"></path>
|
||||
<path d="M8 18h.01"></path>
|
||||
<path d="M12 18h.01"></path>
|
||||
<path d="M16 18h.01"></path>
|
||||
case "refresh":
|
||||
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
|
||||
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
|
||||
case "license-plate":
|
||||
<rect x="4" y="4" width="6" height="6" rx="1"></rect>
|
||||
<rect x="4" y="14" width="6" height="6" rx="1"></rect>
|
||||
<rect x="14" y="14" width="6" height="6" rx="1"></rect>
|
||||
<line x1="14" y1="7" x2="20" y2="7"></line>
|
||||
<line x1="17" y1="4" x2="17" y2="10"></line>
|
||||
case "brand":
|
||||
<path d="M9 11l-4 4l4 4m-4 -4h11a4 4 0 0 0 0 -8h-1"></path>
|
||||
case "model":
|
||||
<path d="M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9z"></path>
|
||||
<path d="M12 12l8 -4.5"></path>
|
||||
<path d="M12 12l0 9"></path>
|
||||
<path d="M12 12l-8 -4.5"></path>
|
||||
case "status":
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
<polyline points="9,11 12,8 15,11"></polyline>
|
||||
<line x1="12" y1="8" x2="12" y2="16"></line>
|
||||
case "trip":
|
||||
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"></path>
|
||||
<path d="M12 7v5l3 3"></path>
|
||||
case "database":
|
||||
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
|
||||
<path d="M3 5v14a9 3 0 0 0 18 0v-14"></path>
|
||||
<path d="M3 12a9 3 0 0 0 18 0"></path>
|
||||
case "download":
|
||||
<path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2"></path>
|
||||
<polyline points="7,11 12,16 17,11"></polyline>
|
||||
<line x1="12" y1="2" x2="12" y2="16"></line>
|
||||
case "upload":
|
||||
<path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2"></path>
|
||||
<polyline points="7,9 12,4 17,9"></polyline>
|
||||
<line x1="12" y1="4" x2="12" y2="16"></line>
|
||||
case "zap":
|
||||
<polygon points="13,2 3,14 12,14 11,22 21,10 12,10"></polygon>
|
||||
case "search":
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
case "dots-vertical":
|
||||
<circle cx="12" cy="12" r="1"></circle>
|
||||
<circle cx="12" cy="5" r="1"></circle>
|
||||
<circle cx="12" cy="19" r="1"></circle>
|
||||
case "award":
|
||||
<circle cx="12" cy="8" r="7"></circle>
|
||||
<polyline points="8.21,13.89 7,23 12,20 17,23 15.79,13.88"></polyline>
|
||||
default:
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
}
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ IconWithClass(name string, size int, class string) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={ "icon", class }
|
||||
width={ fmt.Sprintf("%d", size) }
|
||||
height={ fmt.Sprintf("%d", size) }
|
||||
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>
|
||||
@renderIconPath(name)
|
||||
</svg>
|
||||
}
|
||||
|
||||
templ renderIconPath(name string) {
|
||||
switch name {
|
||||
case "fuel":
|
||||
<path d="M14 11h1a2 2 0 0 1 2 2v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1 -1v-2a6 6 0 0 0 -6 -6h-1m-4 0a2 2 0 0 1 -2 -2v-4a2 2 0 0 1 2 -2h6a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-6a2 2 0 0 1 -2 -2v-6z"></path>
|
||||
case "plus":
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
default:
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.906
|
||||
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"
|
||||
|
||||
func Icon(name string, size int) 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 = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" width=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", size))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/components/icons.templ`, Line: 9, Col: 33}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" height=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", size))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/components/icons.templ`, Line: 10, Col: 34}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" 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> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
switch name {
|
||||
case "fuel":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<path d=\"M14 11h1a2 2 0 0 1 2 2v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1 -1v-2a6 6 0 0 0 -6 -6h-1m-4 0a2 2 0 0 1 -2 -2v-4a2 2 0 0 1 2 -2h6a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-6a2 2 0 0 1 -2 -2v-6z\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "plus":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\"></line> <line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"></line>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "home":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<polyline points=\"5,12 3,12 12,3 21,12 19,12\"></polyline> <path d=\"m5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7\"></path> <path d=\"m9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "car":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<circle cx=\"7\" cy=\"17\" r=\"2\"></circle> <circle cx=\"17\" cy=\"17\" r=\"2\"></circle> <path d=\"M5 17h-2v-6l2 -5h9l4 5h1a2 2 0 0 1 2 2v4h-2m-4 0h-6m-6 -6h15m-6 0v-5\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "chart-bar":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<path d=\"M3 12m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v6a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z\"></path> <path d=\"M9 8m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z\"></path> <path d=\"M15 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "settings":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<path d=\"M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z\"></path> <circle cx=\"12\" cy=\"12\" r=\"3\"></circle>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "logout":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<path d=\"M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2\"></path> <path d=\"M20 12h-13l3 -3m0 6l-3 -3\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "check":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<path d=\"M5 12l5 5l10 -10\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "alert-circle":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<circle cx=\"12\" cy=\"12\" r=\"9\"></circle> <line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\"></line> <line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\"></line>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "alert-triangle":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<path d=\"M12 9v2m0 4v.01\"></path> <path d=\"M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "info-circle":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<circle cx=\"12\" cy=\"12\" r=\"9\"></circle> <line x1=\"12\" y1=\"8\" x2=\"12.01\" y2=\"8\"></line> <polyline points=\"11,12 12,12 12,16 13,16\"></polyline>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "calendar":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<rect x=\"4\" y=\"5\" width=\"16\" height=\"16\" rx=\"2\"></rect> <line x1=\"16\" y1=\"3\" x2=\"16\" y2=\"7\"></line> <line x1=\"8\" y1=\"3\" x2=\"8\" y2=\"7\"></line> <line x1=\"4\" y1=\"11\" x2=\"20\" y2=\"11\"></line>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "location":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<circle cx=\"12\" cy=\"11\" r=\"3\"></circle> <path d=\"M17.657 16.657l-4.243 4.243a2 2 0 0 1 -2.827 0l-4.244 -4.243a8 8 0 1 1 11.314 0z\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "edit":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<path d=\"M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1\"></path> <path d=\"M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z\"></path> <path d=\"M16 5l3 3\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "trash":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<path d=\"M4 7l16 0\"></path> <path d=\"M10 11l0 6\"></path> <path d=\"M14 11l0 6\"></path> <path d=\"M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12\"></path> <path d=\"M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "currency":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<circle cx=\"12\" cy=\"12\" r=\"3\"></circle> <path d=\"M3 12h6m6 0h6\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "save":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<path d=\"M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2\"></path> <circle cx=\"12\" cy=\"14\" r=\"2\"></circle> <polyline points=\"14,4 14,8 8,8 8,4\"></polyline>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "arrow-left":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<path d=\"M5 12l14 -7\"></path> <path d=\"M5 12l14 7\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "clock":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<circle cx=\"12\" cy=\"12\" r=\"9\"></circle> <polyline points=\"12,7 12,12 15,15\"></polyline>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "gas-station":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<path d=\"M6.8 11a6 6 0 1 0 10.396 0l-.436 -2.183a4 4 0 1 0 -9.564 0l-.396 2.183z\"></path> <path d=\"M6 14h12a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-2a2 2 0 0 1 2 -2z\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "user":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<circle cx=\"12\" cy=\"7\" r=\"4\"></circle> <path d=\"M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "lock":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<rect x=\"5\" y=\"11\" width=\"14\" height=\"10\" rx=\"2\"></rect> <circle cx=\"12\" cy=\"16\" r=\"1\"></circle> <path d=\"M8 11v-4a4 4 0 0 1 8 0v4\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "eye":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<circle cx=\"12\" cy=\"12\" r=\"2\"></circle> <path d=\"M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "eye-off":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<line x1=\"3\" y1=\"3\" x2=\"21\" y2=\"21\"></line> <path d=\"M10.584 10.587a2 2 0 0 0 2.828 2.83\"></path> <path d=\"M9.363 5.365a9.466 9.466 0 0 1 2.637 -.365c4 0 7.333 2.333 10 7c-.778 1.361 -1.612 2.524 -2.503 3.488m-2.14 1.861c-1.631 1.1 -3.415 1.651 -5.357 1.651c-4 0 -7.333 -2.333 -10 -7c1.369 -2.395 2.913 -4.175 4.632 -5.341\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "gauge":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<circle cx=\"12\" cy=\"12\" r=\"9\"></circle> <path d=\"M14.8 9a2 2 0 0 0 -1.8 -1h-2a2 2 0 1 0 0 4h2a2 2 0 1 1 0 4h-2a2 2 0 0 1 -1.8 -1\"></path> <path d=\"M12 6v2m0 8v2\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "notes":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<path d=\"M8 2v4\"></path> <path d=\"M16 2v4\"></path> <rect x=\"3\" y=\"4\" width=\"18\" height=\"18\" rx=\"2\"></rect> <path d=\"M3 10h18\"></path> <path d=\"M8 14h.01\"></path> <path d=\"M12 14h.01\"></path> <path d=\"M16 14h.01\"></path> <path d=\"M8 18h.01\"></path> <path d=\"M12 18h.01\"></path> <path d=\"M16 18h.01\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "refresh":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<path d=\"M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4\"></path> <path d=\"M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "license-plate":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<rect x=\"4\" y=\"4\" width=\"6\" height=\"6\" rx=\"1\"></rect> <rect x=\"4\" y=\"14\" width=\"6\" height=\"6\" rx=\"1\"></rect> <rect x=\"14\" y=\"14\" width=\"6\" height=\"6\" rx=\"1\"></rect> <line x1=\"14\" y1=\"7\" x2=\"20\" y2=\"7\"></line> <line x1=\"17\" y1=\"4\" x2=\"17\" y2=\"10\"></line>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "brand":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<path d=\"M9 11l-4 4l4 4m-4 -4h11a4 4 0 0 0 0 -8h-1\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "model":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<path d=\"M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9z\"></path> <path d=\"M12 12l8 -4.5\"></path> <path d=\"M12 12l0 9\"></path> <path d=\"M12 12l-8 -4.5\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "status":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<circle cx=\"12\" cy=\"12\" r=\"9\"></circle> <polyline points=\"9,11 12,8 15,11\"></polyline> <line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"16\"></line>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "trip":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<path d=\"M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0\"></path> <path d=\"M12 7v5l3 3\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "database":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<ellipse cx=\"12\" cy=\"5\" rx=\"9\" ry=\"3\"></ellipse> <path d=\"M3 5v14a9 3 0 0 0 18 0v-14\"></path> <path d=\"M3 12a9 3 0 0 0 18 0\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "download":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<path d=\"M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2\"></path> <polyline points=\"7,11 12,16 17,11\"></polyline> <line x1=\"12\" y1=\"2\" x2=\"12\" y2=\"16\"></line>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "upload":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<path d=\"M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2\"></path> <polyline points=\"7,9 12,4 17,9\"></polyline> <line x1=\"12\" y1=\"4\" x2=\"12\" y2=\"16\"></line>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "zap":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<polygon points=\"13,2 3,14 12,14 11,22 21,10 12,10\"></polygon>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "search":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<circle cx=\"11\" cy=\"11\" r=\"8\"></circle> <line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"></line>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "dots-vertical":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<circle cx=\"12\" cy=\"12\" r=\"1\"></circle> <circle cx=\"12\" cy=\"5\" r=\"1\"></circle> <circle cx=\"12\" cy=\"19\" r=\"1\"></circle>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "award":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<circle cx=\"12\" cy=\"8\" r=\"7\"></circle> <polyline points=\"8.21,13.89 7,23 12,20 17,23 15.79,13.88\"></polyline>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
default:
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<circle cx=\"12\" cy=\"12\" r=\"10\"></circle>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func IconWithClass(name string, size int, class string) 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_Var4 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var4 == nil {
|
||||
templ_7745c5c3_Var4 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var5 = []any{"icon", class}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var5).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/components/icons.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\" width=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", size))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/components/icons.templ`, Line: 176, Col: 33}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\" height=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", size))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/views/components/icons.templ`, Line: 177, Col: 34}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\" 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>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = renderIconPath(name).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</svg>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func renderIconPath(name string) 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_Var9 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var9 == nil {
|
||||
templ_7745c5c3_Var9 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
switch name {
|
||||
case "fuel":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<path d=\"M14 11h1a2 2 0 0 1 2 2v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1 -1v-2a6 6 0 0 0 -6 -6h-1m-4 0a2 2 0 0 1 -2 -2v-4a2 2 0 0 1 2 -2h6a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-6a2 2 0 0 1 -2 -2v-6z\"></path>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "plus":
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\"></line> <line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"></line>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
default:
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<circle cx=\"12\" cy=\"12\" r=\"10\"></circle>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,405 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"tankstopp/internal/models"
|
||||
)
|
||||
|
||||
templ BaseLayout(title string, user *models.User, username string) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>{ title } - TankStopp</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="https://cdn.jsdelivr.net/npm/@tabler/icons@latest/icons-sprite.svg" rel="stylesheet"/>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
@Navbar(user, username)
|
||||
<div class="page-wrapper">
|
||||
{ children... }
|
||||
@Footer()
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/js/tabler.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
templ Navbar(user *models.User, username string) {
|
||||
<header class="navbar navbar-expand-md navbar-light d-print-none">
|
||||
<div class="container-xl">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
|
||||
<a href="/dashboard" class="text-decoration-none">
|
||||
@Icon("fuel", 24)
|
||||
TankStopp
|
||||
</a>
|
||||
</h1>
|
||||
<div class="navbar-nav flex-row order-md-last">
|
||||
if user != nil {
|
||||
<a href="/add" class="btn btn-primary me-2">
|
||||
@Icon("plus", 24)
|
||||
Add Stop
|
||||
</a>
|
||||
@UserDropdown(user, username)
|
||||
}
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="navbar-menu">
|
||||
<div class="d-flex flex-column flex-md-row flex-fill align-items-stretch align-items-md-center">
|
||||
<ul class="navbar-nav">
|
||||
@NavItem("/dashboard", "home", "Dashboard", false)
|
||||
@NavItem("/vehicles", "car", "Vehicles", false)
|
||||
@NavItem("/api/stats", "chart-bar", "API", false)
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
}
|
||||
|
||||
templ NavItem(href, icon, title string, active bool) {
|
||||
<li class={ "nav-item", templ.KV("active", active) }>
|
||||
<a class="nav-link" href={ templ.SafeURL(href) }>
|
||||
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||
@Icon(icon, 24)
|
||||
</span>
|
||||
<span class="nav-link-title">{ title }</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
|
||||
templ UserDropdown(user *models.User, username string) {
|
||||
<div class="nav-item dropdown">
|
||||
<a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu">
|
||||
<span class="avatar avatar-sm" style="background: var(--tblr-primary); text-transform: uppercase;">
|
||||
if username != "" {
|
||||
{ string(username[0]) }
|
||||
} else {
|
||||
{ "U" }
|
||||
}
|
||||
</span>
|
||||
<div class="d-none d-xl-block ps-2">
|
||||
<div>{ username }</div>
|
||||
<div class="mt-1 small text-muted">
|
||||
{ user.BaseCurrency }
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
|
||||
<div class="dropdown-item">
|
||||
<div class="text-muted">Signed in as</div>
|
||||
<strong>{ username }</strong>
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="/settings" class="dropdown-item">
|
||||
@Icon("settings", 24)
|
||||
Settings
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<form method="POST" action="/logout" class="d-inline">
|
||||
<button type="submit" class="dropdown-item text-danger">
|
||||
@Icon("logout", 24)
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ Footer() {
|
||||
<footer class="footer footer-transparent d-print-none">
|
||||
<div class="container-xl">
|
||||
<div class="row text-center align-items-center flex-row-reverse">
|
||||
<div class="col-lg-auto ms-lg-auto">
|
||||
<ul class="list-inline list-inline-dots mb-0">
|
||||
<li class="list-inline-item">
|
||||
<a href="https://github.com/tabler/tabler" class="link-secondary">Built with Tabler</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-12 col-lg-auto mt-3 mt-lg-0">
|
||||
<ul class="list-inline list-inline-dots mb-0">
|
||||
<li class="list-inline-item">
|
||||
Copyright © 2024 TankStopp - Fuel Tracking App
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
}
|
||||
|
||||
templ PageHeader(pretitle, title string) {
|
||||
<div class="page-header d-print-none">
|
||||
<div class="container-xl">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
if pretitle != "" {
|
||||
<div class="page-pretitle">{ pretitle }</div>
|
||||
}
|
||||
<h2 class="page-title">{ title }</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ Alert(alertType, message string) {
|
||||
<div class={ "alert", "alert-" + alertType, "alert-dismissible" } role="alert">
|
||||
<div class="d-flex">
|
||||
<div>
|
||||
switch alertType {
|
||||
case "success":
|
||||
@Icon("check", 24)
|
||||
case "danger":
|
||||
@Icon("alert-circle", 24)
|
||||
case "warning":
|
||||
@Icon("alert-triangle", 24)
|
||||
case "info":
|
||||
@Icon("info-circle", 24)
|
||||
}
|
||||
</div>
|
||||
<div>{ message }</div>
|
||||
</div>
|
||||
<a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ Card(title string, icon string) {
|
||||
<div class="card">
|
||||
if title != "" {
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
if icon != "" {
|
||||
@Icon(icon, 24)
|
||||
}
|
||||
{ title }
|
||||
</h3>
|
||||
</div>
|
||||
}
|
||||
<div class="card-body">
|
||||
{ children... }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ EmptyState(icon, title, subtitle, actionText, actionHref string) {
|
||||
<div class="empty">
|
||||
<div class="empty-img">
|
||||
@Icon(icon, 128)
|
||||
</div>
|
||||
<p class="empty-title">{ title }</p>
|
||||
<p class="empty-subtitle text-muted">{ subtitle }</p>
|
||||
if actionText != "" && actionHref != "" {
|
||||
<div class="empty-action">
|
||||
<a href={ templ.SafeURL(actionHref) } class="btn btn-primary">
|
||||
@Icon("plus", 24)
|
||||
{ actionText }
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
templ LoadingSpinner(size string) {
|
||||
<div class={ "spinner-border", "spinner-border-" + size } role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ Badge(text, variant string) {
|
||||
<span class={ "badge", "bg-" + variant }>{ text }</span>
|
||||
}
|
||||
|
||||
templ ProgressBar(percentage int, variant string) {
|
||||
<div class="progress">
|
||||
<div class={ "progress-bar", "bg-" + variant } role="progressbar" style={ fmt.Sprintf("width: %d%%", percentage) }>
|
||||
{ fmt.Sprintf("%d%%", percentage) }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ Modal(id, title string) {
|
||||
<div class="modal fade" id={ id } tabindex="-1" aria-labelledby={ id + "Label" } aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id={ id + "Label" }>{ title }</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{ children... }
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{ children... }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ Tooltip(text string) {
|
||||
<span data-bs-toggle="tooltip" data-bs-placement="top" title={ text }>
|
||||
{ children... }
|
||||
</span>
|
||||
}
|
||||
|
||||
templ Breadcrumb(items []BreadcrumbItem) {
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
for i, item := range items {
|
||||
if i == len(items)-1 {
|
||||
<li class="breadcrumb-item active" aria-current="page">{ item.Title }</li>
|
||||
} else {
|
||||
<li class="breadcrumb-item">
|
||||
<a href={ templ.SafeURL(item.Href) }>{ item.Title }</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</ol>
|
||||
</nav>
|
||||
}
|
||||
|
||||
type BreadcrumbItem struct {
|
||||
Title string
|
||||
Href string
|
||||
}
|
||||
|
||||
templ Tabs(activeTab string, tabs []TabItem) {
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
for _, tab := range tabs {
|
||||
<li class="nav-item" role="presentation">
|
||||
<a
|
||||
class={ "nav-link", templ.KV("active", tab.ID == activeTab) }
|
||||
href={ templ.SafeURL(tab.Href) }
|
||||
role="tab"
|
||||
>
|
||||
if tab.Icon != "" {
|
||||
@Icon(tab.Icon, 24)
|
||||
}
|
||||
{ tab.Title }
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
||||
type TabItem struct {
|
||||
ID string
|
||||
Title string
|
||||
Href string
|
||||
Icon string
|
||||
}
|
||||
|
||||
templ StatCard(title, value, subtitle, icon, variant string) {
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-fill">
|
||||
<div class="subheader">{ title }</div>
|
||||
<div class="h2 mb-0">{ value }</div>
|
||||
if subtitle != "" {
|
||||
<div class="text-muted">{ subtitle }</div>
|
||||
}
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<div class={ "text-" + variant }>
|
||||
@Icon(icon, 32)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ ActionButton(href, text, icon, variant string) {
|
||||
<a href={ templ.SafeURL(href) } class={ "btn", "btn-" + variant }>
|
||||
if icon != "" {
|
||||
@Icon(icon, 24)
|
||||
}
|
||||
{ text }
|
||||
</a>
|
||||
}
|
||||
|
||||
templ TableResponsive() {
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter table-mobile-md card-table">
|
||||
{ children... }
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ Pagination(currentPage, totalPages int, baseURL string) {
|
||||
if totalPages > 1 {
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination">
|
||||
if currentPage > 1 {
|
||||
<li class="page-item">
|
||||
<a class="page-link" href={ templ.SafeURL(fmt.Sprintf("%s?page=%d", baseURL, currentPage-1)) }>Previous</a>
|
||||
</li>
|
||||
}
|
||||
for i := 1; i <= totalPages; i++ {
|
||||
<li class={ "page-item", templ.KV("active", i == currentPage) }>
|
||||
<a class="page-link" href={ templ.SafeURL(fmt.Sprintf("%s?page=%d", baseURL, i)) }>{ fmt.Sprintf("%d", i) }</a>
|
||||
</li>
|
||||
}
|
||||
if currentPage < totalPages {
|
||||
<li class="page-item">
|
||||
<a class="page-link" href={ templ.SafeURL(fmt.Sprintf("%s?page=%d", baseURL, currentPage+1)) }>Next</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
|
||||
templ ConfirmDialog(id, title, message, confirmText, cancelText string) {
|
||||
<div class="modal fade" id={ id } tabindex="-1" aria-labelledby={ id + "Label" } aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id={ id + "Label" }>{ title }</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{ message }</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{ cancelText }</button>
|
||||
<button type="button" class="btn btn-danger" onclick="confirmAction(this)" data-action={ id }>{ confirmText }</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ ListGroup() {
|
||||
<div class="list-group">
|
||||
{ children... }
|
||||
</div>
|
||||
}
|
||||
|
||||
templ ListGroupItem(active bool) {
|
||||
<div class={ "list-group-item", templ.KV("active", active) }>
|
||||
{ children... }
|
||||
</div>
|
||||
}
|
||||
|
||||
templ ButtonToolbar() {
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
{ children... }
|
||||
</div>
|
||||
}
|
||||
|
||||
templ StatusIndicator(status, text string) {
|
||||
<span class="status-indicator">
|
||||
<span class={ "status", "status-" + status }></span>
|
||||
{ text }
|
||||
</span>
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,133 @@
|
||||
package pages
|
||||
|
||||
import "tankstopp/internal/views/components"
|
||||
|
||||
templ AuthLayout(title string) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>{ title } - TankStopp</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="https://cdn.jsdelivr.net/npm/@tabler/icons@latest/icons-sprite.svg" rel="stylesheet"/>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
</head>
|
||||
<body class="d-flex flex-column">
|
||||
<div class="page page-center">
|
||||
<div class="container container-tight py-4">
|
||||
<div class="text-center mb-4">
|
||||
<a href="/" class="navbar-brand navbar-brand-autodark">
|
||||
@components.Icon("fuel", 48)
|
||||
TankStopp
|
||||
</a>
|
||||
</div>
|
||||
{ children... }
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/js/tabler.min.js"></script>
|
||||
<script>
|
||||
function togglePassword(fieldName) {
|
||||
const passwordInput = document.querySelector(`input[name="${fieldName}"]`);
|
||||
const icon = passwordInput.nextElementSibling.querySelector('svg');
|
||||
|
||||
if (passwordInput.type === 'password') {
|
||||
passwordInput.type = 'text';
|
||||
icon.innerHTML = `
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<line x1="3" y1="3" x2="21" y2="21"/>
|
||||
<path d="M10.584 10.587a2 2 0 0 0 2.828 2.83"/>
|
||||
<path d="M9.363 5.365a9.466 9.466 0 0 1 2.637 -.365c4 0 7.333 2.333 10 7c-.778 1.361 -1.612 2.524 -2.503 3.488m-2.14 1.861c-1.631 1.1 -3.415 1.651 -5.357 1.651c-4 0 -7.333 -2.333 -10 -7c1.369 -2.395 2.913 -4.175 4.632 -5.341"/>
|
||||
`;
|
||||
} else {
|
||||
passwordInput.type = 'password';
|
||||
icon.innerHTML = `
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<circle cx="12" cy="12" r="2"/>
|
||||
<path d="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7"/>
|
||||
`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
templ LoginPage(errorMessage string) {
|
||||
@AuthLayout("Login") {
|
||||
<div class="card card-md">
|
||||
<div class="card-body">
|
||||
<h2 class="h2 text-center mb-4">Login to your account</h2>
|
||||
if errorMessage != "" {
|
||||
@components.Alert("danger", errorMessage)
|
||||
}
|
||||
@components.Form("post", "/login") {
|
||||
@components.FormGroup("Username", "") {
|
||||
@components.Input("username", "text", "Enter your username", "", true)
|
||||
}
|
||||
@components.FormGroup("Password", "") {
|
||||
@components.PasswordInput("password", "Enter your password", true)
|
||||
}
|
||||
<div class="form-footer">
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
@components.Icon("lock", 24)
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center text-muted mt-3">
|
||||
Don't have an account yet?
|
||||
<a href="/register" tabindex="-1">Sign up</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ RegisterPage(errorMessage string) {
|
||||
@AuthLayout("Register") {
|
||||
<div class="card card-md">
|
||||
<div class="card-body">
|
||||
<h2 class="h2 text-center mb-4">Create new account</h2>
|
||||
if errorMessage != "" {
|
||||
@components.Alert("danger", errorMessage)
|
||||
}
|
||||
@components.Form("post", "/register") {
|
||||
@components.FormGroup("Username", "Choose a unique username") {
|
||||
@components.Input("username", "text", "Enter your username", "", true)
|
||||
}
|
||||
@components.FormGroup("Email", "Enter a valid email address") {
|
||||
@components.Input("email", "email", "Enter your email", "", true)
|
||||
}
|
||||
@components.FormGroup("Password", "Password must be at least 8 characters") {
|
||||
@components.PasswordInput("password", "Enter your password", true)
|
||||
}
|
||||
@components.FormGroup("Confirm Password", "") {
|
||||
@components.PasswordInput("confirm_password", "Confirm your password", true)
|
||||
}
|
||||
@components.FormGroup("Base Currency", "Choose your preferred currency for fuel prices") {
|
||||
@components.Select("base_currency", true) {
|
||||
@components.Option("EUR", "EUR - Euro", false)
|
||||
@components.Option("USD", "USD - US Dollar", false)
|
||||
@components.Option("GBP", "GBP - British Pound", false)
|
||||
@components.Option("CHF", "CHF - Swiss Franc", false)
|
||||
@components.Option("JPY", "JPY - Japanese Yen", false)
|
||||
@components.Option("CAD", "CAD - Canadian Dollar", false)
|
||||
@components.Option("AUD", "AUD - Australian Dollar", false)
|
||||
}
|
||||
}
|
||||
<div class="form-footer">
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
@components.Icon("user", 24)
|
||||
Create account
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center text-muted mt-3">
|
||||
Already have an account?
|
||||
<a href="/login" tabindex="-1">Sign in</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.906
|
||||
package pages
|
||||
|
||||
//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 "tankstopp/internal/views/components"
|
||||
|
||||
func AuthLayout(title string) 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 = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><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/views/pages/auth.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 = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - TankStopp</title><link href=\"https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/css/tabler.min.css\" rel=\"stylesheet\"><link href=\"https://cdn.jsdelivr.net/npm/@tabler/icons@latest/icons-sprite.svg\" rel=\"stylesheet\"><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"d-flex flex-column\"><div class=\"page page-center\"><div class=\"container container-tight py-4\"><div class=\"text-center mb-4\"><a href=\"/\" class=\"navbar-brand navbar-brand-autodark\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.Icon("fuel", 48).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "TankStopp</a></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div></div><script src=\"https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/js/tabler.min.js\"></script><script>\n function togglePassword(fieldName) {\n const passwordInput = document.querySelector(`input[name=\"${fieldName}\"]`);\n const icon = passwordInput.nextElementSibling.querySelector('svg');\n\n if (passwordInput.type === 'password') {\n passwordInput.type = 'text';\n icon.innerHTML = `\n <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"/>\n <line x1=\"3\" y1=\"3\" x2=\"21\" y2=\"21\"/>\n <path d=\"M10.584 10.587a2 2 0 0 0 2.828 2.83\"/>\n <path d=\"M9.363 5.365a9.466 9.466 0 0 1 2.637 -.365c4 0 7.333 2.333 10 7c-.778 1.361 -1.612 2.524 -2.503 3.488m-2.14 1.861c-1.631 1.1 -3.415 1.651 -5.357 1.651c-4 0 -7.333 -2.333 -10 -7c1.369 -2.395 2.913 -4.175 4.632 -5.341\"/>\n `;\n } else {\n passwordInput.type = 'password';\n icon.innerHTML = `\n <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\"/>\n <circle cx=\"12\" cy=\"12\" r=\"2\"/>\n <path d=\"M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7\"/>\n `;\n }\n }\n </script></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func LoginPage(errorMessage string) 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_Var3 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var3 == nil {
|
||||
templ_7745c5c3_Var3 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var4 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
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_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"card card-md\"><div class=\"card-body\"><h2 class=\"h2 text-center mb-4\">Login to your account</h2>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if errorMessage != "" {
|
||||
templ_7745c5c3_Err = components.Alert("danger", errorMessage).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Var5 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
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_Var6 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
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_Err = components.Input("username", "text", "Enter your username", "", true).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = components.FormGroup("Username", "").Render(templ.WithChildren(ctx, templ_7745c5c3_Var6), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var7 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
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_Err = components.PasswordInput("password", "Enter your password", true).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = components.FormGroup("Password", "").Render(templ.WithChildren(ctx, templ_7745c5c3_Var7), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " <div class=\"form-footer\"><button type=\"submit\" class=\"btn btn-primary w-100\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.Icon("lock", 24).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "Sign in</button></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = components.Form("post", "/login").Render(templ.WithChildren(ctx, templ_7745c5c3_Var5), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div></div><div class=\"text-center text-muted mt-3\">Don't have an account yet? <a href=\"/register\" tabindex=\"-1\">Sign up</a></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = AuthLayout("Login").Render(templ.WithChildren(ctx, templ_7745c5c3_Var4), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func RegisterPage(errorMessage string) 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_Var8 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var8 == nil {
|
||||
templ_7745c5c3_Var8 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var9 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
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_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"card card-md\"><div class=\"card-body\"><h2 class=\"h2 text-center mb-4\">Create new account</h2>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if errorMessage != "" {
|
||||
templ_7745c5c3_Err = components.Alert("danger", errorMessage).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Var10 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
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_Var11 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
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_Err = components.Input("username", "text", "Enter your username", "", true).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = components.FormGroup("Username", "Choose a unique username").Render(templ.WithChildren(ctx, templ_7745c5c3_Var11), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var12 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
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_Err = components.Input("email", "email", "Enter your email", "", true).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = components.FormGroup("Email", "Enter a valid email address").Render(templ.WithChildren(ctx, templ_7745c5c3_Var12), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var13 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
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_Err = components.PasswordInput("password", "Enter your password", true).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = components.FormGroup("Password", "Password must be at least 8 characters").Render(templ.WithChildren(ctx, templ_7745c5c3_Var13), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var14 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
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_Err = components.PasswordInput("confirm_password", "Confirm your password", true).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = components.FormGroup("Confirm Password", "").Render(templ.WithChildren(ctx, templ_7745c5c3_Var14), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var15 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
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_Var16 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
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_Err = components.Option("EUR", "EUR - Euro", false).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.Option("USD", "USD - US Dollar", false).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.Option("GBP", "GBP - British Pound", false).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.Option("CHF", "CHF - Swiss Franc", false).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.Option("JPY", "JPY - Japanese Yen", false).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.Option("CAD", "CAD - Canadian Dollar", false).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.Option("AUD", "AUD - Australian Dollar", false).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = components.Select("base_currency", true).Render(templ.WithChildren(ctx, templ_7745c5c3_Var16), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = components.FormGroup("Base Currency", "Choose your preferred currency for fuel prices").Render(templ.WithChildren(ctx, templ_7745c5c3_Var15), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, " <div class=\"form-footer\"><button type=\"submit\" class=\"btn btn-primary w-100\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.Icon("user", 24).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "Create account</button></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = components.Form("post", "/register").Render(templ.WithChildren(ctx, templ_7745c5c3_Var10), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</div></div><div class=\"text-center text-muted mt-3\">Already have an account? <a href=\"/login\" tabindex=\"-1\">Sign in</a></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = AuthLayout("Register").Render(templ.WithChildren(ctx, templ_7745c5c3_Var9), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,478 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"tankstopp/internal/models"
|
||||
"tankstopp/internal/views/components"
|
||||
)
|
||||
|
||||
templ DashboardPage(user *models.User, username string, stops []models.FuelStop, vehicles []models.Vehicle, totalStops int, totalCost float64, avgConsumption float64, lastFillUp *models.FuelStop) {
|
||||
@components.BaseLayout("Dashboard", user, username) {
|
||||
@components.PageHeader("Overview", "Dashboard")
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<!-- Statistics Cards -->
|
||||
<div class="row row-deck row-cards mb-4">
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
@components.Card("Total Stops", "gas-station") {
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="subheader">Fuel stops recorded</div>
|
||||
<div class="ms-auto lh-1">
|
||||
<div class="dropdown">
|
||||
<a class="dropdown-toggle text-muted" href="#" data-bs-toggle="dropdown">Last 30 days</a>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<a class="dropdown-item active" href="#">Last 30 days</a>
|
||||
<a class="dropdown-item" href="#">Last 90 days</a>
|
||||
<a class="dropdown-item" href="#">All time</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h1 mb-3">{ fmt.Sprintf("%d", totalStops) }</div>
|
||||
<div class="d-flex mb-2">
|
||||
<div class="flex-fill">
|
||||
<div class="progress progress-sm">
|
||||
<div class="progress-bar bg-primary" style="width: 75%" role="progressbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted ms-2">75%</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
@components.Card("Total Spent", "currency") {
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="subheader">Total fuel costs</div>
|
||||
</div>
|
||||
<div class="h1 mb-3">{ fmt.Sprintf("%.2f %s", totalCost, user.BaseCurrency) }</div>
|
||||
<div class="d-flex mb-2">
|
||||
<div class="flex-fill">
|
||||
<div class="progress progress-sm">
|
||||
<div class="progress-bar bg-success" style="width: 60%" role="progressbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted ms-2">60%</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
@components.Card("Avg Consumption", "gauge") {
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="subheader">Liters per 100km</div>
|
||||
</div>
|
||||
<div class="h1 mb-3">
|
||||
if avgConsumption > 0 {
|
||||
{ fmt.Sprintf("%.1f L/100km", avgConsumption) }
|
||||
} else {
|
||||
{ "N/A" }
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex mb-2">
|
||||
<div class="flex-fill">
|
||||
<div class="progress progress-sm">
|
||||
<div class="progress-bar bg-warning" style="width: 45%" role="progressbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted ms-2">Efficiency</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
@components.Card("Vehicles", "car") {
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="subheader">Active vehicles</div>
|
||||
</div>
|
||||
<div class="h1 mb-3">{ fmt.Sprintf("%d", len(vehicles)) }</div>
|
||||
<div class="d-flex mb-2">
|
||||
<div class="flex-fill">
|
||||
<div class="progress progress-sm">
|
||||
<div class="progress-bar bg-info" style="width: 100%" role="progressbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted ms-2">100%</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Trip Length and Efficiency Statistics -->
|
||||
<div class="row row-deck row-cards mb-4">
|
||||
<div class="col-sm-6 col-lg-4">
|
||||
@components.Card("Total Distance", "trip") {
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="subheader">Kilometers driven</div>
|
||||
</div>
|
||||
<div class="h1 mb-3">
|
||||
{ fmt.Sprintf("%.0f km", calculateTotalDistance(stops)) }
|
||||
</div>
|
||||
<div class="d-flex mb-2">
|
||||
<div class="flex-fill">
|
||||
<div class="progress progress-sm">
|
||||
<div class="progress-bar bg-info" style="width: 80%" role="progressbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted ms-2">Tracked</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-4">
|
||||
@components.Card("Efficiency Trend", "chart-bar") {
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="subheader">Recent performance</div>
|
||||
</div>
|
||||
<div class="h1 mb-3">
|
||||
if len(stops) >= 3 {
|
||||
if calculateEfficiencyTrend(stops) == "improving" {
|
||||
<span class="text-success">Improving</span>
|
||||
} else if calculateEfficiencyTrend(stops) == "worsening" {
|
||||
<span class="text-danger">Declining</span>
|
||||
} else {
|
||||
<span class="text-info">Stable</span>
|
||||
}
|
||||
} else {
|
||||
<span class="text-muted">N/A</span>
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex mb-2">
|
||||
<div class="flex-fill">
|
||||
<div class="progress progress-sm">
|
||||
if calculateEfficiencyTrend(stops) == "improving" {
|
||||
<div class="progress-bar bg-success" style="width: 75%" role="progressbar"></div>
|
||||
} else if calculateEfficiencyTrend(stops) == "worsening" {
|
||||
<div class="progress-bar bg-danger" style="width: 30%" role="progressbar"></div>
|
||||
} else {
|
||||
<div class="progress-bar bg-info" style="width: 50%" role="progressbar"></div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted ms-2">Trend</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-4">
|
||||
@components.Card("Best Efficiency", "award") {
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="subheader">Lowest consumption</div>
|
||||
</div>
|
||||
<div class="h1 mb-3">
|
||||
{ fmt.Sprintf("%.1f L/100km", getBestEfficiency(stops)) }
|
||||
</div>
|
||||
<div class="d-flex mb-2">
|
||||
<div class="flex-fill">
|
||||
<div class="progress progress-sm">
|
||||
<div class="progress-bar bg-success" style="width: 90%" role="progressbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted ms-2">Personal best</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Last Fill-up Info -->
|
||||
if lastFillUp != nil {
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
@components.Icon("clock", 24)
|
||||
Last Fill-up
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="mb-3">
|
||||
<div class="text-muted">Date</div>
|
||||
<div class="h4">{ lastFillUp.Date.Format("Jan 2, 2006") }</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="mb-3">
|
||||
<div class="text-muted">Amount</div>
|
||||
<div class="h4">{ fmt.Sprintf("%.2f L", lastFillUp.Liters) }</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="mb-3">
|
||||
<div class="text-muted">Cost</div>
|
||||
<div class="h4">{ fmt.Sprintf("%.2f %s", lastFillUp.TotalPrice, user.BaseCurrency) }</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="mb-3">
|
||||
<div class="text-muted">Location</div>
|
||||
<div class="h4">{ lastFillUp.Location }</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!-- Filters and Search -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Filters</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
@components.FormGroup("Vehicle", "") {
|
||||
@components.VehicleSelect("vehicle_id", 0, vehicles, false)
|
||||
}
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
@components.FormGroup("Fuel Type", "") {
|
||||
@components.FuelTypeSelect("fuel_type", "", false)
|
||||
}
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
@components.FormGroup("Date From", "") {
|
||||
@components.DateInput("date_from", "", false)
|
||||
}
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
@components.FormGroup("Date To", "") {
|
||||
@components.DateInput("date_to", "", false)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@components.ButtonGroup() {
|
||||
<button type="button" class="btn btn-primary" onclick="applyFilters()">
|
||||
@components.Icon("search", 24)
|
||||
Apply Filters
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="clearFilters()">
|
||||
@components.Icon("refresh", 24)
|
||||
Clear
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Fuel Stops Table -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
if len(stops) > 0 {
|
||||
@FuelStopsTable(stops, user.BaseCurrency)
|
||||
} else {
|
||||
@components.EmptyState("gas-station", "No fuel stops found", "Start tracking your fuel expenses by adding your first fuel stop.", "Add your first stop", "/add")
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@DashboardScript()
|
||||
}
|
||||
}
|
||||
|
||||
templ FuelStopsTable(stops []models.FuelStop, currency string) {
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Recent Fuel Stops</h3>
|
||||
<div class="card-actions">
|
||||
<a href="/add" class="btn btn-primary">
|
||||
@components.Icon("plus", 24)
|
||||
Add Stop
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter table-mobile-md card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Vehicle</th>
|
||||
<th>Location</th>
|
||||
<th>Fuel Type</th>
|
||||
<th>Amount</th>
|
||||
<th>Price/L</th>
|
||||
<th>Total</th>
|
||||
<th>Trip Length</th>
|
||||
<th>Consumption</th>
|
||||
<th>Odometer</th>
|
||||
<th class="w-1">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
for _, stop := range stops {
|
||||
<tr>
|
||||
<td data-label="Date">
|
||||
<div class="d-flex py-1 align-items-center">
|
||||
<div class="flex-fill">
|
||||
<div class="font-weight-medium">{ stop.Date.Format("Jan 2, 2006") }</div>
|
||||
<div class="text-muted">{ stop.Date.Format("15:04") }</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Vehicle">
|
||||
<div class="d-flex py-1 align-items-center">
|
||||
<div class="flex-fill">
|
||||
<div class="font-weight-medium">{ stop.Vehicle.Name }</div>
|
||||
if stop.Vehicle.LicensePlate != "" {
|
||||
<div class="text-muted">{ stop.Vehicle.LicensePlate }</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Location">
|
||||
<div class="d-flex py-1 align-items-center">
|
||||
@components.Icon("location", 24)
|
||||
<div class="ms-2">{ stop.Location }</div>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Fuel Type">
|
||||
<span class="badge bg-secondary">{ stop.FuelType }</span>
|
||||
</td>
|
||||
<td data-label="Amount">
|
||||
<div class="font-weight-medium">{ fmt.Sprintf("%.2f L", stop.Liters) }</div>
|
||||
</td>
|
||||
<td data-label="Price/L">
|
||||
<div class="font-weight-medium">{ fmt.Sprintf("%.3f %s", stop.PricePerL, currency) }</div>
|
||||
</td>
|
||||
<td data-label="Total">
|
||||
<div class="font-weight-medium text-success">{ fmt.Sprintf("%.2f %s", stop.TotalPrice, currency) }</div>
|
||||
</td>
|
||||
<td data-label="Trip Length">
|
||||
if stop.TripLength > 0 {
|
||||
<div class="font-weight-medium">{ fmt.Sprintf("%.1f km", stop.TripLength) }</div>
|
||||
} else {
|
||||
<div class="text-muted">Not recorded</div>
|
||||
}
|
||||
</td>
|
||||
<td data-label="Consumption">
|
||||
if stop.TripLength > 0 {
|
||||
<div class="font-weight-medium">
|
||||
{ fmt.Sprintf("%.1f L/100km", (stop.Liters/stop.TripLength)*100) }
|
||||
</div>
|
||||
} else {
|
||||
<div class="text-muted">N/A</div>
|
||||
}
|
||||
</td>
|
||||
<td data-label="Odometer">
|
||||
if stop.Odometer > 0 {
|
||||
<div class="font-weight-medium">{ fmt.Sprintf("%d km", stop.Odometer) }</div>
|
||||
} else {
|
||||
<div class="text-muted">Not recorded</div>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@components.ButtonGroup() {
|
||||
@components.EditButton(fmt.Sprintf("/edit/%d", stop.ID))
|
||||
@components.DeleteButton(fmt.Sprintf("/delete/%d", stop.ID), fmt.Sprintf("fuel stop from %s", stop.Date.Format("Jan 2, 2006")))
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
// Helper functions for statistics calculations
|
||||
func calculateTotalDistance(stops []models.FuelStop) float64 {
|
||||
var total float64
|
||||
for _, stop := range stops {
|
||||
if stop.TripLength > 0 {
|
||||
total += stop.TripLength
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func calculateEfficiencyTrend(stops []models.FuelStop) string {
|
||||
if len(stops) < 3 {
|
||||
return "insufficient_data"
|
||||
}
|
||||
|
||||
// Calculate average consumption for recent vs older stops
|
||||
recent := stops[:len(stops)/2]
|
||||
older := stops[len(stops)/2:]
|
||||
|
||||
recentAvg := calculateAverageConsumption(recent)
|
||||
olderAvg := calculateAverageConsumption(older)
|
||||
|
||||
if recentAvg == 0 || olderAvg == 0 {
|
||||
return "insufficient_data"
|
||||
}
|
||||
|
||||
diff := recentAvg - olderAvg
|
||||
if diff < -0.5 {
|
||||
return "improving"
|
||||
} else if diff > 0.5 {
|
||||
return "worsening"
|
||||
}
|
||||
return "stable"
|
||||
}
|
||||
|
||||
func calculateAverageConsumption(stops []models.FuelStop) float64 {
|
||||
var totalConsumption float64
|
||||
var count int
|
||||
|
||||
for _, stop := range stops {
|
||||
if stop.TripLength > 0 {
|
||||
consumption := (stop.Liters / stop.TripLength) * 100
|
||||
if consumption > 0 && consumption < 50 {
|
||||
totalConsumption += consumption
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return 0
|
||||
}
|
||||
return totalConsumption / float64(count)
|
||||
}
|
||||
|
||||
func getBestEfficiency(stops []models.FuelStop) float64 {
|
||||
bestEfficiency := float64(999) // Start with high value
|
||||
|
||||
for _, stop := range stops {
|
||||
if stop.TripLength > 0 {
|
||||
consumption := (stop.Liters / stop.TripLength) * 100
|
||||
if consumption > 0 && consumption < bestEfficiency && consumption < 50 {
|
||||
bestEfficiency = consumption
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bestEfficiency == 999 {
|
||||
return 0
|
||||
}
|
||||
return bestEfficiency
|
||||
}
|
||||
|
||||
script DashboardScript() {
|
||||
function applyFilters() {
|
||||
const vehicleId = document.querySelector('select[name="vehicle_id"]').value;
|
||||
const fuelType = document.querySelector('select[name="fuel_type"]').value;
|
||||
const dateFrom = document.querySelector('input[name="date_from"]').value;
|
||||
const dateTo = document.querySelector('input[name="date_to"]').value;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (vehicleId) params.append('vehicle_id', vehicleId);
|
||||
if (fuelType) params.append('fuel_type', fuelType);
|
||||
if (dateFrom) params.append('date_from', dateFrom);
|
||||
if (dateTo) params.append('date_to', dateTo);
|
||||
|
||||
window.location.href = '/dashboard?' + params.toString();
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
|
||||
function confirmDelete(itemName) {
|
||||
return confirm(`Are you sure you want to delete this ${itemName}? This action cannot be undone.`);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,814 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"tankstopp/internal/currency"
|
||||
"tankstopp/internal/models"
|
||||
"tankstopp/internal/views/components"
|
||||
)
|
||||
|
||||
templ AddFuelStopPage(user *models.User, username string, vehicles []models.Vehicle, currencies []currency.Currency) {
|
||||
@components.BaseLayout("Add Fuel Stop", user, username) {
|
||||
@components.PageHeader("Add Fuel Stop", "Record a new fuel stop")
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-lg-8">
|
||||
<form method="POST" class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Fuel Stop Details</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Date", "") {
|
||||
@components.DateInput("date", "", true)
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Vehicle", "") {
|
||||
@components.VehicleSelect("vehicle_id", 0, vehicles, true)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Station Name", "") {
|
||||
<div class="input-group">
|
||||
@components.Input("station_name", "text", "Enter station name", "", false)
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="findNearbyStations()">
|
||||
@components.Icon("search", 24)
|
||||
Find Nearby
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Location", "") {
|
||||
@components.Input("location", "text", "Enter location", "", false)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Station Search Results Modal -->
|
||||
<div class="modal fade" id="stationSearchModal" tabindex="-1" aria-labelledby="stationSearchModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="stationSearchModalLabel">Nearby Fuel Stations</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="stationSearchResults">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Searching...</span>
|
||||
</div>
|
||||
<p class="mt-2">Finding nearby fuel stations...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-outline-primary" onclick="showManualEntry()">Enter Manually</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Fuel Type", "") {
|
||||
@components.FuelTypeSelect("fuel_type", "", true)
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Amount (Liters)", "") {
|
||||
@components.NumberInput("amount", "0.00", 0.0, "0.01", 0.0, true)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
@components.FormGroup("Price per Liter", "") {
|
||||
<div class="input-group">
|
||||
@components.NumberInput("price_per_liter", "0.000", 0.0, "0.001", 0.0, true)
|
||||
<span class="input-group-text" id="price-currency">{ user.BaseCurrency }</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
@components.FormGroup("Total Cost", "") {
|
||||
<div class="input-group">
|
||||
@components.NumberInput("total_cost", "0.00", 0.0, "0.01", 0.0, true)
|
||||
<span class="input-group-text" id="total-currency">{ user.BaseCurrency }</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
@components.FormGroup("Currency", "") {
|
||||
@components.CurrencySelect("currency", user.BaseCurrency, currencies)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Odometer Reading (km)", "") {
|
||||
@components.NumberInput("odometer", "0", 0.0, "1", 0.0, false)
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Trip Length (km)", "") {
|
||||
@components.NumberInput("trip_length", "0.0", 0.0, "0.1", 0.0, false)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@components.FormGroup("Notes", "") {
|
||||
@components.TextArea("notes", "Optional notes about this fuel stop", "", 3)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<div class="d-flex">
|
||||
<a href="/dashboard" class="btn btn-link">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary ms-auto">
|
||||
@components.Icon("plus", 24)
|
||||
Add Fuel Stop
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@FuelStopScript(vehicles)
|
||||
}
|
||||
}
|
||||
|
||||
templ EditFuelStopPage(user *models.User, username string, stop *models.FuelStop, vehicles []models.Vehicle, currencies []currency.Currency) {
|
||||
@components.BaseLayout("Edit Fuel Stop", user, username) {
|
||||
@components.PageHeader("Edit Fuel Stop", "Update fuel stop details")
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-lg-8">
|
||||
<form method="POST" class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Fuel Stop Details</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Date", "") {
|
||||
@components.DateInput("date", stop.Date.Format("2006-01-02"), true)
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Vehicle", "") {
|
||||
@components.VehicleSelect("vehicle_id", stop.VehicleID, vehicles, true)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Station Name", "") {
|
||||
<div class="input-group">
|
||||
@components.Input("station_name", "text", "Enter station name", stop.StationName, false)
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="findNearbyStations()">
|
||||
@components.Icon("search", 24)
|
||||
Find Nearby
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Location", "") {
|
||||
@components.Input("location", "text", "Enter location", stop.Location, false)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Station Search Results Modal -->
|
||||
<div class="modal fade" id="stationSearchModal" tabindex="-1" aria-labelledby="stationSearchModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="stationSearchModalLabel">Nearby Fuel Stations</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="stationSearchResults">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Searching...</span>
|
||||
</div>
|
||||
<p class="mt-2">Finding nearby fuel stations...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-outline-primary" onclick="showManualEntry()">Enter Manually</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Fuel Type", "") {
|
||||
@components.FuelTypeSelect("fuel_type", stop.FuelType, true)
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Amount (Liters)", "") {
|
||||
@components.NumberInput("amount", "0.00", stop.Liters, "0.01", 0.0, true)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
@components.FormGroup("Price per Liter", "") {
|
||||
<div class="input-group">
|
||||
@components.NumberInput("price_per_liter", "0.000", stop.PricePerL, "0.001", 0.0, true)
|
||||
<span class="input-group-text" id="price-currency">{ stop.Currency }</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
@components.FormGroup("Total Cost", "") {
|
||||
<div class="input-group">
|
||||
@components.NumberInput("total_cost", "0.00", stop.TotalPrice, "0.01", 0.0, true)
|
||||
<span class="input-group-text" id="total-currency">{ stop.Currency }</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
@components.FormGroup("Currency", "") {
|
||||
@components.CurrencySelect("currency", stop.Currency, currencies)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Odometer Reading (km)", "") {
|
||||
@components.NumberInput("odometer", "0", float64(stop.Odometer), "1", 0.0, false)
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Trip Length (km)", "") {
|
||||
@components.NumberInput("trip_length", "0.0", stop.TripLength, "0.1", 0.0, false)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@components.FormGroup("Notes", "") {
|
||||
@components.TextArea("notes", "Optional notes about this fuel stop", stop.Notes, 3)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<div class="d-flex">
|
||||
<a href="/dashboard" class="btn btn-link">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary ms-auto">
|
||||
@components.Icon("check", 24)
|
||||
Update Fuel Stop
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@FuelStopScript(vehicles)
|
||||
}
|
||||
}
|
||||
|
||||
script FuelStopScript(vehicles []models.Vehicle) {
|
||||
// Update currency display when currency dropdown changes
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const currencySelect = document.querySelector('select[name="currency"]');
|
||||
const priceCurrency = document.getElementById('price-currency');
|
||||
const totalCurrency = document.getElementById('total-currency');
|
||||
|
||||
if (currencySelect) {
|
||||
currencySelect.addEventListener('change', function() {
|
||||
const selectedCurrency = this.value;
|
||||
if (priceCurrency) priceCurrency.textContent = selectedCurrency;
|
||||
if (totalCurrency) totalCurrency.textContent = selectedCurrency;
|
||||
});
|
||||
}
|
||||
|
||||
// Update fuel type when vehicle is selected
|
||||
const vehicleSelect = document.querySelector('select[name="vehicle_id"]');
|
||||
const fuelTypeSelect = document.querySelector('select[name="fuel_type"]');
|
||||
|
||||
if (vehicleSelect && fuelTypeSelect) {
|
||||
vehicleSelect.addEventListener('change', async function() {
|
||||
const selectedVehicleId = this.value;
|
||||
if (selectedVehicleId) {
|
||||
try {
|
||||
// Fetch vehicle information from API
|
||||
const response = await fetch(`/api/vehicles/${selectedVehicleId}`);
|
||||
if (response.ok) {
|
||||
const vehicle = await response.json();
|
||||
if (vehicle.fuel_type) {
|
||||
fuelTypeSelect.value = vehicle.fuel_type;
|
||||
}
|
||||
} else {
|
||||
console.warn('Failed to fetch vehicle information');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching vehicle information:', error);
|
||||
}
|
||||
} else {
|
||||
// Reset fuel type when no vehicle is selected
|
||||
fuelTypeSelect.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-calculate total cost when amount or price per liter changes
|
||||
const amountInput = document.querySelector('input[name="amount"]');
|
||||
const priceInput = document.querySelector('input[name="price_per_liter"]');
|
||||
const totalInput = document.querySelector('input[name="total_cost"]');
|
||||
|
||||
function calculateTotal() {
|
||||
if (amountInput && priceInput && totalInput) {
|
||||
const amount = parseFloat(amountInput.value) || 0;
|
||||
const price = parseFloat(priceInput.value) || 0;
|
||||
const total = amount * price;
|
||||
totalInput.value = total.toFixed(2);
|
||||
}
|
||||
}
|
||||
|
||||
if (amountInput) {
|
||||
amountInput.addEventListener('input', calculateTotal);
|
||||
}
|
||||
|
||||
if (priceInput) {
|
||||
priceInput.addEventListener('input', calculateTotal);
|
||||
}
|
||||
|
||||
// Also calculate total when total is changed (reverse calculation)
|
||||
if (totalInput && amountInput) {
|
||||
totalInput.addEventListener('input', function() {
|
||||
const total = parseFloat(this.value) || 0;
|
||||
const amount = parseFloat(amountInput.value) || 0;
|
||||
if (amount > 0) {
|
||||
const pricePerLiter = total / amount;
|
||||
if (priceInput) {
|
||||
priceInput.value = pricePerLiter.toFixed(3);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Check if page is served over HTTPS
|
||||
function isSecureContext() {
|
||||
return location.protocol === 'https:' || location.hostname === 'localhost' || location.hostname === '127.0.0.1';
|
||||
}
|
||||
|
||||
// Debug function for location issues
|
||||
function debugLocationInfo() {
|
||||
console.log('=== Location Debug Info ===');
|
||||
console.log('Protocol:', location.protocol);
|
||||
console.log('Hostname:', location.hostname);
|
||||
console.log('Is secure context:', isSecureContext());
|
||||
console.log('Geolocation supported:', !!navigator.geolocation);
|
||||
console.log('Permissions API supported:', !!navigator.permissions);
|
||||
|
||||
if (navigator.permissions) {
|
||||
navigator.permissions.query({name: 'geolocation'}).then(function(result) {
|
||||
console.log('Geolocation permission:', result.state);
|
||||
}).catch(function(error) {
|
||||
console.log('Permission query error:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fuel Station Search Functions
|
||||
window.findNearbyStations = function() {
|
||||
// Debug location setup
|
||||
debugLocationInfo();
|
||||
|
||||
// Check if we're in a secure context
|
||||
if (!isSecureContext()) {
|
||||
showStationSearchError('Geolocation requires HTTPS. Please access this page via HTTPS or use manual entry.');
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = new bootstrap.Modal(document.getElementById('stationSearchModal'));
|
||||
modal.show();
|
||||
|
||||
// Reset modal content
|
||||
document.getElementById('stationSearchResults').innerHTML = `
|
||||
<div class="text-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Searching...</span>
|
||||
</div>
|
||||
<p class="mt-2">Finding nearby fuel stations...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Get user's location with improved error handling
|
||||
if (navigator.geolocation) {
|
||||
console.log('Starting geolocation request...');
|
||||
// Update status to show we're requesting location
|
||||
document.getElementById('stationSearchResults').innerHTML = `
|
||||
<div class="text-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Getting location...</span>
|
||||
</div>
|
||||
<p class="mt-2">Requesting your location...</p>
|
||||
<small class="text-muted">Please allow location access when prompted</small>
|
||||
</div>
|
||||
`;
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
function(position) {
|
||||
const lat = position.coords.latitude;
|
||||
const lon = position.coords.longitude;
|
||||
console.log('Location obtained:', lat, lon);
|
||||
console.log('Accuracy:', position.coords.accuracy + 'm');
|
||||
console.log('Timestamp:', new Date(position.timestamp));
|
||||
searchNearbyStations(lat, lon);
|
||||
},
|
||||
function(error) {
|
||||
console.error('Geolocation error:', error);
|
||||
console.error('Error code:', error.code);
|
||||
console.error('Error message:', error.message);
|
||||
|
||||
let errorMessage = 'Unable to get your location. ';
|
||||
let showRetryOption = false;
|
||||
|
||||
switch(error.code) {
|
||||
case error.PERMISSION_DENIED:
|
||||
errorMessage += 'Location access was denied. Please enable location services, refresh the page, and try again.';
|
||||
if (!isSecureContext()) {
|
||||
errorMessage += ' Note: This page requires HTTPS for location access.';
|
||||
}
|
||||
break;
|
||||
case error.POSITION_UNAVAILABLE:
|
||||
errorMessage += 'Location information is unavailable. Please check your GPS settings or try again.';
|
||||
showRetryOption = true;
|
||||
break;
|
||||
case error.TIMEOUT:
|
||||
errorMessage += 'Location request timed out. Trying with lower accuracy...';
|
||||
// Try again with lower accuracy
|
||||
tryLowAccuracyLocation();
|
||||
return;
|
||||
default:
|
||||
errorMessage += 'An unknown error occurred while retrieving location.';
|
||||
showRetryOption = true;
|
||||
break;
|
||||
}
|
||||
|
||||
showStationSearchError(errorMessage, showRetryOption);
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
timeout: 15000, // Increased timeout to 15 seconds
|
||||
maximumAge: 300000 // 5 minutes
|
||||
}
|
||||
);
|
||||
} else {
|
||||
showStationSearchError('Geolocation is not supported by this browser. Please enter station details manually.');
|
||||
}
|
||||
};
|
||||
|
||||
// Fallback function for low accuracy location
|
||||
function tryLowAccuracyLocation() {
|
||||
document.getElementById('stationSearchResults').innerHTML = `
|
||||
<div class="text-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Trying low accuracy...</span>
|
||||
</div>
|
||||
<p class="mt-2">Trying with lower accuracy...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
function(position) {
|
||||
const lat = position.coords.latitude;
|
||||
const lon = position.coords.longitude;
|
||||
console.log('Low accuracy location obtained:', lat, lon);
|
||||
searchNearbyStations(lat, lon);
|
||||
},
|
||||
function(error) {
|
||||
console.error('Low accuracy geolocation error:', error);
|
||||
showStationSearchError('Unable to get your location even with low accuracy. Please enter station details manually or try again later.');
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: false, // Use less accurate but faster location
|
||||
timeout: 30000, // Longer timeout for low accuracy
|
||||
maximumAge: 600000 // 10 minutes cache for low accuracy
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function searchNearbyStations(lat, lon) {
|
||||
console.log('Searching for stations near:', lat, lon);
|
||||
|
||||
// Update status to show we're searching
|
||||
document.getElementById('stationSearchResults').innerHTML = `
|
||||
<div class="text-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Searching...</span>
|
||||
</div>
|
||||
<p class="mt-2">Searching for nearby fuel stations...</p>
|
||||
<small class="text-muted">This may take a few seconds</small>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const overpassUrl = 'https://overpass-api.de/api/interpreter';
|
||||
const query = `
|
||||
[out:json][timeout:30];
|
||||
(
|
||||
node["amenity"="fuel"](around:5000,${lat},${lon});
|
||||
way["amenity"="fuel"](around:5000,${lat},${lon});
|
||||
relation["amenity"="fuel"](around:5000,${lat},${lon});
|
||||
);
|
||||
out center meta;
|
||||
`;
|
||||
|
||||
console.log('Overpass query:', query);
|
||||
|
||||
// Add timeout to the fetch request
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
|
||||
|
||||
fetch(overpassUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: 'data=' + encodeURIComponent(query),
|
||||
signal: controller.signal
|
||||
})
|
||||
.then(response => {
|
||||
clearTimeout(timeoutId);
|
||||
console.log('API response status:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('API response data:', data);
|
||||
|
||||
if (data.remark && data.remark.includes('timeout')) {
|
||||
throw new Error('API request timed out');
|
||||
}
|
||||
|
||||
if (data.elements && data.elements.length > 0) {
|
||||
console.log(`Found ${data.elements.length} stations`);
|
||||
displayStationResults(data.elements, lat, lon);
|
||||
} else {
|
||||
console.log('No stations found in API response');
|
||||
showStationSearchError('No fuel stations found within 5km of your location. Try searching manually or check a different area.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
clearTimeout(timeoutId);
|
||||
console.error('Error searching for stations:', error);
|
||||
|
||||
let errorMessage = 'Error searching for fuel stations. ';
|
||||
|
||||
if (error.name === 'AbortError') {
|
||||
errorMessage += 'The search timed out. Please try again or enter station details manually.';
|
||||
} else if (error.message.includes('HTTP error')) {
|
||||
errorMessage += 'The map service is temporarily unavailable. Please try again later.';
|
||||
} else if (error.message.includes('Failed to fetch')) {
|
||||
errorMessage += 'Network error. Please check your internet connection and try again.';
|
||||
} else {
|
||||
errorMessage += 'Please try again or enter station details manually.';
|
||||
}
|
||||
|
||||
showStationSearchError(errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
function displayStationResults(stations, userLat, userLon) {
|
||||
// Calculate distances and sort by distance
|
||||
const stationsWithDistance = stations.map(station => {
|
||||
const stationLat = station.lat || (station.center && station.center.lat);
|
||||
const stationLon = station.lon || (station.center && station.center.lon);
|
||||
|
||||
if (stationLat && stationLon) {
|
||||
const distance = calculateDistance(userLat, userLon, stationLat, stationLon);
|
||||
return {
|
||||
...station,
|
||||
distance: distance,
|
||||
displayLat: stationLat,
|
||||
displayLon: stationLon
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}).filter(station => station !== null);
|
||||
|
||||
stationsWithDistance.sort((a, b) => a.distance - b.distance);
|
||||
|
||||
const resultsHTML = stationsWithDistance.map(station => {
|
||||
const name = station.tags.name || station.tags.brand || 'Unknown Station';
|
||||
const address = [
|
||||
station.tags['addr:street'],
|
||||
station.tags['addr:housenumber'],
|
||||
station.tags['addr:city'],
|
||||
station.tags['addr:postcode']
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
const brand = station.tags.brand || '';
|
||||
const operator = station.tags.operator || '';
|
||||
const displayName = brand || operator || name;
|
||||
|
||||
return `
|
||||
<div class="card mb-2 station-result" style="cursor: pointer;"
|
||||
onclick="selectStation('${displayName}', '${address}', ${station.displayLat}, ${station.displayLon})">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h6 class="card-title mb-1">${displayName}</h6>
|
||||
<p class="card-text text-muted mb-0">${address}</p>
|
||||
${brand && brand !== displayName ? `<small class="text-muted">${brand}</small>` : ''}
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span class="badge bg-primary">${station.distance.toFixed(1)} km</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
document.getElementById('stationSearchResults').innerHTML = resultsHTML ||
|
||||
'<div class="text-center text-muted">No fuel stations found nearby.</div>';
|
||||
}
|
||||
|
||||
function calculateDistance(lat1, lon1, lat2, lon2) {
|
||||
const R = 6371; // Earth's radius in km
|
||||
const dLat = (lat2 - lat1) * Math.PI / 180;
|
||||
const dLon = (lon2 - lon1) * Math.PI / 180;
|
||||
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
return R * c;
|
||||
}
|
||||
|
||||
function showStationSearchError(message, showRetryOption = false) {
|
||||
const retryButton = showRetryOption ? `
|
||||
<button type="button" class="btn btn-outline-secondary me-2" onclick="findNearbyStations()">
|
||||
Try Again
|
||||
</button>
|
||||
` : '';
|
||||
|
||||
document.getElementById('stationSearchResults').innerHTML = `
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<i class="fas fa-exclamation-triangle"></i> ${message}
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
${retryButton}
|
||||
<button type="button" class="btn btn-outline-primary" onclick="showManualEntry()">
|
||||
Enter Station Details Manually
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
window.showManualEntry = function() {
|
||||
document.getElementById('stationSearchResults').innerHTML = `
|
||||
<div class="alert alert-info" role="alert">
|
||||
<h6>Manual Entry</h6>
|
||||
<p>Enter the station details manually:</p>
|
||||
</div>
|
||||
<form onsubmit="return selectManualStation(event)">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Station Name</label>
|
||||
<input type="text" class="form-control" id="manual-station-name" placeholder="e.g., Shell, TOTAL, Aral" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Brand (optional)</label>
|
||||
<select class="form-select" id="manual-station-brand">
|
||||
<option value="">Select brand...</option>
|
||||
<option value="Shell">Shell</option>
|
||||
<option value="TOTAL">TOTAL</option>
|
||||
<option value="Aral">Aral</option>
|
||||
<option value="Esso">Esso</option>
|
||||
<option value="BP">BP</option>
|
||||
<option value="AGIP">AGIP</option>
|
||||
<option value="OMV">OMV</option>
|
||||
<option value="JET">JET</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Address</label>
|
||||
<input type="text" class="form-control" id="manual-station-address" placeholder="Street, City, Country" required>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<button type="button" class="btn btn-secondary" onclick="findNearbyStations()">
|
||||
Back to Search
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Use This Station
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
};
|
||||
|
||||
window.selectManualStation = function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const stationName = document.getElementById('manual-station-name').value;
|
||||
const stationBrand = document.getElementById('manual-station-brand').value;
|
||||
const stationAddress = document.getElementById('manual-station-address').value;
|
||||
|
||||
// Use brand name if provided, otherwise use the entered name
|
||||
const finalName = stationBrand && stationBrand !== 'Other' ? stationBrand : stationName;
|
||||
|
||||
// Fill form fields
|
||||
document.querySelector('input[name="station_name"]').value = finalName;
|
||||
document.querySelector('input[name="location"]').value = stationAddress;
|
||||
|
||||
// Close modal
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('stationSearchModal'));
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
|
||||
// Show success message
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast align-items-center text-white bg-success border-0';
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.setAttribute('aria-live', 'assertive');
|
||||
toast.setAttribute('aria-atomic', 'true');
|
||||
toast.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
Station entered manually: ${finalName}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
const bsToast = new bootstrap.Toast(toast);
|
||||
bsToast.show();
|
||||
|
||||
// Remove toast after it hides
|
||||
toast.addEventListener('hidden.bs.toast', function() {
|
||||
document.body.removeChild(toast);
|
||||
});
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
window.selectStation = function(name, address, lat, lon) {
|
||||
// Fill form fields
|
||||
document.querySelector('input[name="station_name"]').value = name;
|
||||
document.querySelector('input[name="location"]').value = address;
|
||||
|
||||
// Close modal
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('stationSearchModal'));
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
|
||||
// Show success message
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast align-items-center text-white bg-success border-0';
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.setAttribute('aria-live', 'assertive');
|
||||
toast.setAttribute('aria-atomic', 'true');
|
||||
toast.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
Station selected: ${name}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
const bsToast = new bootstrap.Toast(toast);
|
||||
bsToast.show();
|
||||
|
||||
// Remove toast after it hides
|
||||
toast.addEventListener('hidden.bs.toast', function() {
|
||||
document.body.removeChild(toast);
|
||||
});
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,360 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"tankstopp/internal/currency"
|
||||
"tankstopp/internal/models"
|
||||
"tankstopp/internal/views/components"
|
||||
)
|
||||
|
||||
templ SettingsPage(user *models.User, username string, currencies []currency.Currency, successMessage, errorMessage string) {
|
||||
@components.BaseLayout("Settings", user, username) {
|
||||
@components.PageHeader("Manage your account", "Settings")
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
if successMessage != "" {
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
@components.Alert("success", successMessage)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
if errorMessage != "" {
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
@components.Alert("danger", errorMessage)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-8">
|
||||
<!-- Profile Settings -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
@components.Card("Profile Settings", "user") {
|
||||
@components.Form("post", "/settings/profile") {
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Username", "Your unique username") {
|
||||
@components.Input("username", "text", "Enter username", username, true)
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Email", "Your email address") {
|
||||
@components.Input("email", "email", "Enter email", user.Email, true)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Base Currency", "Default currency for fuel prices") {
|
||||
@components.CurrencySelect("base_currency", user.BaseCurrency, currencies)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@components.ButtonGroup() {
|
||||
@components.PrimaryButton("Save Profile", "save")
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Application Preferences -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
@components.Card("Application Preferences", "settings") {
|
||||
@components.Form("post", "/settings/preferences") {
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Distance Unit", "Unit for distance measurements") {
|
||||
@components.Select("distance_unit", false) {
|
||||
@components.Option("km", "Kilometers (km)", true)
|
||||
@components.Option("mi", "Miles (mi)", false)
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Volume Unit", "Unit for fuel volume measurements") {
|
||||
@components.Select("volume_unit", false) {
|
||||
@components.Option("L", "Liters (L)", true)
|
||||
@components.Option("gal", "Gallons (gal)", false)
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Date Format", "How dates are displayed") {
|
||||
@components.Select("date_format", false) {
|
||||
@components.Option("DD/MM/YYYY", "DD/MM/YYYY", false)
|
||||
@components.Option("MM/DD/YYYY", "MM/DD/YYYY", false)
|
||||
@components.Option("YYYY-MM-DD", "YYYY-MM-DD", true)
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Language", "Application language") {
|
||||
@components.Select("language", false) {
|
||||
@components.Option("en", "English", true)
|
||||
@components.Option("de", "Deutsch", false)
|
||||
@components.Option("fr", "Français", false)
|
||||
@components.Option("es", "Español", false)
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@components.FormGroup("Notifications", "Email notification preferences") {
|
||||
@components.Switch("email_notifications", "Send email notifications", false)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@components.ButtonGroup() {
|
||||
@components.PrimaryButton("Save Preferences", "save")
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Security Settings -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
@components.Card("Security Settings", "lock") {
|
||||
@components.Form("post", "/settings/password") {
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Current Password", "Enter your current password") {
|
||||
@components.PasswordInput("current_password", "Current password", true)
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("New Password", "Choose a new password") {
|
||||
@components.PasswordInput("new_password", "New password", true)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Confirm New Password", "Confirm your new password") {
|
||||
@components.PasswordInput("confirm_password", "Confirm new password", true)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@components.ButtonGroup() {
|
||||
@components.PrimaryButton("Change Password", "lock")
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Data Management -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
@components.Card("Data Management", "database") {
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>Export Data</h5>
|
||||
<p class="text-muted">Download your fuel stop data in various formats</p>
|
||||
@components.ButtonGroup() {
|
||||
<a href="/export/csv" class="btn btn-outline-primary">
|
||||
@components.Icon("download", 24)
|
||||
Export CSV
|
||||
</a>
|
||||
<a href="/export/json" class="btn btn-outline-primary">
|
||||
@components.Icon("download", 24)
|
||||
Export JSON
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h5>Import Data</h5>
|
||||
<p class="text-muted">Import fuel stop data from a CSV file</p>
|
||||
@components.Form("post", "/import/csv") {
|
||||
<div class="mb-3">
|
||||
<input type="file" class="form-control" name="csv_file" accept=".csv" required/>
|
||||
</div>
|
||||
@components.PrimaryButton("Import CSV", "upload")
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-4"/>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h5 class="text-danger">Danger Zone</h5>
|
||||
<p class="text-muted">These actions cannot be undone</p>
|
||||
@components.ButtonGroup() {
|
||||
<button type="button" class="btn btn-outline-danger" onclick="confirmClearData()">
|
||||
@components.Icon("trash", 24)
|
||||
Clear All Data
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Account Management -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
@components.Card("Account Management", "user") {
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h5 class="text-danger">Delete Account</h5>
|
||||
<p class="text-muted">
|
||||
Permanently delete your account and all associated data. This action cannot be undone.
|
||||
</p>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<div class="d-flex">
|
||||
<div>
|
||||
@components.Icon("alert-triangle", 24)
|
||||
</div>
|
||||
<div>
|
||||
<strong>Warning:</strong> This will permanently delete your account, all vehicles, and all fuel stop records.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-danger" onclick="confirmDeleteAccount()">
|
||||
@components.Icon("trash", 24)
|
||||
Delete Account
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Sidebar with Quick Stats -->
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@components.Card("Account Summary", "info-circle") {
|
||||
<div class="list-group list-group-flush">
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Member since</span>
|
||||
<span class="text-muted">{ user.CreatedAt.Format("Jan 2006") }</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Email</span>
|
||||
<span class="text-muted">{ user.Email }</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Base currency</span>
|
||||
<span class="text-muted">{ user.BaseCurrency }</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Account status</span>
|
||||
<span class="text-muted">
|
||||
<span class="badge bg-success">Active</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
@components.Card("Quick Actions", "zap") {
|
||||
<div class="list-group list-group-flush">
|
||||
<a href="/add" class="list-group-item list-group-item-action d-flex align-items-center">
|
||||
<span class="me-2">
|
||||
@components.Icon("plus", 24)
|
||||
</span>
|
||||
Add Fuel Stop
|
||||
</a>
|
||||
<a href="/vehicles" class="list-group-item list-group-item-action d-flex align-items-center">
|
||||
<span class="me-2">
|
||||
@components.Icon("car", 24)
|
||||
</span>
|
||||
Manage Vehicles
|
||||
</a>
|
||||
<a href="/dashboard" class="list-group-item list-group-item-action d-flex align-items-center">
|
||||
<span class="me-2">
|
||||
@components.Icon("home", 24)
|
||||
</span>
|
||||
Go to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@SettingsScript()
|
||||
}
|
||||
}
|
||||
|
||||
script SettingsScript() {
|
||||
function confirmClearData() {
|
||||
if (confirm('Are you sure you want to clear all your data? This will delete all fuel stops and vehicles permanently.')) {
|
||||
if (confirm('This action cannot be undone. Are you absolutely sure?')) {
|
||||
// Create a form to submit the clear data request
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/settings/clear-data';
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDeleteAccount() {
|
||||
if (confirm('Are you sure you want to delete your account? This will permanently delete all your data.')) {
|
||||
if (confirm('This action cannot be undone. Type "DELETE" to confirm:')) {
|
||||
const confirmation = prompt('Please type "DELETE" to confirm account deletion:');
|
||||
if (confirmation === 'DELETE') {
|
||||
// Create a form to submit the delete account request
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/settings/delete-account';
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
} else {
|
||||
alert('Account deletion cancelled. The confirmation text did not match.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize settings page
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize tooltips
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Handle file input styling
|
||||
const fileInputs = document.querySelectorAll('input[type="file"]');
|
||||
fileInputs.forEach(function(input) {
|
||||
input.addEventListener('change', function(e) {
|
||||
const fileName = e.target.files[0] ? e.target.files[0].name : 'No file chosen';
|
||||
const label = e.target.parentNode.querySelector('.file-label');
|
||||
if (label) {
|
||||
label.textContent = fileName;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,379 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"tankstopp/internal/models"
|
||||
"tankstopp/internal/views/components"
|
||||
)
|
||||
|
||||
templ VehiclesPage(user *models.User, username string, vehicles []models.Vehicle) {
|
||||
@components.BaseLayout("Vehicles", user, username) {
|
||||
@components.PageHeader("Manage your vehicles", "Vehicles")
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<!-- Quick Actions -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h3 class="mb-0">Your Vehicles</h3>
|
||||
<p class="text-muted">Manage and track your vehicles</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/vehicles/add" class="btn btn-primary">
|
||||
@components.Icon("plus", 24)
|
||||
Add Vehicle
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Vehicles Grid -->
|
||||
<div class="row">
|
||||
if len(vehicles) > 0 {
|
||||
for _, vehicle := range vehicles {
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
@VehicleCard(vehicle)
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
<div class="col-12">
|
||||
@components.EmptyState("car", "No vehicles found", "Add your first vehicle to start tracking fuel expenses.", "Add Vehicle", "/vehicles/add")
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<!-- Statistics Cards -->
|
||||
if len(vehicles) > 0 {
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<h4 class="mb-3">Vehicle Statistics</h4>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
@components.Card("Total Vehicles", "car") {
|
||||
<div class="h2 mb-2">{ fmt.Sprintf("%d", len(vehicles)) }</div>
|
||||
<div class="text-muted">Registered vehicles</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
@components.Card("Active Vehicles", "status") {
|
||||
<div class="h2 mb-2">{ fmt.Sprintf("%d", countActiveVehicles(vehicles)) }</div>
|
||||
<div class="text-muted">Currently active</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
@components.Card("Brands", "brand") {
|
||||
<div class="h2 mb-2">{ fmt.Sprintf("%d", countUniqueBrands(vehicles)) }</div>
|
||||
<div class="text-muted">Different brands</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
@components.Card("Fuel Types", "fuel") {
|
||||
<div class="h2 mb-2">{ fmt.Sprintf("%d", countUniqueFuelTypes(vehicles)) }</div>
|
||||
<div class="text-muted">Different fuel types</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ VehicleCard(vehicle models.Vehicle) {
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<div class="flex-fill">
|
||||
<h4 class="card-title mb-1">{ vehicle.Name }</h4>
|
||||
<div class="text-muted small">
|
||||
{ vehicle.Make } { vehicle.Model }
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-ghost-secondary" type="button" data-bs-toggle="dropdown">
|
||||
@components.Icon("dots-vertical", 24)
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<a class="dropdown-item" href={ templ.SafeURL(fmt.Sprintf("/vehicles/edit/%d", vehicle.ID)) }>
|
||||
@components.Icon("edit", 24)
|
||||
Edit
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<form method="POST" action={ fmt.Sprintf("/vehicles/delete/%d", vehicle.ID) } style="display: inline;" onsubmit="return confirmDelete(this)" data-item={ vehicle.Name }>
|
||||
<button type="submit" class="dropdown-item text-danger">
|
||||
@components.Icon("trash", 24)
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
@components.Icon("license-plate", 24)
|
||||
<span class="ms-2 small">
|
||||
if vehicle.LicensePlate != "" {
|
||||
{ vehicle.LicensePlate }
|
||||
} else {
|
||||
<span class="text-muted">No plate</span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
@components.Icon("fuel", 24)
|
||||
<span class="ms-2 small">{ vehicle.FuelType }</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
@components.Icon("calendar", 24)
|
||||
<span class="ms-2 small">{ fmt.Sprintf("%d", vehicle.Year) }</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
if vehicle.IsActive {
|
||||
<span class="badge bg-success">Active</span>
|
||||
} else {
|
||||
<span class="badge bg-secondary">Inactive</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
if vehicle.Notes != "" {
|
||||
<div class="mt-3 pt-3 border-top">
|
||||
<small class="text-muted">{ vehicle.Notes }</small>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ AddVehiclePage(user *models.User, username string) {
|
||||
@components.BaseLayout("Add Vehicle", user, username) {
|
||||
@components.PageHeader("Add a new vehicle", "Add Vehicle")
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-lg-8">
|
||||
@components.Card("Vehicle Information", "car") {
|
||||
@components.Form("post", "/vehicles/add") {
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Vehicle Name", "A friendly name for your vehicle") {
|
||||
@components.Input("name", "text", "e.g., My Car, Work Van", "", true)
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("License Plate", "Vehicle registration number") {
|
||||
@components.Input("license_plate", "text", "e.g., ABC-123", "", false)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Make", "Vehicle manufacturer") {
|
||||
@VehicleBrandSelect("make", "")
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Model", "Vehicle model") {
|
||||
@components.Input("model", "text", "e.g., Corolla, Golf", "", true)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Year", "Manufacturing year") {
|
||||
@components.NumberInput("year", "2024", 0, "1", 1900, true)
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Fuel Type", "Primary fuel type") {
|
||||
@components.FuelTypeSelect("fuel_type", "", true)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Active", "Vehicle status") {
|
||||
@components.Switch("is_active", "Vehicle is active", true)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@components.FormGroup("Notes", "Additional information (optional)") {
|
||||
@components.TextArea("notes", "Add any additional notes about this vehicle...", "", 3)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@components.FormButtons("/vehicles", "Add Vehicle", "save")
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ EditVehiclePage(user *models.User, username string, vehicle *models.Vehicle) {
|
||||
@components.BaseLayout("Edit Vehicle", user, username) {
|
||||
@components.PageHeader("Update vehicle information", "Edit Vehicle")
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-lg-8">
|
||||
@components.Card("Vehicle Information", "car") {
|
||||
@components.Form("post", fmt.Sprintf("/vehicles/edit/%d", vehicle.ID)) {
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Vehicle Name", "A friendly name for your vehicle") {
|
||||
@components.Input("name", "text", "e.g., My Car, Work Van", vehicle.Name, true)
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("License Plate", "Vehicle registration number") {
|
||||
@components.Input("license_plate", "text", "e.g., ABC-123", vehicle.LicensePlate, false)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Make", "Vehicle manufacturer") {
|
||||
@VehicleBrandSelect("make", vehicle.Make)
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Model", "Vehicle model") {
|
||||
@components.Input("model", "text", "e.g., Corolla, Golf", vehicle.Model, true)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Year", "Manufacturing year") {
|
||||
@components.NumberInput("year", "2024", float64(vehicle.Year), "1", 1900, true)
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Fuel Type", "Primary fuel type") {
|
||||
@components.FuelTypeSelect("fuel_type", vehicle.FuelType, true)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@components.FormGroup("Active", "Vehicle status") {
|
||||
@components.Switch("is_active", "Vehicle is active", vehicle.IsActive)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
@components.FormGroup("Notes", "Additional information (optional)") {
|
||||
@components.TextArea("notes", "Add any additional notes about this vehicle...", vehicle.Notes, 3)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@components.FormButtons("/vehicles", "Update Vehicle", "save")
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ VehicleBrandSelect(name, selectedMake string) {
|
||||
@components.Select(name, true) {
|
||||
@components.Option("", "Select make...", selectedMake == "")
|
||||
@components.Option("Audi", "Audi", selectedMake == "Audi")
|
||||
@components.Option("BMW", "BMW", selectedMake == "BMW")
|
||||
@components.Option("Mercedes-Benz", "Mercedes-Benz", selectedMake == "Mercedes-Benz")
|
||||
@components.Option("Volkswagen", "Volkswagen", selectedMake == "Volkswagen")
|
||||
@components.Option("Ford", "Ford", selectedMake == "Ford")
|
||||
@components.Option("Toyota", "Toyota", selectedMake == "Toyota")
|
||||
@components.Option("Honda", "Honda", selectedMake == "Honda")
|
||||
@components.Option("Nissan", "Nissan", selectedMake == "Nissan")
|
||||
@components.Option("Hyundai", "Hyundai", selectedMake == "Hyundai")
|
||||
@components.Option("Kia", "Kia", selectedMake == "Kia")
|
||||
@components.Option("Mazda", "Mazda", selectedMake == "Mazda")
|
||||
@components.Option("Subaru", "Subaru", selectedMake == "Subaru")
|
||||
@components.Option("Volvo", "Volvo", selectedMake == "Volvo")
|
||||
@components.Option("Peugeot", "Peugeot", selectedMake == "Peugeot")
|
||||
@components.Option("Renault", "Renault", selectedMake == "Renault")
|
||||
@components.Option("Citroen", "Citroen", selectedMake == "Citroen")
|
||||
@components.Option("Fiat", "Fiat", selectedMake == "Fiat")
|
||||
@components.Option("Opel", "Opel", selectedMake == "Opel")
|
||||
@components.Option("Skoda", "Skoda", selectedMake == "Skoda")
|
||||
@components.Option("SEAT", "SEAT", selectedMake == "SEAT")
|
||||
@components.Option("Chevrolet", "Chevrolet", selectedMake == "Chevrolet")
|
||||
@components.Option("Jeep", "Jeep", selectedMake == "Jeep")
|
||||
@components.Option("Land Rover", "Land Rover", selectedMake == "Land Rover")
|
||||
@components.Option("Jaguar", "Jaguar", selectedMake == "Jaguar")
|
||||
@components.Option("Porsche", "Porsche", selectedMake == "Porsche")
|
||||
@components.Option("Tesla", "Tesla", selectedMake == "Tesla")
|
||||
@components.Option("Other", "Other", selectedMake == "Other")
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for statistics
|
||||
func countActiveVehicles(vehicles []models.Vehicle) int {
|
||||
count := 0
|
||||
for _, vehicle := range vehicles {
|
||||
if vehicle.IsActive {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func countUniqueBrands(vehicles []models.Vehicle) int {
|
||||
brands := make(map[string]bool)
|
||||
for _, vehicle := range vehicles {
|
||||
if vehicle.Make != "" {
|
||||
brands[vehicle.Make] = true
|
||||
}
|
||||
}
|
||||
return len(brands)
|
||||
}
|
||||
|
||||
func countUniqueFuelTypes(vehicles []models.Vehicle) int {
|
||||
fuelTypes := make(map[string]bool)
|
||||
for _, vehicle := range vehicles {
|
||||
if vehicle.FuelType != "" {
|
||||
fuelTypes[vehicle.FuelType] = true
|
||||
}
|
||||
}
|
||||
return len(fuelTypes)
|
||||
}
|
||||
|
||||
script VehicleScript() {
|
||||
function confirmDelete(form) {
|
||||
const vehicleName = form.dataset.item;
|
||||
return confirm(`Are you sure you want to delete the vehicle "${vehicleName}"? This action cannot be undone and will also delete all associated fuel stops.`);
|
||||
}
|
||||
|
||||
// Initialize tooltips and dropdowns
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize Bootstrap tooltips
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Set current year as default
|
||||
const yearInput = document.querySelector('input[name="year"]');
|
||||
if (yearInput && !yearInput.value) {
|
||||
yearInput.value = new Date().getFullYear();
|
||||
}
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user