mirror of
https://github.com/tavo-wasd-gh/conex-builder.git
synced 2025-06-07 04:03:29 -06:00
advancements
This commit is contained in:
parent
2f423d35a0
commit
e9e0429e7c
7 changed files with 460 additions and 291 deletions
4
Makefile
4
Makefile
|
@ -1,12 +1,12 @@
|
||||||
BIN = builder
|
BIN = builder
|
||||||
SRC = main.go
|
SRC = main.go
|
||||||
GOFILES = go.sum go.mod
|
GOFILES = go.sum go.mod
|
||||||
GOMODS = github.com/joho/godotenv
|
GOMODS = github.com/joho/godotenv github.com/lib/pq
|
||||||
|
|
||||||
all: ${BIN}
|
all: ${BIN}
|
||||||
|
|
||||||
${BIN}: ${SRC} ${GOFILES}
|
${BIN}: ${SRC} ${GOFILES}
|
||||||
go build -o $@
|
go build -o builder
|
||||||
|
|
||||||
${GOFILES}:
|
${GOFILES}:
|
||||||
go mod init ${BIN}
|
go mod init ${BIN}
|
||||||
|
|
370
main.go
370
main.go
|
@ -1,21 +1,89 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"os"
|
"os"
|
||||||
"fmt"
|
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"bytes"
|
"time"
|
||||||
// "time"
|
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"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"`
|
ID string `json:"id"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
PurchaseUnits []struct {
|
PurchaseUnits []struct {
|
||||||
|
@ -23,67 +91,51 @@ type OrderData struct {
|
||||||
Captures []struct {
|
Captures []struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
CreateTime time.Time `json:"create_time"`
|
||||||
} `json:"captures"`
|
} `json:"captures"`
|
||||||
} `json:"payments"`
|
} `json:"payments"`
|
||||||
} `json:"purchase_units"`
|
} `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 {
|
type SubscriptionResponse struct {
|
||||||
ID string `json:"id"`
|
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
// StatusUpdateTime time.Time `json:"status_update_time"`
|
StatusUpdateTime time.Time `json:"status_update_time"`
|
||||||
PlanID string `json:"plan_id"`
|
StartTime time.Time `json:"start_time"`
|
||||||
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"`
|
|
||||||
Subscriber struct {
|
Subscriber struct {
|
||||||
Name struct {
|
Name struct {
|
||||||
GivenName string `json:"given_name"`
|
GivenName string `json:"given_name"`
|
||||||
Surname string `json:"surname"`
|
Surname string `json:"surname"`
|
||||||
} `json:"name"`
|
} `json:"name"`
|
||||||
EmailAddress string `json:"email_address"`
|
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"`
|
} `json:"subscriber"`
|
||||||
// CreateTime time.Time `json:"create_time"`
|
CreateTime time.Time `json:"create_time"`
|
||||||
Links []struct {
|
|
||||||
Href string `json:"href"`
|
|
||||||
Rel string `json:"rel"`
|
|
||||||
Method string `json:"method"`
|
|
||||||
} `json:"links"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
type Cart struct {
|
||||||
baseURL = "https://api-m.sandbox.paypal.com"
|
Directory string `json:"directory"`
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
if err := godotenv.Load() ; err != nil {
|
|
||||||
log.Fatalf("Error loading .env file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Handlers
|
http.HandleFunc("/api/order", CreateOrder)
|
||||||
http.HandleFunc("/api/orders", CreateOrder)
|
http.HandleFunc("/api/order/", CaptureOrder)
|
||||||
http.HandleFunc("/api/orders/", CaptureOrder)
|
http.HandleFunc("/api/paypal/subscribe", CreateSubscription)
|
||||||
http.HandleFunc("/api/paypal/create-subscription", CreateSubscription)
|
http.HandleFunc("/api/paypal/subscribe/", CaptureSubscription)
|
||||||
http.Handle("/", http.FileServer(http.Dir("./public")))
|
http.Handle("/", http.FileServer(http.Dir("./public")))
|
||||||
|
|
||||||
// Channel to listen for signals
|
// Channel to listen for signals
|
||||||
|
@ -103,13 +155,13 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Token() (string, error) {
|
func Token() (string, error) {
|
||||||
// Create request
|
// Create
|
||||||
req, err := http.NewRequest("POST", baseURL+"/v1/oauth2/token", strings.NewReader(`grant_type=client_credentials`))
|
req, err := http.NewRequest("POST", baseURL+"/v1/oauth2/token", strings.NewReader(`grant_type=client_credentials`))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("Error creating request: %v", err)
|
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.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
req.SetBasicAuth(os.Getenv("CLIENT_ID"), os.Getenv("CLIENT_SECRET"))
|
req.SetBasicAuth(os.Getenv("CLIENT_ID"), os.Getenv("CLIENT_SECRET"))
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
@ -118,7 +170,7 @@ func Token() (string, error) {
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// Decode response into result
|
// Decode
|
||||||
var result struct {
|
var result struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
}
|
}
|
||||||
|
@ -126,9 +178,64 @@ func Token() (string, error) {
|
||||||
return "", fmt.Errorf("Error decoding response: %v", err)
|
return "", fmt.Errorf("Error decoding response: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return
|
||||||
return result.AccessToken, nil
|
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) {
|
func CreateOrder(w http.ResponseWriter, r *http.Request) {
|
||||||
token, err := Token()
|
token, err := Token()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -144,17 +251,26 @@ func CreateOrder(w http.ResponseWriter, r *http.Request) {
|
||||||
"value": "20.00"
|
"value": "20.00"
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
|
"payment_source": {
|
||||||
|
"paypal": {
|
||||||
|
"address" : {
|
||||||
|
"country_code": "CR"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"application_context": {
|
"application_context": {
|
||||||
"shipping_preference": "NO_SHIPPING"
|
"shipping_preference": "NO_SHIPPING"
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
|
// Create
|
||||||
req, err := http.NewRequest("POST", baseURL+"/v2/checkout/orders", bytes.NewBuffer([]byte(data)))
|
req, err := http.NewRequest("POST", baseURL+"/v2/checkout/orders", bytes.NewBuffer([]byte(data)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Failed to create request", http.StatusInternalServerError)
|
http.Error(w, "Failed to create request", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
@ -164,74 +280,110 @@ func CreateOrder(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
var result map[string]interface{}
|
// Decode
|
||||||
|
var result CreateOrderResponse
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
http.Error(w, "Failed to decode response", http.StatusInternalServerError)
|
http.Error(w, "Failed to decode response", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if id, ok := result["id"].(string); ok {
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]string{"id": id})
|
json.NewEncoder(w).Encode(map[string]string{"id": result.ID})
|
||||||
return
|
return
|
||||||
} else {
|
|
||||||
http.Error(w, "Order ID not found", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CaptureOrder(w http.ResponseWriter, r *http.Request) {
|
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/")
|
path := strings.TrimPrefix(r.URL.Path, "/api/orders/")
|
||||||
parts := strings.Split(path, "/")
|
parts := strings.Split(path, "/")
|
||||||
orderID := parts[0]
|
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)
|
req, err := http.NewRequest("POST", baseURL+"/v2/checkout/orders/"+orderID+"/capture", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Failed to create request", http.StatusInternalServerError)
|
http.Error(w, "Failed to create request", http.StatusInternalServerError)
|
||||||
return
|
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 {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send, PAYMENT MADE HERE
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
resp, err := client.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Failed to send request", http.StatusInternalServerError)
|
http.Error(w, "Failed to send request", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// Create an instance of AutoGenerated
|
// Decode
|
||||||
var result OrderData
|
var order OrderResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&order); err != nil {
|
||||||
// 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)
|
http.Error(w, "Failed to decode response", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now, `result` contains the entire structured response
|
RegisterOrder(order, directory)
|
||||||
// You can send the whole `result` back to the client, or you can selectively send fields.
|
|
||||||
|
// Respond
|
||||||
w.Header().Set("Content-Type", "application/json")
|
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)
|
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateSubscription(w http.ResponseWriter, r *http.Request) {
|
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")
|
// Parse to get directory
|
||||||
returnUrl := "https://suckless.org"
|
var cart Cart
|
||||||
cancelUrl := "https://suckless.org"
|
err = json.Unmarshal(body, &cart)
|
||||||
|
if err != nil {
|
||||||
log.Printf("This is the planid: %s", planID)
|
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
directory := cart.Directory
|
||||||
|
|
||||||
token, err := Token()
|
token, err := Token()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -239,9 +391,7 @@ func CreateSubscription(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("This is the token: %s", token)
|
payload := map[string]interface{}{
|
||||||
|
|
||||||
body := map[string]interface{}{
|
|
||||||
"plan_id": planID,
|
"plan_id": planID,
|
||||||
"application_context": map[string]string{
|
"application_context": map[string]string{
|
||||||
"shipping_preference": "NO_SHIPPING",
|
"shipping_preference": "NO_SHIPPING",
|
||||||
|
@ -249,12 +399,13 @@ func CreateSubscription(w http.ResponseWriter, r *http.Request) {
|
||||||
"cancel_url": cancelUrl,
|
"cancel_url": cancelUrl,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
jsonData, err := json.Marshal(body)
|
jsonData, err := json.Marshal(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Server error", http.StatusInternalServerError)
|
http.Error(w, "Server error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create request
|
||||||
log.Printf("Creating request")
|
log.Printf("Creating request")
|
||||||
req, err := http.NewRequest("POST", baseURL+"/v1/billing/subscriptions", bytes.NewBuffer(jsonData))
|
req, err := http.NewRequest("POST", baseURL+"/v1/billing/subscriptions", bytes.NewBuffer(jsonData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -262,38 +413,69 @@ func CreateSubscription(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
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("Content-Type", "application/json")
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
req.Header.Set("Prefer", "return=representation")
|
req.Header.Set("Prefer", "return=representation")
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
log.Printf("Sending request")
|
|
||||||
client := &http.Client{}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Failed to send request", http.StatusInternalServerError)
|
http.Error(w, "Failed to send request", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
log.Printf("Request sent")
|
|
||||||
|
|
||||||
// Create an instance of AutoGenerated
|
// Decode
|
||||||
// var result SubscriptionData
|
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
|
|
||||||
// Decode the response into the AutoGenerated struct
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
http.Error(w, "Failed to decode response", http.StatusInternalServerError)
|
http.Error(w, "Failed to decode response", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Raw JSON Response: %v", result)
|
|
||||||
|
|
||||||
// Now, `result` contains the entire structured response
|
// Respond
|
||||||
// You can send the whole `result` back to the client, or you can selectively send fields.
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
if err := json.NewEncoder(w).Encode(result); err != nil {
|
if err := json.NewEncoder(w).Encode(result); err != nil {
|
||||||
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
log.Printf("sent response to client")
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,44 +1,24 @@
|
||||||
// TODO
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
//
|
const dialog = document.getElementById("dialog");
|
||||||
// 1. Try to disable asking for shipping info (although could be
|
const overlay = document.getElementById("overlay");
|
||||||
// useful to mark as sent).
|
const menu = document.getElementById("floatingButtons");
|
||||||
//
|
|
||||||
// 2. Read about IPN and Webhooks to automate registering process.
|
|
||||||
|
|
||||||
const clientId = "";
|
function openDialog() {
|
||||||
const OneTimePID = "";
|
dialog.style.display = "block";
|
||||||
const PlanID = "";
|
overlay.style.display = "block";
|
||||||
|
menu.style.display = "none";
|
||||||
const form = document.getElementById('mainForm');
|
|
||||||
|
|
||||||
['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 hideDialog() {
|
|
||||||
document.getElementById('overlay').style.display = 'none';
|
|
||||||
document.getElementById('dialog').style.display = 'none';
|
|
||||||
document.getElementById('openDialogButton').style.display = 'block';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showDialog() {
|
function closeDialog() {
|
||||||
document.getElementById('overlay').style.display = 'block';
|
dialog.style.display = "none";
|
||||||
document.getElementById('dialog').style.display = 'block';
|
overlay.style.display = "none";
|
||||||
document.getElementById('openDialogButton').style.display = 'none';
|
menu.style.display = "block";
|
||||||
}
|
}
|
||||||
|
|
||||||
function togglePaymentMethod(selectedButtonId) {
|
function togglePaymentMethod(selectedButtonId) {
|
||||||
// Deselect all buttons and hide all PayPal buttons
|
// Deselect all buttons and hide all PayPal buttons
|
||||||
document.querySelectorAll('#method-button-container button').forEach(button => {
|
document.querySelectorAll('#method-button-container button').forEach(button => { button.classList.remove('active'); });
|
||||||
button.classList.remove('active');
|
document.querySelectorAll('#paypal-button-container > div').forEach(div => { div.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
|
// Select the clicked button and show the corresponding PayPal button
|
||||||
const selectedButton = document.getElementById(selectedButtonId);
|
const selectedButton = document.getElementById(selectedButtonId);
|
||||||
|
@ -53,29 +33,30 @@ function togglePaymentMethod(selectedButtonId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFormValid(form) {
|
document.getElementById('showOneTimeButton').addEventListener('click', function() {
|
||||||
return form.checkValidity();
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
window.paypal_order.Buttons({
|
window.paypal_order.Buttons({
|
||||||
style: { shape: 'pill', color: 'black', layout: 'vertical', label: 'pay' },
|
style: { shape: 'pill', color: 'black', layout: 'vertical', label: 'pay' },
|
||||||
async createOrder() {
|
async createOrder() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/orders", {
|
const response = await fetch("/api/order", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"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();
|
const orderData = await response.json();
|
||||||
|
@ -97,11 +78,16 @@ window.paypal_order.Buttons({
|
||||||
},
|
},
|
||||||
async onApprove(data, actions) {
|
async onApprove(data, actions) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/orders/${data.orderID}/capture`, {
|
const response = await fetch(`/api/order/${data.orderID}/capture`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
|
body: JSON.stringify(
|
||||||
|
{
|
||||||
|
directory: "tutorias",
|
||||||
|
}
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const orderData = await response.json();
|
const orderData = await response.json();
|
||||||
|
@ -149,16 +135,24 @@ window.paypal_subscribe.Buttons({
|
||||||
style: { shape: 'pill', color: 'black', layout: 'vertical', label: 'subscribe' },
|
style: { shape: 'pill', color: 'black', layout: 'vertical', label: 'subscribe' },
|
||||||
async createSubscription() {
|
async createSubscription() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/paypal/create-subscription", {
|
const response = await fetch("/api/paypal/subscribe", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ userAction: "SUBSCRIBE_NOW" }),
|
body: JSON.stringify(
|
||||||
|
{
|
||||||
|
// userAction: "SUBSCRIBE_NOW"
|
||||||
|
directory: "testsite",
|
||||||
|
}
|
||||||
|
),
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data?.id) {
|
if (data?.id) {
|
||||||
resultMessage(`Successful subscription with ID ${data.id}...<br><br>`);
|
const approvalUrl = data.links.find(link => link.rel === "approve").href;
|
||||||
|
window.location.href = approvalUrl;
|
||||||
|
resultMessage(`Successful subscription with ID ${approvalUrl}...<br><br>`);
|
||||||
|
// resultMessage(`Successful subscription with ID ${data.id}...<br><br>`);
|
||||||
return data.id;
|
return data.id;
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
console.error(
|
||||||
|
@ -175,8 +169,6 @@ window.paypal_subscribe.Buttons({
|
||||||
{ hideButtons: true },
|
{ hideButtons: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const approvalUrl = data.links.find(link => link.rel === "approve").href;
|
|
||||||
window.location.href = approvalUrl;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
resultMessage(
|
resultMessage(
|
||||||
|
@ -201,35 +193,3 @@ window.paypal_subscribe.Buttons({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}).render("#paypal-button-container-subscribe"); // Renders the PayPal button
|
}).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();
|
|
||||||
});
|
|
|
@ -10,9 +10,6 @@
|
||||||
<meta name="robots" content="index, follow">
|
<meta name="robots" content="index, follow">
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
<!-- <link rel="stylesheet" href="/static/css/simplemde.min.css"> -->
|
|
||||||
<!-- <link rel="stylesheet" href="/static/css/simplemde-dark.min.css"> -->
|
|
||||||
<!-- <script src="/static/js/simplemde.min.js"></script> -->
|
|
||||||
<!-- EDITORJS -->
|
<!-- EDITORJS -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script><!-- Header -->
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script><!-- Header -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/simple-image@latest"></script><!-- Image -->
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/simple-image@latest"></script><!-- Image -->
|
||||||
|
@ -26,27 +23,35 @@
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
|
||||||
<!-- EDITORJS -->
|
<!-- EDITORJS -->
|
||||||
<script
|
<script
|
||||||
src="https://www.paypal.com/sdk/js?client-id=AcCW43LI1S6lLQgtLkF4V8UOPfmXcqXQ8xfEl41hRuMxSskR2jkWNwQN6Ab1WK7E2E52GNaoYBHqgIKd&components=buttons&enable-funding=card&disable-funding=paylater,venmo"
|
src="https://sandbox.paypal.com/sdk/js?client-id=AcCW43LI1S6lLQgtLkF4V8UOPfmXcqXQ8xfEl41hRuMxSskR2jkWNwQN6Ab1WK7E2E52GNaoYBHqgIKd&components=buttons&enable-funding=card&disable-funding=paylater,venmo"
|
||||||
data-namespace="paypal_order"
|
data-namespace="paypal_order"
|
||||||
></script>
|
></script>
|
||||||
<script
|
<script
|
||||||
src="https://www.paypal.com/sdk/js?client-id=AcCW43LI1S6lLQgtLkF4V8UOPfmXcqXQ8xfEl41hRuMxSskR2jkWNwQN6Ab1WK7E2E52GNaoYBHqgIKd&components=buttons&vault=true&intent=subscription&enable-funding=card&disable-funding=paylater"
|
src="https://sandbox.paypal.com/sdk/js?client-id=AcCW43LI1S6lLQgtLkF4V8UOPfmXcqXQ8xfEl41hRuMxSskR2jkWNwQN6Ab1WK7E2E52GNaoYBHqgIKd&components=buttons&vault=true&intent=subscription&enable-funding=card&disable-funding=paylater"
|
||||||
data-namespace="paypal_subscribe"
|
data-namespace="paypal_subscribe"
|
||||||
></script>
|
></script>
|
||||||
</head>
|
</head>
|
||||||
<form action="submit.php" id="mainForm" method="post">
|
|
||||||
<div class="banner" style="background-image: url(/static/banner.jpg);">
|
<div class="banner" style="background-image: url(/static/banner.jpg);">
|
||||||
<div class="desc">
|
<div class="desc">
|
||||||
<input type="text" name="title" class="input-title" placeholder="[Nombre Ejemplo]">
|
<input type="text" id="title" name="title" class="input-title" placeholder="[Nombre Ejemplo]">
|
||||||
<input type="text" name="slogan" class="input-slogan" placeholder="[Slogan llamativo o breve descripción]">
|
<input type="text" id="slogan" name="slogan" class="input-slogan" placeholder="[Slogan llamativo o breve descripción]">
|
||||||
<button type="button" id="openDialogButton">
|
<div class="floating-buttons" id="floatingButtons">
|
||||||
|
<button class="floating-button" id="openDialogButton">
|
||||||
<img src="/static/svg/cart.svg">
|
<img src="/static/svg/cart.svg">
|
||||||
<span>$20/año</span>
|
<span>$20/año</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="floating-button" id="updateSiteButton">
|
||||||
|
<img src="/static/svg/edit.svg">
|
||||||
|
<span>Actualizar mi página</span>
|
||||||
|
</button>
|
||||||
|
<button class="floating-button" id="deleteSiteButton">
|
||||||
|
<img src="/static/svg/delete.svg">
|
||||||
|
<span>Eliminar página</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<body>
|
<body>
|
||||||
<!-- <button type="button" id="saveButton">Salvar</button> -->
|
|
||||||
<div id="content" class="content">
|
<div id="content" class="content">
|
||||||
<div id="editorjs"></div>
|
<div id="editorjs"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,7 +59,6 @@
|
||||||
<div id="overlay"></div>
|
<div id="overlay"></div>
|
||||||
<div id="dialog">
|
<div id="dialog">
|
||||||
<div>
|
<div>
|
||||||
<h2>Información de Contacto</h2>
|
|
||||||
<button id="cancelDialogButton" type="button">
|
<button id="cancelDialogButton" type="button">
|
||||||
<picture>
|
<picture>
|
||||||
<source srcset="/static/svg/xd.svg" media="(prefers-color-scheme: dark)">
|
<source srcset="/static/svg/xd.svg" media="(prefers-color-scheme: dark)">
|
||||||
|
@ -62,12 +66,6 @@
|
||||||
</picture>
|
</picture>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p>Utilizaremos esta información para contactarle acerca de la publicación del sitio.</p>
|
|
||||||
<div id="form-container">
|
|
||||||
<input type="text" id="name" name="name" placeholder="Nombre (Requerido)" required>
|
|
||||||
<input type="email" id="email" name="email" placeholder="Correo electrónico (Requerido)" required>
|
|
||||||
<input type="tel" id="phone" name="phone" placeholder="Teléfono (Opcional)">
|
|
||||||
</div>
|
|
||||||
<h2>Tipo de Pago</h2>
|
<h2>Tipo de Pago</h2>
|
||||||
<div>
|
<div>
|
||||||
<p><strong>Pago Único:</strong> El pago debe realizarse manualmente un año después de contratar el servicio, le enviaremos notificaciones al correo electrónico recordando el pago.</p>
|
<p><strong>Pago Único:</strong> El pago debe realizarse manualmente un año después de contratar el servicio, le enviaremos notificaciones al correo electrónico recordando el pago.</p>
|
||||||
|
@ -86,8 +84,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<script src="/app.js"></script>
|
||||||
<!-- <script src="/static/js/simplemde.config.js"></script> -->
|
|
||||||
<script src="/form.js"></script>
|
|
||||||
<script src="/static/js/config.editor.js"></script>
|
<script src="/static/js/config.editor.js"></script>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -35,6 +35,9 @@
|
||||||
--page-width: 90%;
|
--page-width: 90%;
|
||||||
--navbar-width: 50vh;
|
--navbar-width: 50vh;
|
||||||
}
|
}
|
||||||
|
.floating-button span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
@ -210,30 +213,6 @@ button {
|
||||||
color: var(--color);
|
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 {
|
#method-button-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -278,29 +257,59 @@ button {
|
||||||
font-size: 1.1em;
|
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;
|
width: 1.2em;
|
||||||
height: 1.2em;
|
height: 1.2em;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
#openDialogButton span {
|
.floating-button span {
|
||||||
margin-left: 0.6em;
|
margin-left: 0.6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom SimpleMDE styling */
|
.floating-button:hover {
|
||||||
.CodeMirror {
|
background-color: #0056b3;
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 1.2em;
|
|
||||||
font-family: monospace;
|
|
||||||
text-align: left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-preview {
|
#updateSiteButton {
|
||||||
font-size: 1em;
|
background: linear-gradient(135deg, #214353, #4c9abf);
|
||||||
font-family: sans-serif;
|
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;
|
||||||
}
|
}
|
||||||
|
|
13
public/static/svg/delete.svg
Normal file
13
public/static/svg/delete.svg
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg fill="#fff" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
width="800px" height="800px" viewBox="0 0 485 485" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<rect x="67.224" width="350.535" height="71.81"/>
|
||||||
|
<path d="M417.776,92.829H67.237V485h350.537V92.829H417.776z M165.402,431.447h-28.362V146.383h28.362V431.447z M256.689,431.447
|
||||||
|
h-28.363V146.383h28.363V431.447z M347.97,431.447h-28.361V146.383h28.361V431.447z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 727 B |
9
public/static/svg/edit.svg
Normal file
9
public/static/svg/edit.svg
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg fill="#fff" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||||
|
<path d="M70.2,337.4l104.4,104.4L441.5,175L337,70.5L70.2,337.4z M0.6,499.8c-2.3,9.3,2.3,13.9,11.6,11.6L151.4,465L47,360.6
|
||||||
|
L0.6,499.8z M487.9,24.1c-46.3-46.4-92.8-11.6-92.8-11.6c-7.6,5.8-34.8,34.8-34.8,34.8l104.4,104.4c0,0,28.9-27.2,34.8-34.8
|
||||||
|
C499.5,116.9,534.3,70.6,487.9,24.1z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 665 B |
Loading…
Reference in a new issue