From e9e0429e7cb778dffd7131bbc14bbc89dc8c9e7b Mon Sep 17 00:00:00 2001 From: tavo-wasd Date: Wed, 21 Aug 2024 00:55:17 -0600 Subject: [PATCH] advancements --- Makefile | 4 +- main.go | 376 ++++++++++++++++++++++++++--------- public/{form.js => app.js} | 164 ++++++--------- public/index.html | 102 +++++----- public/static/css/style.css | 83 ++++---- public/static/svg/delete.svg | 13 ++ public/static/svg/edit.svg | 9 + 7 files changed, 460 insertions(+), 291 deletions(-) rename public/{form.js => app.js} (57%) create mode 100644 public/static/svg/delete.svg create mode 100644 public/static/svg/edit.svg diff --git a/Makefile b/Makefile index 8b37b88..87b70e6 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,12 @@ BIN = builder SRC = main.go GOFILES = go.sum go.mod -GOMODS = github.com/joho/godotenv +GOMODS = github.com/joho/godotenv github.com/lib/pq all: ${BIN} ${BIN}: ${SRC} ${GOFILES} - go build -o $@ + go build -o builder ${GOFILES}: go mod init ${BIN} diff --git a/main.go b/main.go index 8856da3..834dbff 100644 --- a/main.go +++ b/main.go @@ -1,89 +1,141 @@ package main import ( + "bytes" + "database/sql" "encoding/json" + "fmt" + "io/ioutil" "log" "net/http" - "strings" "os" - "fmt" "os/signal" + "strings" "syscall" - "bytes" - // "time" + "time" "github.com/joho/godotenv" + _ "github.com/lib/pq" ) -type OrderData struct { +var ( + baseURL string + clientID string + clientSecret string + planID string + returnUrl string + cancelUrl string + + exists bool + err error + + dbHost string + dbPort string + dbUser string + dbPass string + dbName string + db *sql.DB + query string +) + +func init() { + // Load .env + if err := godotenv.Load(); err != nil { + log.Fatalf("Error loading .env file: %v", err) + } + + // Set variables + baseURL = os.Getenv("BASE_URL") + clientID = os.Getenv("CLIENT_ID") + clientSecret = os.Getenv("CLIENT_SECRET") + planID = os.Getenv("PLAN_ID") + returnUrl = os.Getenv("RETURN_URL") + cancelUrl = os.Getenv("CANCEL_URL") + // DB creds + dbHost = os.Getenv("DB_HOST") + dbPort = os.Getenv("DB_PORT") + dbUser = os.Getenv("DB_USER") + dbPass = os.Getenv("DB_PASS") + dbName = os.Getenv("DB_NAME") + + // Error if empty + if baseURL == "" || clientID == "" || clientSecret == "" || planID == "" || returnUrl == "" || cancelUrl == "" || + dbHost == "" || dbPort == "" || dbUser == "" || dbPass == "" || dbName == "" { + log.Fatalf("Error setting credentials") + } + + // Connect to DB + var err error + connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + dbHost, dbPort, dbUser, dbPass, dbName) + db, err = sql.Open("postgres", connStr) + if err != nil { + log.Fatalf("Failed to connect to database: %v", err) + } + + // Ping DB + if err = db.Ping(); err != nil { + log.Fatalf("Failed to ping database: %v", err) + } +} + +type CreateOrderResponse struct { + ID string `json:"id"` +} + +type OrderResponse struct { ID string `json:"id"` Status string `json:"status"` PurchaseUnits []struct { Payments struct { Captures []struct { - ID string `json:"id"` - Status string `json:"status"` + ID string `json:"id"` + Status string `json:"status"` + CreateTime time.Time `json:"create_time"` } `json:"captures"` } `json:"payments"` } `json:"purchase_units"` + Payer struct { + Name struct { + GivenName string `json:"given_name"` + Surname string `json:"surname"` + } `json:"name"` + EmailAddress string `json:"email_address"` + Phone struct { + PhoneType string `json:"phone_type"` + PhoneNumber struct { + NationalNumber string `json:"national_number"` + } `json:"phone_number"` + } `json:"phone"` + Address struct { + CountryCode string `json:"country_code"` + } `json:"address"` + } `json:"payer"` } -type SubscriptionData struct { - ID string `json:"id"` +type SubscriptionResponse struct { Status string `json:"status"` - // StatusUpdateTime time.Time `json:"status_update_time"` - PlanID string `json:"plan_id"` - PlanOverridden bool `json:"plan_overridden"` - // StartTime time.Time `json:"start_time"` - Quantity string `json:"quantity"` - ShippingAmount struct { - CurrencyCode string `json:"currency_code"` - Value string `json:"value"` - } `json:"shipping_amount"` + StatusUpdateTime time.Time `json:"status_update_time"` + StartTime time.Time `json:"start_time"` Subscriber struct { Name struct { GivenName string `json:"given_name"` Surname string `json:"surname"` } `json:"name"` EmailAddress string `json:"email_address"` - PayerID string `json:"payer_id"` - ShippingAddress struct { - Name struct { - FullName string `json:"full_name"` - } `json:"name"` - Address struct { - AddressLine1 string `json:"address_line_1"` - AddressLine2 string `json:"address_line_2"` - AdminArea2 string `json:"admin_area_2"` - AdminArea1 string `json:"admin_area_1"` - PostalCode string `json:"postal_code"` - CountryCode string `json:"country_code"` - } `json:"address"` - } `json:"shipping_address"` } `json:"subscriber"` - // CreateTime time.Time `json:"create_time"` - Links []struct { - Href string `json:"href"` - Rel string `json:"rel"` - Method string `json:"method"` - } `json:"links"` + CreateTime time.Time `json:"create_time"` } -var ( - baseURL = "https://api-m.sandbox.paypal.com" -) - -func init() { - if err := godotenv.Load() ; err != nil { - log.Fatalf("Error loading .env file: %v", err) - } +type Cart struct { + Directory string `json:"directory"` } func main() { - // Handlers - http.HandleFunc("/api/orders", CreateOrder) - http.HandleFunc("/api/orders/", CaptureOrder) - http.HandleFunc("/api/paypal/create-subscription", CreateSubscription) + http.HandleFunc("/api/order", CreateOrder) + http.HandleFunc("/api/order/", CaptureOrder) + http.HandleFunc("/api/paypal/subscribe", CreateSubscription) + http.HandleFunc("/api/paypal/subscribe/", CaptureSubscription) http.Handle("/", http.FileServer(http.Dir("./public"))) // Channel to listen for signals @@ -103,13 +155,13 @@ func main() { } func Token() (string, error) { - // Create request + // Create req, err := http.NewRequest("POST", baseURL+"/v1/oauth2/token", strings.NewReader(`grant_type=client_credentials`)) if err != nil { return "", fmt.Errorf("Error creating request: %v", err) } - // Make POST req, should return JSON with AccessToken + // Send req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth(os.Getenv("CLIENT_ID"), os.Getenv("CLIENT_SECRET")) resp, err := http.DefaultClient.Do(req) @@ -118,7 +170,7 @@ func Token() (string, error) { } defer resp.Body.Close() - // Decode response into result + // Decode var result struct { AccessToken string `json:"access_token"` } @@ -126,9 +178,64 @@ func Token() (string, error) { return "", fmt.Errorf("Error decoding response: %v", err) } + // Return return result.AccessToken, nil } +func RegisterOrder(order OrderResponse, directory string) { + var ( + capture string + status string + name string + surname string + email string + phone string + country string + date time.Time + ) + + for _, Unit := range order.PurchaseUnits { + for _, Capture := range Unit.Payments.Captures { + capture = Capture.ID + status = Capture.Status + date = Capture.CreateTime + } + } + + name = order.Payer.Name.GivenName + surname = order.Payer.Name.Surname + email = order.Payer.EmailAddress + phone = order.Payer.Phone.PhoneNumber.NationalNumber + country = order.Payer.Address.CountryCode + + // Register Payment + _, err = db.Exec(`INSERT INTO payments (id, client, directory, status, step, date) VALUES ($1, $2, $3, $4, $5, $6);`, + capture, email, directory, status, "REGISTERED", date) + if err != nil { + fmt.Printf("$v", err) // TODO consider logging in server + } + + // Register Client + err = db.QueryRow(`SELECT EXISTS(SELECT 1 FROM clients WHERE email = $1);`, email).Scan(&exists) + if err != nil { + fmt.Printf("$v", err) // TODO consider logging in server + } + if !exists { + _, err = db.Exec(`INSERT INTO clients (email, name, surname, phone, country) VALUES ($1, $2, $3, $4, $5);`, + email, name, surname, phone, country) + if err != nil { + fmt.Printf("$v", err) // TODO consider logging in server + } + } + + // Register Site + _, err = db.Exec(`INSERT INTO sites (directory, client, status, ends) VALUES ($1, $2, $3, $4);`, + directory, email, "ACTIVE", date.AddDate(1, 0, 0)) // Ends a year later + if err != nil { + fmt.Printf("$v", err) // TODO consider logging in server + } +} + func CreateOrder(w http.ResponseWriter, r *http.Request) { token, err := Token() if err != nil { @@ -144,17 +251,26 @@ func CreateOrder(w http.ResponseWriter, r *http.Request) { "value": "20.00" } }], + "payment_source": { + "paypal": { + "address" : { + "country_code": "CR" + } + } + }, "application_context": { "shipping_preference": "NO_SHIPPING" } }` + // Create req, err := http.NewRequest("POST", baseURL+"/v2/checkout/orders", bytes.NewBuffer([]byte(data))) if err != nil { http.Error(w, "Failed to create request", http.StatusInternalServerError) return } + // Send req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) @@ -164,74 +280,110 @@ func CreateOrder(w http.ResponseWriter, r *http.Request) { } defer resp.Body.Close() - var result map[string]interface{} + // Decode + var result CreateOrderResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { http.Error(w, "Failed to decode response", http.StatusInternalServerError) return } - if id, ok := result["id"].(string); ok { - json.NewEncoder(w).Encode(map[string]string{"id": id}) - return - } else { - http.Error(w, "Order ID not found", http.StatusInternalServerError) - return - } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"id": result.ID}) + return } func CaptureOrder(w http.ResponseWriter, r *http.Request) { + // Read body from order + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusInternalServerError) + return + } + + // Parse to get directory + var cart Cart + err = json.Unmarshal(body, &cart) + if err != nil { + http.Error(w, "Failed to parse request body", http.StatusBadRequest) + return + } + directory := cart.Directory + + // Get orderID path := strings.TrimPrefix(r.URL.Path, "/api/orders/") parts := strings.Split(path, "/") orderID := parts[0] + if orderID == "" { + http.Error(w, "Failed to get orderID from client URL", http.StatusInternalServerError) + return + } - client := &http.Client{} + token, err := Token() + if err != nil { + http.Error(w, "Failed to get access token", http.StatusInternalServerError) + return + } + + // Create req, err := http.NewRequest("POST", baseURL+"/v2/checkout/orders/"+orderID+"/capture", nil) if err != nil { http.Error(w, "Failed to create request", http.StatusInternalServerError) return } - token, err := Token() + // Check if directory already exists + err = db.QueryRow("SELECT EXISTS (SELECT 1 FROM sites WHERE directory = $1 LIMIT 1);", directory).Scan(&exists) if err != nil { - http.Error(w, "Failed to get access token", http.StatusInternalServerError) + http.Error(w, "Failed to check directory ID against database", http.StatusBadRequest) + return + } + if exists { + http.Error(w, "This directory ID is already taken", http.StatusBadRequest) return } + // Send, PAYMENT MADE HERE req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") - resp, err := client.Do(req) + resp, err := http.DefaultClient.Do(req) if err != nil { http.Error(w, "Failed to send request", http.StatusInternalServerError) return } defer resp.Body.Close() - // Create an instance of AutoGenerated - var result OrderData - - // Decode the response into the AutoGenerated struct - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + // Decode + var order OrderResponse + if err := json.NewDecoder(resp.Body).Decode(&order); err != nil { http.Error(w, "Failed to decode response", http.StatusInternalServerError) return } - // Now, `result` contains the entire structured response - // You can send the whole `result` back to the client, or you can selectively send fields. + RegisterOrder(order, directory) + + // Respond w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(result); err != nil { + if err := json.NewEncoder(w).Encode(order); err != nil { http.Error(w, "Failed to encode response", http.StatusInternalServerError) - return } } func CreateSubscription(w http.ResponseWriter, r *http.Request) { - log.Printf("asked to create sub") + // Read body from order + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusInternalServerError) + return + } - planID := os.Getenv("PLAN_ID") - returnUrl := "https://suckless.org" - cancelUrl := "https://suckless.org" - - log.Printf("This is the planid: %s", planID) + // Parse to get directory + var cart Cart + err = json.Unmarshal(body, &cart) + if err != nil { + http.Error(w, "Failed to parse request body", http.StatusBadRequest) + return + } + directory := cart.Directory token, err := Token() if err != nil { @@ -239,9 +391,7 @@ func CreateSubscription(w http.ResponseWriter, r *http.Request) { return } - log.Printf("This is the token: %s", token) - - body := map[string]interface{}{ + payload := map[string]interface{}{ "plan_id": planID, "application_context": map[string]string{ "shipping_preference": "NO_SHIPPING", @@ -249,12 +399,13 @@ func CreateSubscription(w http.ResponseWriter, r *http.Request) { "cancel_url": cancelUrl, }, } - jsonData, err := json.Marshal(body) + jsonData, err := json.Marshal(payload) if err != nil { http.Error(w, "Server error", http.StatusInternalServerError) return } + // Create request log.Printf("Creating request") req, err := http.NewRequest("POST", baseURL+"/v1/billing/subscriptions", bytes.NewBuffer(jsonData)) if err != nil { @@ -262,38 +413,69 @@ func CreateSubscription(w http.ResponseWriter, r *http.Request) { return } + // Check if directory already exists + err = db.QueryRow("SELECT EXISTS (SELECT 1 FROM sites WHERE directory = $1 LIMIT 1);", directory).Scan(&exists) + if err != nil { + http.Error(w, "Failed to check directory ID against database", http.StatusBadRequest) + return + } + if exists { + http.Error(w, "This directory ID is already taken", http.StatusBadRequest) + return + } + + // Send req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Accept", "application/json") req.Header.Set("Prefer", "return=representation") - - log.Printf("Sending request") - client := &http.Client{} - resp, err := client.Do(req) + resp, err := http.DefaultClient.Do(req) if err != nil { http.Error(w, "Failed to send request", http.StatusInternalServerError) return } defer resp.Body.Close() - log.Printf("Request sent") - // Create an instance of AutoGenerated - // var result SubscriptionData + // Decode var result map[string]interface{} - - // Decode the response into the AutoGenerated struct if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { http.Error(w, "Failed to decode response", http.StatusInternalServerError) return } - log.Printf("Raw JSON Response: %v", result) - // Now, `result` contains the entire structured response - // You can send the whole `result` back to the client, or you can selectively send fields. + // Respond w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(result); err != nil { http.Error(w, "Failed to encode response", http.StatusInternalServerError) } log.Printf("sent response to client") - +} + +// Capture just like CaptureOrder, but with response from paypal +// https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_get +func CaptureSubscription(w http.ResponseWriter, r *http.Request) { + // Read body from order + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read request body", http.StatusInternalServerError) + return + } + + // Parse to get directory + var cart Cart + err = json.Unmarshal(body, &cart) + if err != nil { + http.Error(w, "Failed to parse request body", http.StatusBadRequest) + return + } + // directory := cart.Directory + + // Get subID + path := strings.TrimPrefix(r.URL.Path, "/api/subscribe/") + parts := strings.Split(path, "/") + subID := parts[0] + if subID == "" { + http.Error(w, "Failed to get subID from client URL", http.StatusInternalServerError) + return + } } diff --git a/public/form.js b/public/app.js similarity index 57% rename from public/form.js rename to public/app.js index 9ad3d0b..f07f19a 100644 --- a/public/form.js +++ b/public/app.js @@ -1,81 +1,62 @@ -// TODO -// -// 1. Try to disable asking for shipping info (although could be -// useful to mark as sent). -// -// 2. Read about IPN and Webhooks to automate registering process. +document.addEventListener("DOMContentLoaded", function() { + const dialog = document.getElementById("dialog"); + const overlay = document.getElementById("overlay"); + const menu = document.getElementById("floatingButtons"); -const clientId = ""; -const OneTimePID = ""; -const PlanID = ""; + function openDialog() { + dialog.style.display = "block"; + overlay.style.display = "block"; + menu.style.display = "none"; + } -const form = document.getElementById('mainForm'); + function closeDialog() { + dialog.style.display = "none"; + overlay.style.display = "none"; + menu.style.display = "block"; + } -['name', 'email', 'phone'].forEach(id => { - const input = document.createElement('input'); - input.type = 'hidden'; - input.name = id; - input.value = document.getElementById(id).value; - form.appendChild(input); + function togglePaymentMethod(selectedButtonId) { + // Deselect all buttons and hide all PayPal buttons + document.querySelectorAll('#method-button-container button').forEach(button => { button.classList.remove('active'); }); + document.querySelectorAll('#paypal-button-container > div').forEach(div => { div.classList.remove('active'); }); + + // Select the clicked button and show the corresponding PayPal button + const selectedButton = document.getElementById(selectedButtonId); + selectedButton.classList.add('active'); + + if (selectedButtonId === 'showOneTimeButton') { + document.getElementById('paypal-button-container').classList.add('active'); + document.getElementById('paypal-button-container-order').classList.add('active'); + } else if (selectedButtonId === 'showSubButton') { + document.getElementById('paypal-button-container').classList.add('active'); + document.getElementById('paypal-button-container-subscribe').classList.add('active'); + } + } + + document.getElementById('showOneTimeButton').addEventListener('click', function() { + document.getElementById('warning-message').style.display = 'none'; + togglePaymentMethod('showOneTimeButton'); + }); + + document.getElementById('showSubButton').addEventListener('click', function() { + document.getElementById('warning-message').style.display = 'none'; + togglePaymentMethod('showSubButton'); + }); + + document.getElementById("openDialogButton").addEventListener("click", openDialog); + document.getElementById("cancelDialogButton").addEventListener("click", closeDialog); }); -function hideDialog() { - document.getElementById('overlay').style.display = 'none'; - document.getElementById('dialog').style.display = 'none'; - document.getElementById('openDialogButton').style.display = 'block'; -} - -function showDialog() { - document.getElementById('overlay').style.display = 'block'; - document.getElementById('dialog').style.display = 'block'; - document.getElementById('openDialogButton').style.display = 'none'; -} - -function togglePaymentMethod(selectedButtonId) { - // Deselect all buttons and hide all PayPal buttons - document.querySelectorAll('#method-button-container button').forEach(button => { - button.classList.remove('active'); - }); - document.querySelectorAll('#paypal-button-container > div').forEach(div => { - div.classList.remove('active'); - }); - - // Select the clicked button and show the corresponding PayPal button - const selectedButton = document.getElementById(selectedButtonId); - selectedButton.classList.add('active'); - - if (selectedButtonId === 'showOneTimeButton') { - document.getElementById('paypal-button-container').classList.add('active'); - document.getElementById('paypal-button-container-order').classList.add('active'); - } else if (selectedButtonId === 'showSubButton') { - document.getElementById('paypal-button-container').classList.add('active'); - document.getElementById('paypal-button-container-subscribe').classList.add('active'); - } -} - -function isFormValid(form) { - return form.checkValidity(); -} window.paypal_order.Buttons({ style: { shape: 'pill', color: 'black', layout: 'vertical', label: 'pay' }, async createOrder() { try { - const response = await fetch("/api/orders", { + const response = await fetch("/api/order", { method: "POST", headers: { "Content-Type": "application/json", }, - // use the "body" param to optionally pass additional order information - // like product ids and quantities - body: JSON.stringify({ - cart: [ - { - id: "YOUR_PRODUCT_ID", - quantity: "YOUR_PRODUCT_QUANTITY", - }, - ], - }), }); const orderData = await response.json(); @@ -97,11 +78,16 @@ window.paypal_order.Buttons({ }, async onApprove(data, actions) { try { - const response = await fetch(`/api/orders/${data.orderID}/capture`, { + const response = await fetch(`/api/order/${data.orderID}/capture`, { method: "POST", headers: { "Content-Type": "application/json", }, + body: JSON.stringify( + { + directory: "tutorias", + } + ), }); const orderData = await response.json(); @@ -149,16 +135,24 @@ window.paypal_subscribe.Buttons({ style: { shape: 'pill', color: 'black', layout: 'vertical', label: 'subscribe' }, async createSubscription() { try { - const response = await fetch("/api/paypal/create-subscription", { + const response = await fetch("/api/paypal/subscribe", { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ userAction: "SUBSCRIBE_NOW" }), + body: JSON.stringify( + { + // userAction: "SUBSCRIBE_NOW" + directory: "testsite", + } + ), }); const data = await response.json(); if (data?.id) { - resultMessage(`Successful subscription with ID ${data.id}...

`); + const approvalUrl = data.links.find(link => link.rel === "approve").href; + window.location.href = approvalUrl; + resultMessage(`Successful subscription with ID ${approvalUrl}...

`); + // resultMessage(`Successful subscription with ID ${data.id}...

`); return data.id; } else { console.error( @@ -175,8 +169,6 @@ window.paypal_subscribe.Buttons({ { hideButtons: true }, ); } - const approvalUrl = data.links.find(link => link.rel === "approve").href; - window.location.href = approvalUrl; } catch (error) { console.error(error); resultMessage( @@ -201,35 +193,3 @@ window.paypal_subscribe.Buttons({ } }, }).render("#paypal-button-container-subscribe"); // Renders the PayPal button - -// Example function to show a result to the user. Your site's UI library can be used instead. -function resultMessage(message) { - const container = document.querySelector("#checkout"); - container.innerHTML = message; -} - -document.getElementById('showOneTimeButton').addEventListener('click', function() { - if (isFormValid(form)) { - document.getElementById('warning-message').style.display = 'none'; - togglePaymentMethod('showOneTimeButton'); - } else { - document.getElementById('warning-message').style.display = 'block'; - } -}); - -document.getElementById('showSubButton').addEventListener('click', function() { - if (isFormValid(form)) { - document.getElementById('warning-message').style.display = 'none'; - togglePaymentMethod('showSubButton'); - } else { - document.getElementById('warning-message').style.display = 'block'; - } -}); - -document.getElementById('openDialogButton').addEventListener('click', () => { - showDialog(); -}); - -document.getElementById('cancelDialogButton').addEventListener('click', () => { - hideDialog(); -}); diff --git a/public/index.html b/public/index.html index b580f42..cf1349d 100644 --- a/public/index.html +++ b/public/index.html @@ -10,9 +10,6 @@ - - - @@ -26,68 +23,67 @@ -
- - - -
-
+
+ +
+
+
+ +
+
+
+ +
+

Tipo de Pago

+
+

Pago Único: El pago debe realizarse manualmente un año después de contratar el servicio, le enviaremos notificaciones al correo electrónico recordando el pago.

+

Pago Automático: Requiere cuenta de PayPal para rebajo automático, si no tiene una le pedirá configurar rápidamente los datos.

+
+

Por favor digite los campos requeridos.

+
+
+
+ +
- -
-
-
-

Información de Contacto

- -
-

Utilizaremos esta información para contactarle acerca de la publicación del sitio.

-
- - - -
-

Tipo de Pago

-
-

Pago Único: El pago debe realizarse manualmente un año después de contratar el servicio, le enviaremos notificaciones al correo electrónico recordando el pago.

-

Pago Automático: Requiere cuenta de PayPal para rebajo automático, si no tiene una le pedirá configurar rápidamente los datos.

-
-

Por favor digite los campos requeridos.

-
-
-
- - -
-
-
-
-
+
+
+
- - - +
+ diff --git a/public/static/css/style.css b/public/static/css/style.css index 34fdadb..d556616 100644 --- a/public/static/css/style.css +++ b/public/static/css/style.css @@ -35,6 +35,9 @@ --page-width: 90%; --navbar-width: 50vh; } + .floating-button span { + display: none; + } } html { @@ -210,30 +213,6 @@ button { color: var(--color); } -#openDialogButton { - display: flex; - align-items: center; - position: fixed; - bottom: 2em; - right: 1em; - height: 2.5em; - cursor: pointer; - font-size: 1em; - z-index: 1001; - background: linear-gradient(135deg, #00336b, #0099c5); - border-radius: 999px; - box-shadow: #006994 0 10px 20px -10px; - box-sizing: border-box; - color: rgba(255, 255, 255, 0.85); - font-size: 1em; - font-weight: bold; - outline: 0 solid transparent; - padding: 8px 18px; - width: fit-content; - word-break: break-word; - border: 0; -} - #method-button-container { display: flex; justify-content: center; @@ -278,29 +257,59 @@ button { font-size: 1.1em; } -#checkout { +.floating-buttons { + display: flex; + position: fixed; + flex-direction: row; + align-items: center; + bottom: 0.2em; + right: 0.5em; + gap: 0.4em; + z-index: 9999; } -#openDialogButton img { +.floating-button { + height: 2.5em; + cursor: pointer; + font-size: 1em; + z-index: 1001; + border-radius: 999px; + box-sizing: border-box; + color: rgba(255, 255, 255, 0.85); + font-size: 1em; + font-weight: bold; + outline: 0 solid transparent; + padding: 8px 18px; + width: fit-content; + word-break: break-word; + border: 0; +} + +.floating-button img { width: 1.2em; height: 1.2em; vertical-align: middle; } -#openDialogButton span { +.floating-button span { margin-left: 0.6em; } -/* Custom SimpleMDE styling */ -.CodeMirror { - border-radius: 10px; - overflow: hidden; - font-size: 1.2em; - font-family: monospace; - text-align: left; +.floating-button:hover { + background-color: #0056b3; } -.editor-preview { - font-size: 1em; - font-family: sans-serif; +#updateSiteButton { + background: linear-gradient(135deg, #214353, #4c9abf); + box-shadow: #0099c5 0 10px 20px -15px; +} + +#deleteSiteButton { + background: linear-gradient(135deg, #5e2329, #bf4c58); + box-shadow: #e31300 0 10px 20px -15px; +} + +#openDialogButton { + background: linear-gradient(135deg, #21532a, #4fc764); + box-shadow: #27d100 0 10px 20px -15px; } diff --git a/public/static/svg/delete.svg b/public/static/svg/delete.svg new file mode 100644 index 0000000..1dcd05f --- /dev/null +++ b/public/static/svg/delete.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/public/static/svg/edit.svg b/public/static/svg/edit.svg new file mode 100644 index 0000000..39c58ad --- /dev/null +++ b/public/static/svg/edit.svg @@ -0,0 +1,9 @@ + + + + + +