allow finalizing orders

This commit is contained in:
2025-03-25 20:36:28 +01:00
parent 30b32a571c
commit 854573eb3a
6 changed files with 360 additions and 88 deletions

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" . }}