2 Commits

Author SHA1 Message Date
854573eb3a allow finalizing orders 2025-03-25 20:36:28 +01:00
30b32a571c update readme 2025-03-25 18:42:49 +01:00
7 changed files with 375 additions and 88 deletions

View File

@@ -7,3 +7,18 @@ The zines will then be printed on demand and be send.
for payment a simple random string will be created to connect payments to orders. for payment a simple random string will be created to connect payments to orders.
This way also cash could be send like mullvad is doing it. This way also cash could be send like mullvad is doing it.
# Development
To get the webserver running do the following:
```bash
nix develop .#
# run the webserver
go run main.go
```
For updating tailwindcss on the fly open extra shell
```bash
nix develop .#
tailwindcss -i static/input.css -o static/output.css --watch
```

View File

@@ -34,14 +34,6 @@ func NewCartItemController() CartItemController {
return &cartItemController{} return &cartItemController{}
} }
func GetShippingMethods() []models.Shipping {
return []models.Shipping{
{Id: "germany", Name: "Germany (DHL)", Price: 3.99},
{Id: "international", Name: "International (DHL)", Price: 5.99},
{Id: "pickup", Name: "Pickup", Price: 0.00},
}
}
func generateSessionId(length int) string { func generateSessionId(length int) string {
bytes := make([]byte, length) // 16 bytes = 128 bits bytes := make([]byte, length) // 16 bytes = 128 bits
_, err := rand.Read(bytes) _, err := rand.Read(bytes)
@@ -63,7 +55,7 @@ func GetSessionId(ctx *gin.Context) string {
} }
func GenerateToken() string { func GenerateToken() string {
return generateSessionId(8) return generateSessionId(16)
} }
func (rc *cartItemController) NewCartItemFromForm(ctx *gin.Context) (models.CartItem, error) { func (rc *cartItemController) NewCartItemFromForm(ctx *gin.Context) (models.CartItem, error) {
@@ -143,7 +135,7 @@ func (rc *cartItemController) NewAddressFromForm(ctx *gin.Context) (models.Addre
func (rc *cartItemController) NewOrderFromForm(ctx *gin.Context) (models.Order, error) { func (rc *cartItemController) NewOrderFromForm(ctx *gin.Context) (models.Order, error) {
sessionId := GetSessionId(ctx) sessionId := GetSessionId(ctx)
status := models.OrderStatus("Received") status := models.OrderStatus("AwaitingConfirmation")
token := GenerateToken() token := GenerateToken()
email := ctx.PostForm("email") email := ctx.PostForm("email")
comment := ctx.PostForm("comment") comment := ctx.PostForm("comment")
@@ -162,14 +154,9 @@ func (rc *cartItemController) NewOrderFromForm(ctx *gin.Context) (models.Order,
// } // }
//} //}
var shipping models.Shipping shipping, err := models.GetShippingMethod(shippingStr)
for _, shippingMethod := range GetShippingMethods() {
if shippingMethod.Id == shippingStr {
shipping = shippingMethod
}
}
if shipping == (models.Shipping{}) { if err != nil {
return models.Order{}, fmt.Errorf("Invalid shipping method.") return models.Order{}, fmt.Errorf("Invalid shipping method.")
} }
@@ -267,7 +254,7 @@ func (rc *cartItemController) CartItemView(c *gin.Context) {
data := CreateSessionData(c, gin.H{ data := CreateSessionData(c, gin.H{
"cartItems": cartItems, "cartItems": cartItems,
"priceTotal": fmt.Sprintf("%.2f", priceTotal), //round 2 decimals "priceTotal": fmt.Sprintf("%.2f", priceTotal), //round 2 decimals
"shipping": GetShippingMethods(), "shipping": models.GetShippingMethods(),
}) })
c.HTML(http.StatusOK, "cart.html", data) c.HTML(http.StatusOK, "cart.html", data)
@@ -348,6 +335,11 @@ func (rc *cartItemController) EditItemHandler(c *gin.Context) {
func (rc *cartItemController) CheckoutView(c *gin.Context) { func (rc *cartItemController) CheckoutView(c *gin.Context) {
shippingMethod := c.Query("shippingMethod") shippingMethod := c.Query("shippingMethod")
if shippingMethod == "" {
rc.CartItemView(c)
return
}
c.HTML(http.StatusOK, "checkout.html", gin.H{ c.HTML(http.StatusOK, "checkout.html", gin.H{
"askAddress": (shippingMethod != "pickup"), "askAddress": (shippingMethod != "pickup"),
"shippingMethod": shippingMethod, "shippingMethod": shippingMethod,
@@ -366,10 +358,8 @@ func (rc *cartItemController) CheckoutHandler(c *gin.Context) {
existingOrder, err := repositories.Orders.GetBySession(order.SessionId) existingOrder, err := repositories.Orders.GetBySession(order.SessionId)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
fmt.Println("CREATE")
_, err = repositories.Orders.Create(order) _, err = repositories.Orders.Create(order)
} else if err == nil { } else if err == nil {
fmt.Println("UPDATE")
order.ID = existingOrder.ID order.ID = existingOrder.ID
order.CreatedAt = existingOrder.CreatedAt order.CreatedAt = existingOrder.CreatedAt
repositories.Orders.Update(order) repositories.Orders.Update(order)
@@ -385,19 +375,27 @@ func (rc *cartItemController) CheckoutHandler(c *gin.Context) {
return return
} }
var shipping models.Shipping shipping, err := models.GetShippingMethod(order.Shipping)
for _, shippingMethod := range GetShippingMethods() { if err != nil {
if shippingMethod.Id == order.Shipping { data := CreateSessionData(c, gin.H{
shipping = shippingMethod "error": err,
} "success": "",
})
c.HTML(http.StatusOK, "cart.html", data)
return
} }
priceProducts := 0.0 priceProducts, priceTotal, err := order.CalculatePrices()
for _, cartItem := range order.CartItems { if err != nil {
priceProducts += (float64(cartItem.Quantity) * cartItem.ItemVariant.Price) data := CreateSessionData(c, gin.H{
} "error": err,
"success": "",
})
priceTotal := priceProducts + shipping.Price c.HTML(http.StatusOK, "cart.html", data)
return
}
data := CreateSessionData(c, gin.H{ data := CreateSessionData(c, gin.H{
"error": "", "error": "",
@@ -411,19 +409,85 @@ func (rc *cartItemController) CheckoutHandler(c *gin.Context) {
}) })
fmt.Println(order) fmt.Println(order)
c.HTML(http.StatusOK, "order.html", data) c.HTML(http.StatusOK, "orderpreview.html", data)
} }
func (rc *cartItemController) OrderView(c *gin.Context) { func (rc *cartItemController) OrderView(c *gin.Context) {
shippingMethod := c.Query("shippingMethod") orderToken := c.Param("token")
c.HTML(http.StatusOK, "checkout.html", gin.H{ order, err := repositories.Orders.GetByToken(orderToken)
"askAddress": (shippingMethod != "pickup"), if err != nil {
"shippingMethod": shippingMethod, c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Order does not exist."})
}) return
}
shipping, err := models.GetShippingMethod(order.Shipping)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Could not get shipping method"})
return
}
priceProducts, priceTotal, err := order.CalculatePrices()
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Could not calculate final prices"})
return
}
fmt.Printf("Order: %v\n", order)
fmt.Printf("PriceTotal: %v\n", priceTotal)
fmt.Printf("Amount Items: %v\n", len(order.CartItems))
for _, item := range order.CartItems {
fmt.Printf("Cartitem: %v", item)
}
c.HTML(http.StatusOK, "order.html", CreateSessionData(c, gin.H{
"error": "",
"success": "",
"order": order,
"shipping": shipping,
"priceProducts": fmt.Sprintf("%.2f", priceProducts), //round 2 decimals
"priceTotal": fmt.Sprintf("%.2f", priceTotal), //round 2 decimals
}))
} }
func (rc *cartItemController) OrderHandler(c *gin.Context) { func (rc *cartItemController) OrderHandler(c *gin.Context) {
//get order by session id confirmation := c.PostForm("confirm-order")
//generate token, preview payment info
if confirmation == "" {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Something went wrong, try again later"})
return
}
if confirmation != "true" {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Order was not confirmed."})
return
}
sessionId := GetSessionId(c)
order, err := repositories.Orders.GetBySession(sessionId)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": "Something went wrong, try again later"})
return
}
order.Status = models.AwaitingPayment
err = order.Validate()
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return
}
_, err = repositories.Orders.Update(order)
if err != nil {
c.HTML(http.StatusBadRequest, "error.html", gin.H{"error": err})
return
}
//TODO: cartItemRepository delete all by session - otherwise items stay in cart after completing order..
c.Redirect(http.StatusFound, fmt.Sprintf("/order/%s", order.Token))
} }

View File

@@ -90,7 +90,7 @@ func main() {
viewRoutes.POST("/cart/edit", cartItemController.EditItemHandler) viewRoutes.POST("/cart/edit", cartItemController.EditItemHandler)
viewRoutes.GET("/checkout", cartItemController.CheckoutView) viewRoutes.GET("/checkout", cartItemController.CheckoutView)
viewRoutes.POST("/checkout", cartItemController.CheckoutHandler) viewRoutes.POST("/checkout", cartItemController.CheckoutHandler)
viewRoutes.GET("/order", cartItemController.OrderView) viewRoutes.GET("/order/:token", cartItemController.OrderView)
viewRoutes.POST("/order", cartItemController.OrderHandler) viewRoutes.POST("/order", cartItemController.OrderHandler)
//write middleware that redirects to homescreen on register/login/reset for logged in users //write middleware that redirects to homescreen on register/login/reset for logged in users

View File

@@ -1,59 +1,161 @@
package models package models
import ( import (
"fmt"
"gorm.io/gorm" "gorm.io/gorm"
) )
type OrderStatus string type OrderStatus string
const ( const (
Received OrderStatus = "Received" AwaitingConfirmation OrderStatus = "AwaitingConfirmation"
AwaitingPayment OrderStatus = "AwaitingPayment" Received OrderStatus = "Received"
Payed OrderStatus = "Payed" AwaitingPayment OrderStatus = "AwaitingPayment"
ReadyForPickup OrderStatus = "ReadyForPickup" Payed OrderStatus = "Payed"
Shipped OrderStatus = "Shipped" ReadyForPickup OrderStatus = "ReadyForPickup"
Cancelled OrderStatus = "Cancelled" Shipped OrderStatus = "Shipped"
Cancelled OrderStatus = "Cancelled"
) )
type AddressInfo struct { type AddressInfo struct {
FirstName string `json:"firstname"` FirstName string `json:"firstname"`
LastName string `json:"lastname"` LastName string `json:"lastname"`
Address string `json:"address"` Address string `json:"address"`
PostalCode string `json:"postalcode"` PostalCode string `json:"postalcode"`
City string `json:"city"` City string `json:"city"`
Country string `json:"country"` Country string `json:"country"`
} }
type Shipping struct { type Shipping struct {
Id string `json:"id"` Id string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Price float64 `json:"price"` Price float64 `json:"price"`
} }
func GetShippingMethods() []Shipping {
return []Shipping{
{Id: "germany", Name: "Germany (DHL)", Price: 3.99},
{Id: "international", Name: "International (DHL)", Price: 5.99},
{Id: "pickup", Name: "Pickup", Price: 0.00},
}
}
func GetShippingMethod(id string) (Shipping, error) {
var shipping Shipping
found := false
for _, shippingMethod := range GetShippingMethods() {
if shippingMethod.Id == id {
shipping = shippingMethod
found = true
}
}
if !found {
return Shipping{}, fmt.Errorf("Shipping method does not exist.")
}
return shipping, nil
}
type Order struct { type Order struct {
gorm.Model gorm.Model
SessionId string `json:"sessionid" binding:"required" gorm:"not null"` SessionId string `json:"sessionid" binding:"required" gorm:"not null"`
Status OrderStatus `json:"status"` Status OrderStatus `json:"status"`
Token string `json:"token" binding:"required" gorm:"not null"` Token string `json:"token" binding:"required" gorm:"not null"`
Email string `json:"email"` Email string `json:"email"`
Comment string `json:"comment"` Comment string `json:"comment"`
FirstName string `json:"firstname"` FirstName string `json:"firstname"`
LastName string `json:"lastname"` LastName string `json:"lastname"`
Address string `json:"address"` Address string `json:"address"`
PostalCode string `json:"postalcode"` PostalCode string `json:"postalcode"`
City string `json:"city"` City string `json:"city"`
Country string `json:"country"` Country string `json:"country"`
Shipping string `json:"shipping"` Shipping string `json:"shipping"`
CartItems []CartItem `json:"cartitems" gorm:"foreignKey:OrderID"` CartItems []CartItem `json:"cartitems" gorm:"foreignKey:OrderID"`
}
func (o *Order) Validate() error {
//TODO: validate sessionId
if o.SessionId == "" {
return fmt.Errorf("Invalid SessionId")
}
//TODO: validate token
if o.Token == "" {
return fmt.Errorf("Invalid Token")
}
if o.Status == AwaitingConfirmation {
return fmt.Errorf("Order still awaiting confirmation.")
}
if len(o.CartItems) == 0 {
return fmt.Errorf("Order is empty.")
}
shipping, err := GetShippingMethod(o.Shipping)
if err != nil {
return err
}
//for pickup no address validation is necessary
if shipping.Id == "pickup" {
return nil
}
return o.ValidateAddress()
}
func (o *Order) ValidateAddress() error {
if o.FirstName == "" {
return fmt.Errorf("Firstname missing")
}
if o.LastName == "" {
return fmt.Errorf("Lastname missing")
}
if o.Address == "" {
return fmt.Errorf("Address missing")
}
if o.PostalCode == "" {
return fmt.Errorf("Postalcode missing")
}
if o.City == "" {
return fmt.Errorf("City missing")
}
if o.Country == "" {
return fmt.Errorf("Country missing")
}
return nil
}
func (o *Order) CalculatePrices() (float64, float64, error) {
shipping, err := GetShippingMethod(o.Shipping)
if err != nil {
return 0, 0, err
}
priceProducts := 0.0
for _, cartItem := range o.CartItems {
priceProducts += (float64(cartItem.Quantity) * cartItem.ItemVariant.Price)
}
priceTotal := priceProducts + shipping.Price
return priceProducts, priceTotal, nil
} }
type CartItem struct { type CartItem struct {
gorm.Model gorm.Model
SessionId string `json:"sessionid" binding:"required" gorm:"not null"` SessionId string `json:"sessionid" binding:"required" gorm:"not null"`
ShopItemId uint ShopItemId uint
ShopItem ShopItem `json:"shopitem" gorm:"foreignKey:ShopItemId"` //gorm one2one ShopItem ShopItem `json:"shopitem" gorm:"foreignKey:ShopItemId"` //gorm one2one
ItemVariantId uint ItemVariantId uint
ItemVariant ItemVariant `json:"itemvariant" gorm:"foreignKey:ItemVariantId"` //gorm one2one ItemVariant ItemVariant `json:"itemvariant" gorm:"foreignKey:ItemVariantId"` //gorm one2one
Quantity int `json:"quantity" binding:"required"` Quantity int `json:"quantity" binding:"required"`
OrderID uint OrderID uint
} }

View File

@@ -12,6 +12,7 @@ type OrderRepository interface {
GetAll() ([]models.Order, error) GetAll() ([]models.Order, error)
GetById(string) (models.Order, error) GetById(string) (models.Order, error)
GetBySession(string) (models.Order, error) GetBySession(string) (models.Order, error)
GetByToken(string) (models.Order, error)
Update(models.Order) (models.Order, error) Update(models.Order) (models.Order, error)
DeleteById(string) error DeleteById(string) error
} }
@@ -62,12 +63,19 @@ func (t *GORMOrderRepository) GetById(id string) (models.Order, error) {
func (r *GORMOrderRepository) GetBySession(sessionId string) (models.Order, error) { func (r *GORMOrderRepository) GetBySession(sessionId string) (models.Order, error) {
var orders models.Order var orders models.Order
result := r.DB.Preload("CartItems").Where("session_id = ?", sessionId).First(&orders) result := r.DB.Preload("CartItems").Preload("CartItems.ShopItem").Preload("CartItems.ItemVariant").Where("session_id = ?", sessionId).First(&orders)
return orders, result.Error return orders, result.Error
} }
func (r *GORMOrderRepository) GetByToken(token string) (models.Order, error) {
var orders models.Order
result := r.DB.Preload("CartItems").Preload("CartItems.ShopItem").Preload("CartItems.ItemVariant").Where("token = ?", token).First(&orders)
return orders, result.Error
}
func (r *GORMOrderRepository) Update(order models.Order) (models.Order, error) { func (r *GORMOrderRepository) Update(order models.Order) (models.Order, error) {
result := r.DB.Save(&order) result := r.DB.Save(&order)
if result.Error != nil { if result.Error != nil {

View File

@@ -587,6 +587,22 @@ video {
right: 0px; right: 0px;
} }
.-bottom-\[1\.75rem\] {
bottom: -1.75rem;
}
.end-0 {
inset-inline-end: 0px;
}
.left-1\/2 {
left: 50%;
}
.start-0 {
inset-inline-start: 0px;
}
.col-span-12 { .col-span-12 {
grid-column: span 12 / span 12; grid-column: span 12 / span 12;
} }
@@ -698,6 +714,16 @@ video {
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
} }
.size-5 {
width: 1.25rem;
height: 1.25rem;
}
.size-6 {
width: 1.5rem;
height: 1.5rem;
}
.h-10 { .h-10 {
height: 2.5rem; height: 2.5rem;
} }
@@ -798,6 +824,11 @@ video {
flex-grow: 1; flex-grow: 1;
} }
.-translate-x-1\/2 {
--tw-translate-x: -50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
} }
@@ -810,6 +841,10 @@ video {
grid-template-columns: repeat(12, minmax(0, 1fr)); grid-template-columns: repeat(12, minmax(0, 1fr));
} }
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.flex-col { .flex-col {
flex-direction: column; flex-direction: column;
} }
@@ -822,6 +857,14 @@ video {
align-items: center; align-items: center;
} }
.justify-start {
justify-content: flex-start;
}
.justify-end {
justify-content: flex-end;
}
.justify-center { .justify-center {
justify-content: center; justify-content: center;
} }
@@ -1024,6 +1067,11 @@ video {
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
} }
.bg-gray-600 {
--tw-bg-opacity: 1;
background-color: rgb(75 85 99 / var(--tw-bg-opacity, 1));
}
.fill-red-50 { .fill-red-50 {
fill: #fef2f2; fill: #fef2f2;
} }
@@ -1301,6 +1349,11 @@ video {
color: rgb(255 255 255 / var(--tw-text-opacity, 1)); color: rgb(255 255 255 / var(--tw-text-opacity, 1));
} }
.text-blue-600 {
--tw-text-opacity: 1;
color: rgb(37 99 235 / var(--tw-text-opacity, 1));
}
.underline { .underline {
text-decoration-line: underline; text-decoration-line: underline;
} }
@@ -1357,6 +1410,37 @@ video {
color: rgb(156 163 175 / var(--tw-text-opacity, 1)); color: rgb(156 163 175 / var(--tw-text-opacity, 1));
} }
.after\:mt-4::after {
content: var(--tw-content);
margin-top: 1rem;
}
.after\:block::after {
content: var(--tw-content);
display: block;
}
.after\:h-1::after {
content: var(--tw-content);
height: 0.25rem;
}
.after\:w-full::after {
content: var(--tw-content);
width: 100%;
}
.after\:rounded-lg::after {
content: var(--tw-content);
border-radius: 0.5rem;
}
.after\:bg-gray-200::after {
content: var(--tw-content);
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
}
.focus-within\:outline-none:focus-within { .focus-within\:outline-none:focus-within {
outline: 2px solid transparent; outline: 2px solid transparent;
outline-offset: 2px; outline-offset: 2px;

View File

@@ -1,9 +1,37 @@
{{ template "header.html" . }} {{ template "header.html" . }}
<section class="bg-white py-8 antialiased dark:bg-gray-900 md:py-16"> <section class="bg-white py-8 antialiased dark:bg-gray-900 md:py-16">
<form action="/checkout" method="POST" class="mx-auto max-w-screen-xl px-4 2xl:px-0"> <div class="mx-auto max-w-screen-xl px-4 2xl:px-0">
<div class="mx-auto max-w-3xl"> <div class="mx-auto max-w-3xl">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white sm:text-2xl">Order summary</h2> <h2 class="text-xl font-semibold text-gray-900 dark:text-white sm:text-2xl">Order summary</h2>
<dd class="mt-1 text-base font-normal text-gray-500 dark:text-gray-400">
Thanks for your order! As soon as your payment arrived we will print your Order.
</dd>
<div class="mt-6 space-y-4 border-b border-t border-gray-200 py-8 dark:border-gray-700 sm:mt-8">
<h4 class="text-lg font-semibold text-gray-900 dark:text-white">Order status: {{ .data.order.Status }}</h4>
<dl>
<dd class="mt-1 text-base font-normal text-gray-500 dark:text-gray-400">
Order Code: {{ .data.order.Token }}
</dd>
</dl>
</div>
<div class="mt-6 space-y-4 border-b border-t border-gray-200 py-8 dark:border-gray-700 sm:mt-8">
<h4 class="text-lg font-semibold text-gray-900 dark:text-white">Payment information</h4>
<dl>
<dd class="mt-1 text-base font-normal text-gray-500 dark:text-gray-400">
Either you transfer money to our bank account, or you come by and pay in cash.<br><br>
Miteinander Dresden e.V.*<br>
IBAN: DE66500310001076201001<br>
BIC: TRODDEF1 (Triodos Bank)<br>
Subject: {{ .data.order.Token }}<br>
Amount: {{ .data.priceTotal }}€
</dd>
</dl>
</div>
<div class="mt-6 space-y-4 border-b border-t border-gray-200 py-8 dark:border-gray-700 sm:mt-8"> <div class="mt-6 space-y-4 border-b border-t border-gray-200 py-8 dark:border-gray-700 sm:mt-8">
<h4 class="text-lg font-semibold text-gray-900 dark:text-white">Delivery information</h4> <h4 class="text-lg font-semibold text-gray-900 dark:text-white">Delivery information</h4>
@@ -23,8 +51,6 @@
<p><b>Comment:</b> {{ .data.order.Comment }}</p> <p><b>Comment:</b> {{ .data.order.Comment }}</p>
</dd> </dd>
</dl> </dl>
<a href="/checkout?shippingMethod={{ .data.order.Shipping }}" data-modal-target="billingInformationModal" data-modal-toggle="billingInformationModal" class="text-base font-medium text-primary-700 hover:underline dark:text-primary-500">Edit</a>
</div> </div>
<div class="mt-6 sm:mt-8"> <div class="mt-6 sm:mt-8">
@@ -51,7 +77,6 @@
</div> </div>
<div class="mt-4 space-y-6"> <div class="mt-4 space-y-6">
<h4 class="text-xl font-semibold text-gray-900 dark:text-white">Order summary</h4>
<div class="space-y-4"> <div class="space-y-4">
<div class="space-y-2"> <div class="space-y-2">
@@ -72,21 +97,10 @@
<dd class="text-lg font-bold text-gray-900 dark:text-white">{{ .data.priceTotal }}€</dd> <dd class="text-lg font-bold text-gray-900 dark:text-white">{{ .data.priceTotal }}€</dd>
</dl> </dl>
</div> </div>
<div class="flex items-start sm:items-center">
<input id="terms-checkbox-2" type="checkbox" value="" class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-primary-600 focus:ring-2 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-primary-600" />
<label for="terms-checkbox-2" class="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300"> I agree with the <a href="#" title="" class="text-primary-700 underline hover:no-underline dark:text-primary-500">Terms and Conditions</a> of use of the Flowbite marketplace </label>
</div>
<div class="gap-4 sm:flex sm:items-center">
<button type="button" class="w-full rounded-lg border border-gray-200 bg-white px-5 py-2.5 text-sm font-medium text-gray-900 hover:bg-gray-100 hover:text-primary-700 focus:z-10 focus:outline-none focus:ring-4 focus:ring-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"><a href="/">Return to Shopping</a></button>
<button type="submit" class="w-full bg-gray-900 dark:bg-gray-600 rounded-lg border border-gray-200 bg-white px-5 py-2.5 text-sm font-medium text-gray-900 hover:bg-gray-100 hover:text-primary-700 focus:z-10 focus:outline-none focus:ring-4 focus:ring-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700">Place binding order</button>
</div>
</div> </div>
</div> </div>
</div> </div>
</form> </div>
</section> </section>
{{ template "footer.html" . }} {{ template "footer.html" . }}