remove pocketbase
This commit is contained in:
@ -1,82 +0,0 @@
|
|||||||
export class TurnStilleCaptcha {
|
|
||||||
sitekey = "{{.SiteKey}}";
|
|
||||||
/**
|
|
||||||
* Initializes the TurnStilleCaptcha instance.
|
|
||||||
* Creates a container for the captcha and appends it to the target element.
|
|
||||||
* If the Cloudflare Turnstile script is already loaded, it runs the captcha.
|
|
||||||
* Otherwise, it loads the script and initializes the captcha with the given properties.
|
|
||||||
* @param {HTMLElement} target - The element to attach the captcha container to.
|
|
||||||
* @param {Object} [props={}] - Optional properties for captcha initialization, such as theme.
|
|
||||||
*/
|
|
||||||
|
|
||||||
constructor(target, props = {}) {
|
|
||||||
// create holder
|
|
||||||
this.holder = document.createElement("div");
|
|
||||||
this.holder.id = "turnstile-container";
|
|
||||||
this.theme = props.theme || "auto";
|
|
||||||
target.appendChild(this.holder);
|
|
||||||
|
|
||||||
|
|
||||||
// execute code
|
|
||||||
if (window.turnstile) {
|
|
||||||
this.runCaptcha();
|
|
||||||
} else {
|
|
||||||
this.loadCloudflareScript();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
runCaptcha() {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (globalThis.turnstileInstance) {
|
|
||||||
window.turnstile.remove(globalThis.turnstileInstance);
|
|
||||||
}
|
|
||||||
globalThis.turnstileInstance = window.turnstile.render(this.holder, {
|
|
||||||
sitekey: this.sitekey,
|
|
||||||
theme: this.theme,
|
|
||||||
callback: (token) => {
|
|
||||||
if (token) {
|
|
||||||
const event = new CustomEvent("token", {
|
|
||||||
detail: token,
|
|
||||||
bubbles: true,
|
|
||||||
});
|
|
||||||
this.holder.dispatchEvent(event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
const event = new CustomEvent("failure", {
|
|
||||||
detail: error,
|
|
||||||
bubbles: true,
|
|
||||||
});
|
|
||||||
this.holder.dispatchEvent(event);
|
|
||||||
window.turnstile.reset(globalThis.turnstileInstance);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadCloudflareScript() {
|
|
||||||
const script = document.createElement("script");
|
|
||||||
script.id = "turnstile-script";
|
|
||||||
script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js";
|
|
||||||
// script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
|
|
||||||
script.async = true;
|
|
||||||
script.defer = true;
|
|
||||||
|
|
||||||
script.onload = () => {
|
|
||||||
const event = new CustomEvent("loaded", {
|
|
||||||
detail: "Turnstile script loaded",
|
|
||||||
bubbles: true,
|
|
||||||
});
|
|
||||||
this.holder.dispatchEvent(event);
|
|
||||||
|
|
||||||
this.runCaptcha();
|
|
||||||
};
|
|
||||||
script.onerror = () => {
|
|
||||||
const event = new CustomEvent("failure", {
|
|
||||||
detail: "Failed to load Turnstile script",
|
|
||||||
bubbles: true,
|
|
||||||
});
|
|
||||||
this.holder.dispatchEvent(event);
|
|
||||||
};
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
package cloudflare
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"html/template"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
var VerifyUrl = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
|
|
||||||
|
|
||||||
type TurnstileResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
ErrorCodes []string `json:"error-codes,omitempty"`
|
|
||||||
ChallengeTS string `json:"challenge_ts,omitempty"`
|
|
||||||
Hostname string `json:"hostname,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TurnStilleCaptcha struct {
|
|
||||||
SiteKey string
|
|
||||||
SecretKey string
|
|
||||||
}
|
|
||||||
|
|
||||||
func ServeTurnstilleCaptchaJS(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] {
|
|
||||||
return se.Router.GET("/api/email/script.js", func(e *core.RequestEvent) error {
|
|
||||||
// www.abrasive.ma-al.pl
|
|
||||||
// siteKey: "0x4AAAAAABdgeAdu4Pxxovj3"
|
|
||||||
// secretKey: "0x4AAAAAABdgeHJDjMwmeX5aXaXGh6HWZbw"
|
|
||||||
|
|
||||||
settings, err := GetSettings(app)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := JS.ReadFile("TurnStilleCaptcha.js")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
templ, err := template.New("test").Parse(string(file))
|
|
||||||
|
|
||||||
buf := bytes.Buffer{}
|
|
||||||
templ.Execute(&buf, map[string]interface{}{
|
|
||||||
"SiteKey": settings.SiteKey,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Response.Header().Set("Content-Type", "application/javascript")
|
|
||||||
return e.String(http.StatusOK, buf.String())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetSettings(app *pocketbase.PocketBase) (*TurnStilleCaptcha, error) {
|
|
||||||
record, err := app.FindFirstRecordByFilter("settings", "key='turnstile'", nil)
|
|
||||||
|
|
||||||
settings := TurnStilleCaptcha{}
|
|
||||||
json.Unmarshal([]byte(record.GetString("value")), &settings)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &settings, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func VerifyTurnstile(app *pocketbase.PocketBase, token, ip string) error {
|
|
||||||
conf, err := GetSettings(app)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
data := map[string]string{"secret": conf.SecretKey, "response": token, "remoteip": ip}
|
|
||||||
jsonData, _ := json.Marshal(data)
|
|
||||||
resp, err := http.Post(VerifyUrl, "application/json", bytes.NewBuffer(jsonData))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
var turnstileResp TurnstileResponse
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
json.Unmarshal(body, &turnstileResp)
|
|
||||||
|
|
||||||
if !turnstileResp.Success {
|
|
||||||
return errors.New(turnstileResp.ChallengeTS + ": " + strings.Join(turnstileResp.ErrorCodes, " "))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
package cloudflare
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed TurnStilleCaptcha.js
|
|
||||||
var JS embed.FS
|
|
@ -1,61 +0,0 @@
|
|||||||
package custom
|
|
||||||
|
|
||||||
import (
|
|
||||||
"pocketbase/custom/cloudflare"
|
|
||||||
"pocketbase/custom/gtm"
|
|
||||||
"pocketbase/custom/mail"
|
|
||||||
"pocketbase/custom/manifest"
|
|
||||||
"pocketbase/custom/proxy"
|
|
||||||
"pocketbase/custom/seo"
|
|
||||||
"pocketbase/custom/supervise"
|
|
||||||
"pocketbase/custom/version"
|
|
||||||
webpconverter "pocketbase/custom/webpConverter"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
)
|
|
||||||
|
|
||||||
func LoadCustomCode(app *pocketbase.PocketBase, se *core.ServeEvent) error {
|
|
||||||
|
|
||||||
// include supervised subprocess if command provided
|
|
||||||
supervise.ServeSubprocessSupervisor(app, se)
|
|
||||||
|
|
||||||
// include serving js code from cloudflare
|
|
||||||
cloudflare.ServeTurnstilleCaptchaJS(app, se)
|
|
||||||
|
|
||||||
// include sending emails service
|
|
||||||
mail.ServeMailSender(app, se)
|
|
||||||
|
|
||||||
// include robots.txt endpoint
|
|
||||||
seo.ServeRobotsTxt(app, se)
|
|
||||||
|
|
||||||
// include feeds endpoint
|
|
||||||
seo.ServeFeeds(app, se)
|
|
||||||
seo.ServeFeedsIndex(app, se)
|
|
||||||
|
|
||||||
// include proxy to serve nuxt app
|
|
||||||
proxy.ServeProxyPassingToNuxt(app, se)
|
|
||||||
|
|
||||||
// create endpoint to serve version information
|
|
||||||
version.ServeVersionInfo(app, se)
|
|
||||||
|
|
||||||
// include endpoint to server GTM script
|
|
||||||
gtm.ServeTagMangerJS(app, se)
|
|
||||||
|
|
||||||
// include endpoint serving manifest
|
|
||||||
manifest.ServeManifst(app, se)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExtendApp(app *pocketbase.PocketBase) *pocketbase.PocketBase {
|
|
||||||
app.RootCmd.PersistentFlags().String("proxy", "", "inner proxy target")
|
|
||||||
app.RootCmd.PersistentFlags().String("subcommand", "", "provide command with params like cli that will be executed and supervised by main process")
|
|
||||||
|
|
||||||
// include webp converter
|
|
||||||
app.OnRecordCreate().Bind(webpconverter.CreateEventHandler(app))
|
|
||||||
app.OnRecordUpdate().Bind(webpconverter.CreateEventHandler(app))
|
|
||||||
app.OnFileDownloadRequest().Bind(webpconverter.ThumbEventHandler(app))
|
|
||||||
|
|
||||||
return app
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
package gtm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ServeTagMangerJS(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] {
|
|
||||||
return se.Router.GET("/api/gtm/script.js", func(e *core.RequestEvent) error {
|
|
||||||
|
|
||||||
settings, err := GetSettings(app)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := JS.ReadFile("gtm.js")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
templ, err := template.New("test").Parse(string(file))
|
|
||||||
|
|
||||||
buf := bytes.Buffer{}
|
|
||||||
templ.Execute(&buf, map[string]interface{}{
|
|
||||||
"GtagID": settings.GtagID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Response.Header().Set("Content-Type", "application/javascript")
|
|
||||||
return e.String(http.StatusOK, buf.String())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type GTagSettings struct {
|
|
||||||
GtagID string `json:"gtagID"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetSettings(app *pocketbase.PocketBase) (*GTagSettings, error) {
|
|
||||||
record, err := app.FindFirstRecordByFilter("settings", "key='gtagID'", nil)
|
|
||||||
|
|
||||||
settings := GTagSettings{}
|
|
||||||
json.Unmarshal([]byte(record.GetString("value")), &settings)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &settings, nil
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
export class GTM {
|
|
||||||
gtagID = "{{.GtagID}}";
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.insertScript(window, document, 'script', 'dataLayer', this.gtagID);
|
|
||||||
this.insertNoScript();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
insertScript(w, d, s, l, i) {
|
|
||||||
w[l] = w[l] || []; w[l].push({
|
|
||||||
'gtm.start':
|
|
||||||
new Date().getTime(), event: 'gtm.js'
|
|
||||||
}); var f = d.getElementsByTagName(s)[0],
|
|
||||||
j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =
|
|
||||||
'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);
|
|
||||||
}
|
|
||||||
|
|
||||||
insertNoScript() {
|
|
||||||
const noscript = document.createElement("noscript");
|
|
||||||
const iframe = document.createElement("iframe");
|
|
||||||
iframe.src = "https://www.googletagmanager.com/ns.html?id=" + this.gtagID;
|
|
||||||
iframe.height = 0;
|
|
||||||
iframe.width = 0;
|
|
||||||
iframe.style = "display:none;visibility:hidden";
|
|
||||||
noscript.appendChild(iframe);
|
|
||||||
document.body.appendChild(noscript);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
package gtm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed gtm.js
|
|
||||||
var JS embed.FS
|
|
@ -1,175 +0,0 @@
|
|||||||
package mail
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/mail"
|
|
||||||
"pocketbase/custom/cloudflare"
|
|
||||||
"strings"
|
|
||||||
"text/template"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/mailer"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EmailData struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Phone string `json:"phone"`
|
|
||||||
LangIso string `json:"lang_iso"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MailsSettings struct {
|
|
||||||
ReceiverMail string `json:"receiver_mail"`
|
|
||||||
ReceiverName string `json:"receiver_name"`
|
|
||||||
Subject string `json:"subject"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ServeMailSender(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] {
|
|
||||||
return se.Router.POST("/api/email/send", func(e *core.RequestEvent) error {
|
|
||||||
// name := e.Request.PathValue("name")
|
|
||||||
|
|
||||||
data := EmailData{}
|
|
||||||
if err := e.BindBody(&data); err != nil {
|
|
||||||
e.Response.Header().Set("Content-Type", "application/json")
|
|
||||||
app.Logger().Error(err.Error(), "type", "mail")
|
|
||||||
return e.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
if pass, err := validateMX(data.Email); !pass && err != nil {
|
|
||||||
e.Response.Header().Set("Content-Type", "application/json")
|
|
||||||
app.Logger().Error("Invalid email address.", "type", "mail", "error", err.Error())
|
|
||||||
return e.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid email address."})
|
|
||||||
}
|
|
||||||
|
|
||||||
// fmt.Printf("e.Request.Body: %v IP: %s\n", data, e.RealIP())
|
|
||||||
|
|
||||||
err := cloudflare.VerifyTurnstile(app, data.Token, e.RealIP())
|
|
||||||
if err != nil {
|
|
||||||
e.Response.Header().Set("Content-Type", "application/json")
|
|
||||||
app.Logger().Error("Captcha verification failed.", "type", "mail", "error", err.Error())
|
|
||||||
return e.JSON(http.StatusBadRequest, map[string]string{"error": "Captcha verification failed."})
|
|
||||||
}
|
|
||||||
|
|
||||||
eamil_body, err := app.FindFirstRecordByFilter("email_template", fmt.Sprintf("name='contact_form'&&id_lang='%s'", data.LangIso), nil)
|
|
||||||
if err != nil {
|
|
||||||
e.Response.Header().Set("Content-Type", "application/json")
|
|
||||||
app.Logger().Error("Template not available", "type", "mail", "error", err.Error())
|
|
||||||
return e.JSON(http.StatusBadRequest, map[string]string{"error": "Template not available"})
|
|
||||||
}
|
|
||||||
templ, err := template.New("test").Parse(eamil_body.GetString("template"))
|
|
||||||
if err != nil {
|
|
||||||
app.Logger().Error("Template parsing error.", "type", "mail", "error", err.Error())
|
|
||||||
e.Response.Header().Set("Content-Type", "application/json")
|
|
||||||
return e.JSON(http.StatusBadRequest, map[string]string{"error": "Template parsing error."})
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := bytes.Buffer{}
|
|
||||||
templ.Execute(&buf, map[string]interface{}{
|
|
||||||
"Data": time.Now().Local().Format("2006-01-02 15:04:05"),
|
|
||||||
"Name": data.Name,
|
|
||||||
"Email": data.Email,
|
|
||||||
"Message": strings.ReplaceAll(data.Message, "\n", "<br>"),
|
|
||||||
"Phone": data.Phone,
|
|
||||||
})
|
|
||||||
|
|
||||||
mailsSettings, err := getSettings(app)
|
|
||||||
if err != nil {
|
|
||||||
app.Logger().Error("Mails settings corrupted", "type", "mail", "error", err.Error())
|
|
||||||
e.Response.Header().Set("Content-Type", "application/json")
|
|
||||||
return e.JSON(http.StatusBadRequest, map[string]string{"error": "Mails settings corrupted"})
|
|
||||||
}
|
|
||||||
|
|
||||||
cc := []mail.Address{{Address: data.Email, Name: data.Name}}
|
|
||||||
to := []mail.Address{{Address: mailsSettings.ReceiverMail, Name: mailsSettings.ReceiverName}}
|
|
||||||
|
|
||||||
message := &mailer.Message{
|
|
||||||
From: mail.Address{
|
|
||||||
Address: e.App.Settings().Meta.SenderAddress,
|
|
||||||
Name: e.App.Settings().Meta.SenderName,
|
|
||||||
},
|
|
||||||
Cc: cc,
|
|
||||||
To: to,
|
|
||||||
Subject: mailsSettings.Subject,
|
|
||||||
HTML: buf.String(),
|
|
||||||
}
|
|
||||||
|
|
||||||
err = e.App.NewMailClient().Send(message)
|
|
||||||
if err != nil {
|
|
||||||
app.Logger().Error("Mails sending error", "type", "mail", "error", err.Error())
|
|
||||||
e.Response.Header().Set("Content-Type", "application/json")
|
|
||||||
return e.JSON(http.StatusBadRequest, map[string]string{"error": "Mails sending error"})
|
|
||||||
}
|
|
||||||
|
|
||||||
receivers := formReceiversList(to, cc, []mail.Address{})
|
|
||||||
app.Logger().Info("Mail Sent", "type", "mail", "receivers", receivers)
|
|
||||||
return e.JSON(http.StatusOK, map[string]string{"status": "success", "message": "Email sent successfully. to " + receivers})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func formReceiversList(to []mail.Address, cc []mail.Address, bcc []mail.Address) string {
|
|
||||||
|
|
||||||
res := ""
|
|
||||||
|
|
||||||
for _, a := range to {
|
|
||||||
res = fmt.Sprintf("%s %s<%s>", res, a.Name, a.Address)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, a := range cc {
|
|
||||||
res = fmt.Sprintf("%s %s<%s>", res, a.Name, a.Address)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, a := range bcc {
|
|
||||||
res = fmt.Sprintf("%s %s<%s>", res, a.Name, a.Address)
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
func getSettings(app *pocketbase.PocketBase) (*MailsSettings, error) {
|
|
||||||
record, err := app.FindFirstRecordByFilter("settings", "key='contact_page'", nil)
|
|
||||||
|
|
||||||
settings := MailsSettings{}
|
|
||||||
json.Unmarshal([]byte(record.GetString("value")), &settings)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &settings, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractDomain(email string) string {
|
|
||||||
parts := strings.Split(email, "@")
|
|
||||||
if len(parts) != 2 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(parts[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateMX checks if the domain has valid MX records or A records as a fallback
|
|
||||||
func validateMX(email string) (bool, error) {
|
|
||||||
// Check MX records
|
|
||||||
|
|
||||||
domain := extractDomain(email)
|
|
||||||
mxRecords, err := net.LookupMX(domain)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("'MX' records for %s not found", domain)
|
|
||||||
} else if len(mxRecords) > 0 {
|
|
||||||
// At least one MX record exists
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: Check for A records (some domains accept mail via A records)
|
|
||||||
aRecords, err := net.LookupHost(domain)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("'A' record for %s not found", domain)
|
|
||||||
}
|
|
||||||
return len(aRecords) > 0, nil
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
package manifest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Manifest struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
ShortName string `json:"short_name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Icons []Icon `json:"icons"`
|
|
||||||
StartURL string `json:"start_url"`
|
|
||||||
Display string `json:"display"`
|
|
||||||
BackgroundColor string `json:"background_color"`
|
|
||||||
ThemeColor string `json:"theme_color"`
|
|
||||||
Lang string `json:"lang"`
|
|
||||||
Author string `json:"author"`
|
|
||||||
OgHost string `json:"ogHost"`
|
|
||||||
Orientation string `json:"orientation"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Icon struct {
|
|
||||||
Src string `json:"src"`
|
|
||||||
Sizes string `json:"sizes"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ServeManifst(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] {
|
|
||||||
return se.Router.GET("/api/manifest.json", func(e *core.RequestEvent) error {
|
|
||||||
manifest, err := GetSettings(app)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
e.Response.Header().Add("content-type", "application/json")
|
|
||||||
return e.String(http.StatusOK, manifest)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetSettings(app *pocketbase.PocketBase) (string, error) {
|
|
||||||
record, err := app.FindFirstRecordByFilter("settings", "key='manifest'", nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return record.GetString("value"), nil
|
|
||||||
}
|
|
@ -1,121 +0,0 @@
|
|||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httputil"
|
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Logger *slog.Logger
|
|
||||||
|
|
||||||
func ServeProxyPassingToNuxt(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] {
|
|
||||||
Logger = app.Logger()
|
|
||||||
|
|
||||||
proxyUrl, _ := app.RootCmd.Flags().GetString("proxy")
|
|
||||||
|
|
||||||
if len(proxyUrl) > 0 {
|
|
||||||
target, _ := url.Parse(proxyUrl) // Node.js app
|
|
||||||
|
|
||||||
proxy := httputil.NewSingleHostReverseProxy(target)
|
|
||||||
|
|
||||||
originalDirector := proxy.Director
|
|
||||||
proxy.Director = func(req *http.Request) {
|
|
||||||
originalDirector(req)
|
|
||||||
req.Host = target.Host
|
|
||||||
}
|
|
||||||
proxy.Transport = &loggingTransport{http.DefaultTransport}
|
|
||||||
|
|
||||||
proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, err error) {
|
|
||||||
if errors.Is(err, context.Canceled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
app.Logger().Error(fmt.Sprintf("Proxy error: %v for %s %s", err, req.Method, req.URL.Path), "type", "proxy")
|
|
||||||
http.Error(rw, "Proxy error", http.StatusBadGateway)
|
|
||||||
}
|
|
||||||
|
|
||||||
return se.Router.Any("/", func(e *core.RequestEvent) error {
|
|
||||||
// Ping the backend with a HEAD request (or TCP dial)
|
|
||||||
backendUp := isBackendAlive(target)
|
|
||||||
if !backendUp {
|
|
||||||
app.Logger().Error(fmt.Sprintf("Backend %s is unreachable, sending 502", target), "type", "proxy", "path", e.Request.URL.Path, "remoteIP", e.Request.RemoteAddr)
|
|
||||||
e.Response.WriteHeader(http.StatusBadGateway)
|
|
||||||
e.Response.Write([]byte("502 Backend is unavailable"))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forward to Node.js
|
|
||||||
proxy.ServeHTTP(e.Response, e.Request)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isBackendAlive(target *url.URL) bool {
|
|
||||||
conn, err := net.DialTimeout("tcp", target.Host, 500*time.Millisecond)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
conn.Close()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
type loggingTransport struct {
|
|
||||||
rt http.RoundTripper
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *loggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
||||||
start := time.Now()
|
|
||||||
|
|
||||||
// Do the actual request
|
|
||||||
resp, err := t.rt.RoundTrip(req)
|
|
||||||
duration := time.Since(start)
|
|
||||||
|
|
||||||
// Prepare fields
|
|
||||||
remoteAddr := req.RemoteAddr
|
|
||||||
if ip := req.Header.Get("X-Real-IP"); len(ip) > 0 {
|
|
||||||
remoteAddr = ip
|
|
||||||
}
|
|
||||||
method := req.Method
|
|
||||||
uri := req.URL.RequestURI()
|
|
||||||
// proto := req.Proto
|
|
||||||
status := 0
|
|
||||||
size := 0
|
|
||||||
|
|
||||||
if resp != nil {
|
|
||||||
status = resp.StatusCode
|
|
||||||
if resp.ContentLength > 0 {
|
|
||||||
size = int(resp.ContentLength)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
referer := req.Referer()
|
|
||||||
ua := req.UserAgent()
|
|
||||||
// timestamp := time.Now().Format("02/Jan/2006:15:04:05 -0700")
|
|
||||||
|
|
||||||
Logger.Info(
|
|
||||||
fmt.Sprintf("%s %s", method, uri),
|
|
||||||
"type", "proxy",
|
|
||||||
// "method", method,
|
|
||||||
// "url", uri,
|
|
||||||
"referer", referer,
|
|
||||||
"remoteIP", remoteAddr,
|
|
||||||
"userAgent", ua,
|
|
||||||
"execTime", fmt.Sprintf("%.4fms", float64(duration.Milliseconds())),
|
|
||||||
"status", status,
|
|
||||||
"size", size,
|
|
||||||
)
|
|
||||||
|
|
||||||
return resp, err
|
|
||||||
}
|
|
@ -1,194 +0,0 @@
|
|||||||
package seo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"path"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MenuRecord struct {
|
|
||||||
Active bool `json:"active"`
|
|
||||||
CollectionID string `json:"collectionId"`
|
|
||||||
CollectionName string `json:"collectionName"`
|
|
||||||
Created string `json:"created"`
|
|
||||||
ID string `json:"id"`
|
|
||||||
IDLang string `json:"id_lang"`
|
|
||||||
IDPage string `json:"id_page"`
|
|
||||||
IDParent string `json:"id_parent"`
|
|
||||||
IsDefault bool `json:"is_default"`
|
|
||||||
IsRoot bool `json:"is_root"`
|
|
||||||
LinkRewrite string `json:"link_rewrite"`
|
|
||||||
LinkTitle string `json:"link_title"`
|
|
||||||
MetaDescription string `json:"meta_description"`
|
|
||||||
MetaTitle string `json:"meta_title"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
PageName string `json:"page_name"`
|
|
||||||
PositionID int `json:"position_id"`
|
|
||||||
Updated string `json:"updated"`
|
|
||||||
Url string `json:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UrlSet struct {
|
|
||||||
XMLName xml.Name `xml:"urlset"`
|
|
||||||
Xmlns string `xml:"xmlns,attr"`
|
|
||||||
Urls []Url `xml:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Url struct {
|
|
||||||
Loc string `xml:"loc"`
|
|
||||||
LastMod string `xml:"lastmod,omitempty"`
|
|
||||||
ChangeFreq string `xml:"changefreq,omitempty"`
|
|
||||||
Priority string `xml:"priority,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ServeFeeds(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] {
|
|
||||||
return se.Router.GET("/feeds/{lang}/sitemap.xml", func(e *core.RequestEvent) error {
|
|
||||||
|
|
||||||
lang := e.Request.PathValue("lang")
|
|
||||||
|
|
||||||
urls, err := getLocations(app, lang)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
xx := UrlSet{
|
|
||||||
Xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9",
|
|
||||||
Urls: urls,
|
|
||||||
}
|
|
||||||
|
|
||||||
bytes, err := xml.MarshalIndent(xx, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Response.Header().Add("content-type", "text/xml")
|
|
||||||
return e.String(http.StatusOK, xml.Header+string(bytes))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLocations(app *pocketbase.PocketBase, lang string) ([]Url, error) {
|
|
||||||
records, err := app.FindRecordsByFilter("menu_view", fmt.Sprintf("id_lang='%s'&&active=true", lang), "position_id", 200, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
baseUrl, err := getBaseUrl(app)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
locations := []Url{}
|
|
||||||
lastMod := time.Now().Add(time.Hour * 24 * 7 * -1).Format("2006-01-02")
|
|
||||||
|
|
||||||
for _, r := range records {
|
|
||||||
rec := MenuRecord{}
|
|
||||||
x, _ := r.MarshalJSON()
|
|
||||||
json.Unmarshal(x, &rec)
|
|
||||||
if rec.IsRoot {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(rec.Url) > 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if rec.IsDefault {
|
|
||||||
|
|
||||||
locations = append(locations, Url{
|
|
||||||
Loc: baseUrl + path.Join(lang),
|
|
||||||
LastMod: lastMod,
|
|
||||||
ChangeFreq: "weekly",
|
|
||||||
Priority: "1.0",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
locations = append(locations, Url{
|
|
||||||
Loc: baseUrl + path.Join(lang, rec.IDPage, rec.LinkRewrite),
|
|
||||||
LastMod: lastMod,
|
|
||||||
ChangeFreq: "weekly",
|
|
||||||
Priority: "1.0",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return locations, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type BaseUrl struct {
|
|
||||||
BaseURL string `json:"baseUrl"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBaseUrl(app *pocketbase.PocketBase) (string, error) {
|
|
||||||
record, err := app.FindFirstRecordByFilter("settings", "key='baseUrl'", nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
settings := BaseUrl{}
|
|
||||||
json.Unmarshal([]byte(record.GetString("value")), &settings)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings.BaseURL, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type SitemapIndex struct {
|
|
||||||
XMLName xml.Name `xml:"sitemapindex"`
|
|
||||||
Xmlns string `xml:"xmlns,attr"`
|
|
||||||
Sitemaps []Sitemap `xml:"sitemap"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sitemap represents each <sitemap> entry
|
|
||||||
type Sitemap struct {
|
|
||||||
Loc string `xml:"loc"`
|
|
||||||
LastMod string `xml:"lastmod"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ServeFeedsIndex(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] {
|
|
||||||
return se.Router.GET("/feeds/index.xml", func(e *core.RequestEvent) error {
|
|
||||||
index, err := makeSiteMapIndex(app)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
bytes, err := xml.MarshalIndent(index, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Response.Header().Add("content-type", "text/xml")
|
|
||||||
return e.String(http.StatusOK, xml.Header+string(bytes))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeSiteMapIndex(app *pocketbase.PocketBase) (*SitemapIndex, error) {
|
|
||||||
index := &SitemapIndex{
|
|
||||||
Xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9",
|
|
||||||
}
|
|
||||||
|
|
||||||
bseUrl, err := getBaseUrl(app)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
langs, err := app.FindRecordsByFilter("lang", "active=true", "", 200, 0)
|
|
||||||
if err != nil {
|
|
||||||
return index, err
|
|
||||||
}
|
|
||||||
|
|
||||||
lastMod := time.Now().Add(time.Hour * 24 * 7 * -1).Format("2006-01-02")
|
|
||||||
|
|
||||||
for _, l := range langs {
|
|
||||||
index.Sitemaps = append(index.Sitemaps, Sitemap{
|
|
||||||
Loc: bseUrl + path.Join("feeds/", l.Id, "sitemap.xml"),
|
|
||||||
LastMod: lastMod,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return index, nil
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
package seo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Robots struct {
|
|
||||||
Robots []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func ServeRobotsTxt(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] {
|
|
||||||
return se.Router.GET("/robots.txt", func(e *core.RequestEvent) error {
|
|
||||||
|
|
||||||
text, err := getRobots(app)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return e.String(http.StatusOK, text)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRobots(app *pocketbase.PocketBase) (string, error) {
|
|
||||||
record, err := app.FindFirstRecordByFilter("settings", "key='robots_txt'", nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
settings := Robots{}
|
|
||||||
json.Unmarshal([]byte(record.GetString("value")), &settings)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
baseUrl, err := getBaseUrl(app)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
settings.Robots = append(settings.Robots, fmt.Sprintf("\n\nSitemap: %s%s", baseUrl, "feeds/index.xml"))
|
|
||||||
|
|
||||||
return strings.Join(settings.Robots, "\n"), nil
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
package supervise
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ServeSubprocessSupervisor(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] {
|
|
||||||
|
|
||||||
command, _ := app.RootCmd.PersistentFlags().GetString("subcommand")
|
|
||||||
|
|
||||||
if len(command) > 0 {
|
|
||||||
cmdsub := strings.Split(command, " ")
|
|
||||||
if len(cmdsub) > 0 {
|
|
||||||
startNodeProcessSupervised(cmdsub[0], strings.Join(cmdsub[1:], " "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func startNodeProcessSupervised(command string, args ...string) {
|
|
||||||
const maxRetries = 3
|
|
||||||
const retryDelay = 30 * time.Second
|
|
||||||
|
|
||||||
var (
|
|
||||||
cmdMu sync.Mutex
|
|
||||||
cmd *exec.Cmd
|
|
||||||
)
|
|
||||||
|
|
||||||
stopChan := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(stopChan, os.Interrupt, syscall.SIGTERM)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
retries := 0
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-stopChan:
|
|
||||||
log.Println("Received shutdown signal. Terminating subprocess...")
|
|
||||||
cmdMu.Lock()
|
|
||||||
if cmd != nil && cmd.Process != nil {
|
|
||||||
_ = cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
}
|
|
||||||
cmdMu.Unlock()
|
|
||||||
return
|
|
||||||
|
|
||||||
default:
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
cmdMu.Lock()
|
|
||||||
cmd = exec.CommandContext(ctx, command, args...)
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
cmdMu.Unlock()
|
|
||||||
|
|
||||||
log.Printf("Starting Process: %s %v\n", command, args)
|
|
||||||
err := cmd.Run()
|
|
||||||
cancel() // cancel the context when done
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Process exited with error: %v\n", err)
|
|
||||||
retries++
|
|
||||||
if retries >= maxRetries {
|
|
||||||
log.Printf("Process failed %d times. Shutting down application...", retries)
|
|
||||||
// _ = app.ResetBootstrapState()
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
log.Printf("Retrying in %s (%d/%d)...", retryDelay, retries, maxRetries)
|
|
||||||
time.Sleep(retryDelay)
|
|
||||||
} else {
|
|
||||||
log.Printf("Process exited normally. Resetting retry count.")
|
|
||||||
retries = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
package version
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Version string
|
|
||||||
var BuildDate string
|
|
||||||
var Company string
|
|
||||||
var CompanyUrl string
|
|
||||||
|
|
||||||
func ServeVersionInfo(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] {
|
|
||||||
return se.Router.GET("/api/version/send", func(e *core.RequestEvent) error {
|
|
||||||
|
|
||||||
return e.JSON(http.StatusOK, map[string]string{
|
|
||||||
"version": Version,
|
|
||||||
"build_date": BuildDate,
|
|
||||||
"company": Company,
|
|
||||||
"company_url": CompanyUrl,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
package webpconverter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
type memFileReader struct {
|
|
||||||
data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memFileReader) Open() (io.ReadSeekCloser, error) {
|
|
||||||
return readSeekCloser{bytes.NewReader(m.data)}, nil
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
package webpconverter
|
|
||||||
|
|
||||||
import "bytes"
|
|
||||||
|
|
||||||
type readSeekCloser struct {
|
|
||||||
*bytes.Reader
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r readSeekCloser) Close() error { return nil }
|
|
@ -1,144 +0,0 @@
|
|||||||
package webpconverter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"image"
|
|
||||||
"image/png"
|
|
||||||
"io"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/chai2010/webp"
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/hook"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CreateEventHandler(app *pocketbase.PocketBase) *hook.Handler[*core.RecordEvent] {
|
|
||||||
return &hook.Handler[*core.RecordEvent]{
|
|
||||||
Func: func(e *core.RecordEvent) error {
|
|
||||||
fieldsData := e.Record.FieldsData()
|
|
||||||
for _, v := range fieldsData {
|
|
||||||
if files, ok := v.([]interface{}); ok {
|
|
||||||
for _, f := range files {
|
|
||||||
if file, ok := f.(*filesystem.File); ok {
|
|
||||||
if shouldBeReplacedWithWebp(file.Name) {
|
|
||||||
convertToWebp(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if f, ok := v.(interface{}); ok {
|
|
||||||
if file, ok := f.(*filesystem.File); ok {
|
|
||||||
if shouldBeReplacedWithWebp(file.Name) {
|
|
||||||
convertToWebp(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return e.Next()
|
|
||||||
},
|
|
||||||
Priority: 100,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ThumbEventHandler(app *pocketbase.PocketBase) *hook.Handler[*core.FileDownloadRequestEvent] {
|
|
||||||
return &hook.Handler[*core.FileDownloadRequestEvent]{
|
|
||||||
Func: func(fdre *core.FileDownloadRequestEvent) error {
|
|
||||||
if filepath.Ext(fdre.ServedName) == ".webp" {
|
|
||||||
|
|
||||||
filename := fdre.Request.PathValue("filename")
|
|
||||||
baseFilesPath := fdre.Record.BaseFilesPath()
|
|
||||||
|
|
||||||
fs, err := app.App.NewFilesystem()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer fs.Close()
|
|
||||||
|
|
||||||
blob, err := fs.GetReader(baseFilesPath + "/thumbs_" + filename + "/" + fdre.ServedName)
|
|
||||||
if err != nil {
|
|
||||||
blob, err = fs.GetReader(baseFilesPath + "/" + fdre.ServedName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer blob.Close()
|
|
||||||
|
|
||||||
img, err := png.Decode(blob)
|
|
||||||
if err != nil {
|
|
||||||
return fdre.Next()
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := bytes.Buffer{}
|
|
||||||
err = webp.Encode(&buf, img, &webp.Options{Quality: 80})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = fs.Upload(buf.Bytes(), baseFilesPath+"/thumbs_"+filename+"/"+fdre.ServedName)
|
|
||||||
if err != nil {
|
|
||||||
_, err := fdre.Response.Write(buf.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fdre.Next()
|
|
||||||
}
|
|
||||||
return fdre.Next()
|
|
||||||
},
|
|
||||||
Priority: 99,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertToWebp(file *filesystem.File) error {
|
|
||||||
file.Name = replaceExtWithWebp(file.Name)
|
|
||||||
file.OriginalName = replaceExtWithWebp(file.OriginalName)
|
|
||||||
ff, err := file.Reader.Open()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer ff.Close()
|
|
||||||
fff, err := io.ReadAll(ff)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
img, _, err := image.Decode(bytes.NewReader(fff))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := bytes.Buffer{}
|
|
||||||
err = webp.Encode(&buf, img, &webp.Options{Quality: 80})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
file.Reader = &memFileReader{data: buf.Bytes()}
|
|
||||||
file.Size = int64(buf.Len())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func replaceExtWithWebp(path string) string {
|
|
||||||
// List of image extensions to convert
|
|
||||||
exts := []string{".png", ".jpg", ".jpeg", ".bmp", ".tiff"}
|
|
||||||
|
|
||||||
for _, ext := range exts {
|
|
||||||
if strings.HasSuffix(strings.ToLower(path), ext) {
|
|
||||||
return strings.TrimSuffix(path, ext) + ".webp"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldBeReplacedWithWebp(path string) bool {
|
|
||||||
// List of image extensions to convert
|
|
||||||
exts := []string{".png", ".jpg", ".jpeg", ".bmp", ".tiff"}
|
|
||||||
|
|
||||||
for _, ext := range exts {
|
|
||||||
if strings.HasSuffix(strings.ToLower(path), ext) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
module pocketbase
|
|
||||||
|
|
||||||
go 1.23.2
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/chai2010/webp v1.4.0
|
|
||||||
github.com/pocketbase/pocketbase v0.28.2
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
|
||||||
github.com/disintegration/imaging v1.6.2 // indirect
|
|
||||||
github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
|
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
|
||||||
github.com/fatih/color v1.18.0 // indirect
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
|
||||||
github.com/ganigeorgiev/fexpr v0.5.0 // indirect
|
|
||||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
|
||||||
github.com/pocketbase/dbx v1.11.0 // indirect
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
|
||||||
github.com/spf13/cast v1.8.0 // indirect
|
|
||||||
github.com/spf13/cobra v1.9.1 // indirect
|
|
||||||
github.com/spf13/pflag v1.0.6 // indirect
|
|
||||||
golang.org/x/crypto v0.38.0 // indirect
|
|
||||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
|
|
||||||
golang.org/x/image v0.27.0 // indirect
|
|
||||||
golang.org/x/net v0.40.0 // indirect
|
|
||||||
golang.org/x/oauth2 v0.30.0 // indirect
|
|
||||||
golang.org/x/sync v0.14.0 // indirect
|
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
|
||||||
golang.org/x/text v0.25.0 // indirect
|
|
||||||
modernc.org/libc v1.65.7 // indirect
|
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
|
||||||
modernc.org/memory v1.11.0 // indirect
|
|
||||||
modernc.org/sqlite v1.37.1 // indirect
|
|
||||||
)
|
|
127
backend/go.sum
127
backend/go.sum
@ -1,127 +0,0 @@
|
|||||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
|
||||||
github.com/chai2010/webp v1.4.0 h1:6DA2pkkRUPnbOHvvsmGI3He1hBKf/bkRlniAiSGuEko=
|
|
||||||
github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
|
||||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
|
||||||
github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
|
|
||||||
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
|
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
|
||||||
github.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv/Tk=
|
|
||||||
github.com/ganigeorgiev/fexpr v0.5.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
|
|
||||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
|
|
||||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
|
|
||||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
|
||||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
|
|
||||||
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
|
|
||||||
github.com/pocketbase/pocketbase v0.28.2 h1:b6cfUfr5d4whvUFGFhI8oHRzx/eB76GCUQGftqgv9lM=
|
|
||||||
github.com/pocketbase/pocketbase v0.28.2/go.mod h1:ElwIYS1b5xS9w0U7AK7tsm6FuC0lzw57H8p/118Cu7g=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
|
||||||
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
|
|
||||||
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
|
||||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
|
||||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
|
||||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
|
||||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
|
||||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
|
||||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
|
||||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
|
||||||
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
|
|
||||||
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
|
|
||||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
|
||||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
|
||||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
|
||||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
|
||||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
|
||||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
|
||||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
|
||||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|
||||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
|
||||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
|
||||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
|
||||||
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
|
||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
|
|
||||||
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
|
||||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
|
||||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
|
||||||
modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
|
|
||||||
modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
|
||||||
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
|
|
||||||
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
|
||||||
modernc.org/libc v1.65.8 h1:7PXRJai0TXZ8uNA3srsmYzmTyrLoHImV5QxHeni108Q=
|
|
||||||
modernc.org/libc v1.65.8/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|
||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
|
||||||
modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs=
|
|
||||||
modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g=
|
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
|
||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
|
@ -1,27 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"pocketbase/custom"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := pocketbase.New()
|
|
||||||
|
|
||||||
app = custom.ExtendApp(app)
|
|
||||||
|
|
||||||
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
|
||||||
|
|
||||||
custom.LoadCustomCode(app, se)
|
|
||||||
|
|
||||||
return se.Next()
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := app.Start(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
@ -18,7 +18,7 @@
|
|||||||
<div class="flex items-center h-[100px] sm:h-[205px]">
|
<div class="flex items-center h-[100px] sm:h-[205px]">
|
||||||
<div
|
<div
|
||||||
class="min-w-[100px] sm:min-w-[205px] flex items-center justify-center h-[100px] sm:h-[205px]">
|
class="min-w-[100px] sm:min-w-[205px] flex items-center justify-center h-[100px] sm:h-[205px]">
|
||||||
<img :src="`https://www.yourgold.cz/api/public/file/${item.picture_uuid}.webp`"
|
<img :src="`/api/public/file/${item.picture_uuid}.webp`"
|
||||||
alt="" class="max-w-full max-h-full object-contain">
|
alt="" class="max-w-full max-h-full object-contain">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
<div class="w-full flex items-center justify-between">
|
<div class="w-full flex items-center justify-between">
|
||||||
<div class="flex items-center gap-[30px]">
|
<div class="flex items-center gap-[30px]">
|
||||||
<p @click="menuStore.navigateToItem(menuStore.menuItems?.find((item) => item.id === 4))" class="cursor-pointer">button</p>
|
<p @click="menuStore.navigateToItem(menuStore.menuItems?.find((item) => item.id === 13))" class="cursor-pointer">button</p>
|
||||||
<div>
|
<div>
|
||||||
<i v-if="!userStore.isLogged"
|
<i v-if="!userStore.isLogged"
|
||||||
@click="menuStore.navigateToItem(menuStore.menuItems?.find((item) => item.id === 11))"
|
@click="menuStore.navigateToItem(menuStore.menuItems?.find((item) => item.id === 11))"
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
<template>
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Modi perspiciatis adipisci quam odio natus odit excepturi
|
|
||||||
eveniet vitae. Fugit dicta officiis quos quia debitis perspiciatis porro ducimus earum placeat sunt?
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts"></script>
|
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<UiContainer>
|
<UiContainer>
|
||||||
<div class="xl:w-[85%] mx-auto">
|
<div class="xl:w-[85%] mx-auto">
|
||||||
<div v-if="userStore.isLogged" class="space-25-55">
|
<div class="space-25-55">
|
||||||
<div class="w-full flex items-center sm:justify-center">
|
<div class="w-full flex items-center sm:justify-center">
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between sm:justify-center sm:gap-[25px] text-gray dark:text-button-disabled w-full sm:w-auto">
|
class="flex items-center justify-between sm:justify-center sm:gap-[25px] text-gray dark:text-button-disabled w-full sm:w-auto">
|
||||||
@ -79,7 +79,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm sm:text-xl border-r pr-[10px] border-block">{{ checkoutStore.currentPrefix }}</p>
|
<p class="text-sm sm:text-xl border-r pr-[10px] border-block">{{
|
||||||
|
checkoutStore.currentPrefix }}</p>
|
||||||
</div>
|
</div>
|
||||||
<input id="phone"
|
<input id="phone"
|
||||||
:value="checkoutStore.vUseAccountPhoneNumber ? checkoutStore.accountPhoneNumber : checkoutStore.phoneNumber"
|
:value="checkoutStore.vUseAccountPhoneNumber ? checkoutStore.accountPhoneNumber : checkoutStore.phoneNumber"
|
||||||
@ -107,7 +108,8 @@
|
|||||||
<h2 class="h2-bold-bounded">
|
<h2 class="h2-bold-bounded">
|
||||||
{{ $t("Select delivery address") }}
|
{{ $t("Select delivery address") }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex flex-col md:flex-row items-center justify-center gap-[10px] sm:gap-[25px] md:h-[225px]">
|
<div
|
||||||
|
class="flex flex-col md:flex-row items-center justify-center gap-[10px] sm:gap-[25px] md:h-[225px]">
|
||||||
<div class="w-full sm:w-[500px] flex flex-col gap-4 h-full">
|
<div class="w-full sm:w-[500px] flex flex-col gap-4 h-full">
|
||||||
<div v-for="(item, index) in checkoutStore.addressesList" :key="index"
|
<div v-for="(item, index) in checkoutStore.addressesList" :key="index"
|
||||||
:class="['flex min-h-[200px] md:h-full flex-col py-[15px] px-[25px] gap-[15px] rounded-lg border-2', checkoutStore.activeAddress === item ? 'border-button' : 'border-block']">
|
:class="['flex min-h-[200px] md:h-full flex-col py-[15px] px-[25px] gap-[15px] rounded-lg border-2', checkoutStore.activeAddress === item ? 'border-button' : 'border-block']">
|
||||||
@ -228,7 +230,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { LazyColorScheme } from '#components';
|
|
||||||
import CheckoutInput from '../ui/CheckoutInput.vue';
|
import CheckoutInput from '../ui/CheckoutInput.vue';
|
||||||
import { onClickOutside } from "@vueuse/core";
|
import { onClickOutside } from "@vueuse/core";
|
||||||
|
|
||||||
|
77
components/section/CheckoutSummary.vue
Normal file
77
components/section/CheckoutSummary.vue
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<UiContainer>
|
||||||
|
<div class="xl:w-[85%] mx-auto space-25-55">
|
||||||
|
<div class="space-25-55">
|
||||||
|
<div class="w-full flex items-center sm:justify-center">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between sm:justify-center sm:gap-[25px] text-gray dark:text-button-disabled w-full sm:w-auto">
|
||||||
|
<div class="sm:px-6 sm:py-3 mx-auto">
|
||||||
|
{{ $t("login") }}
|
||||||
|
</div>
|
||||||
|
<div class="sm:px-6 sm:py-3 mx-auto">
|
||||||
|
{{ $t("address") }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="cursor-pointer transition-all text-inter hover:bg-button-hover bg-button text-white font-medium rounded-xl px-3 py-1 sm:px-6 sm:py-3">
|
||||||
|
{{ $t("summary") }}
|
||||||
|
</div>
|
||||||
|
<div class="hidden sm:block sm:px-6 sm:py-3 sm:mx-auto">
|
||||||
|
{{ $t("order_placed") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 gap-[30px]">
|
||||||
|
<div class="col-start-1 col-end-3 space-y-5">
|
||||||
|
<h4 class="h4-uppercase-bold-inter">Seznam produktů</h4>
|
||||||
|
<div class="border-2 border-block rounded-2xl p-[50px] space-25-55">
|
||||||
|
<div v-for="(item, index) in checkoutStore.products" :key="index">
|
||||||
|
<div class="flex items-center h-[150px]">
|
||||||
|
<div class="min-w-[150px] flex items-center justify-center h-[150px]">
|
||||||
|
<img :src="`/api/public/file/${item.picture_uuid}.webp`"
|
||||||
|
alt="" class="max-w-full max-h-full object-contain">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col justify-between min-h-full w-full gap-[7px] sm:gap-[15px]">
|
||||||
|
<div class="w-full flex items-center justify-between">
|
||||||
|
<h3
|
||||||
|
class="text-[10px] sm:text-base md:text-lg text-xl font-bold leading-[130%] sm:leading-[150%] max-w-[100px] sm:max-w-[200px] md:max-w-[250px]">
|
||||||
|
{{ item.name }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full justify-between gap-[10px]">
|
||||||
|
<p
|
||||||
|
class="text-accent-green-light dark:text-accent-green-dark font-inter text-[12px] sm:text-[21px] md:text-2xl leading-[150%] font-bold">
|
||||||
|
{{ item.total_price }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<div class="space-y-5">
|
||||||
|
<h4 class="h4-uppercase-bold-inter">Adresa účtu</h4>
|
||||||
|
<div class="border-2 border-block rounded-2xl px-2 py-3 flex flex-col gap-1">
|
||||||
|
<span>{{ checkoutStore.defaultAddress?.address.name }} {{
|
||||||
|
checkoutStore.defaultAddress?.address.surname }}</span>
|
||||||
|
<span>{{ checkoutStore.defaultAddress?.address.street }}</span>
|
||||||
|
<span>{{ checkoutStore.defaultAddress?.address.postcode }} {{
|
||||||
|
checkoutStore.defaultAddress?.address.city }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const checkoutStore = useCheckoutStore();
|
||||||
|
const productStore = useProductStore()
|
||||||
|
|
||||||
|
checkoutStore.getUserCart()
|
||||||
|
checkoutStore.getDeliveryOptions()
|
||||||
|
checkoutStore.getDefAddress()
|
||||||
|
</script>
|
@ -3,8 +3,8 @@
|
|||||||
<UiContainer class="flex flex-col gap-24">
|
<UiContainer class="flex flex-col gap-24">
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-1 md:grid-cols-2 gap-[75px] xl:gap-0 xl:grid-cols-none xl:grid-flow-col auto-cols-max justify-between">
|
class="grid grid-cols-1 md:grid-cols-2 gap-[75px] xl:gap-0 xl:grid-cols-none xl:grid-flow-col auto-cols-max justify-between">
|
||||||
<div v-for="(item, index) in component.front_section_lang[0].data" :key="index"
|
<div v-if="component.front_section_lang" v-for="(item, index) in component.front_section_lang[0].data"
|
||||||
class="flex flex-col gap-[25px] sm:gap-8 max-w-[280px]">
|
:key="index" class="flex flex-col gap-[25px] sm:gap-8 max-w-[280px]">
|
||||||
<h3 class="h4-uppercase-bold-inter">{{ item.title }}</h3>
|
<h3 class="h4-uppercase-bold-inter">{{ item.title }}</h3>
|
||||||
<div class="cursor-pointer hover:text-text-light/80 dark:hover:text-text-dark/70 transition-all text-inter"
|
<div class="cursor-pointer hover:text-text-light/80 dark:hover:text-text-dark/70 transition-all text-inter"
|
||||||
v-for="(el, indexEl) in item.children" :key="indexEl">
|
v-for="(el, indexEl) in item.children" :key="indexEl">
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
]">
|
]">
|
||||||
<div v-for="(item, index) in productStore.productList" :key="index"
|
<div v-for="(item, index) in productStore.productList" :key="index"
|
||||||
class="w-[200px] sm:w-[260px] md:w-[290px] sm:py-5 sm:px-[15px] py-[15px] px-[10px] bg-block rounded-2xl flex flex-col items-center gap-5 sm:gap-7">
|
class="w-[200px] sm:w-[260px] md:w-[290px] sm:py-5 sm:px-[15px] py-[15px] px-[10px] bg-block rounded-2xl flex flex-col items-center gap-5 sm:gap-7">
|
||||||
<img :src="`https://www.yourgold.cz/api/public/file/${item.cover_picture_uuid}.webp`" alt="pics"
|
<img :src="`/api/public/file/${item.cover_picture_uuid}.webp`" alt="pics"
|
||||||
class="max-h-[150px] sm:max-h-[180px] md:max-h-[205px]" />
|
class="max-h-[150px] sm:max-h-[180px] md:max-h-[205px]" />
|
||||||
<div class="flex flex-col justify-between h-full">
|
<div class="flex flex-col justify-between h-full">
|
||||||
<div class="flex flex-col gap-[10px] sm:gap-[15px] w-full">
|
<div class="flex flex-col gap-[10px] sm:gap-[15px] w-full">
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<!-- product -->
|
<!-- product -->
|
||||||
<div v-for="(item, index) in productStore.productList" :key="index"
|
<div v-for="(item, index) in productStore.productList" :key="index"
|
||||||
class="w-[200px] sm:w-[260px] md:w-[290px] sm:py-5 sm:px-[15px] py-[15px] px-[10px] bg-block rounded-2xl flex flex-col items-center gap-5 sm:gap-7">
|
class="w-[200px] sm:w-[260px] md:w-[290px] sm:py-5 sm:px-[15px] py-[15px] px-[10px] bg-block rounded-2xl flex flex-col items-center gap-5 sm:gap-7">
|
||||||
<img :src="`https://www.yourgold.cz/api/public/file/${item.cover_picture_uuid}.webp`" alt="pics"
|
<img :src="`/api/public/file/${item.cover_picture_uuid}.webp`" alt="pics"
|
||||||
class="max-h-[150px] sm:max-h-[180px] md:max-h-[205px]" />
|
class="max-h-[150px] sm:max-h-[180px] md:max-h-[205px]" />
|
||||||
<div class="flex flex-col justify-between h-full">
|
<div class="flex flex-col justify-between h-full">
|
||||||
<div class="flex flex-col gap-[10px] sm:gap-[15px] w-full">
|
<div class="flex flex-col gap-[10px] sm:gap-[15px] w-full">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="w-[150px] sm:w-[260px] md:w-[330px] px-2 py-3 sm:py-5 sm:px-[15px] bg-block rounded-2xl flex flex-col items-center gap-[15px] sm:gap-[50px]">
|
class="w-[150px] sm:w-[260px] md:w-[330px] px-2 py-3 sm:py-5 sm:px-[15px] bg-block rounded-2xl flex flex-col items-center gap-[15px] sm:gap-[50px]">
|
||||||
<img :src="`https://www.yourgold.cz/api/public/file/${props.product?.cover_picture_uuid}.webp`" alt="Product Image"
|
<img :src="`/api/public/file/${props.product?.cover_picture_uuid}.webp`" alt="Product Image"
|
||||||
class="h-[95px] sm:h-[180px] md:h-[205px] rounded-[5px]"
|
class="h-[95px] sm:h-[180px] md:h-[205px] rounded-[5px]"
|
||||||
onerror="this.onerror=null; this.src='/photo.svg';" />
|
onerror="this.onerror=null; this.src='/photo.svg';" />
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm sm:text-xl font-normal">{{ menuStore.selectedPhoneCountry.call_prefix
|
<p class="text-sm sm:text-xl font-normal">{{ menuStore.selectedPhoneCountry.call_prefix
|
||||||
}}</p>
|
}}</p>
|
||||||
<input id="phone" :placeholder="$t('phone')" type="text"
|
<input id="phone" :placeholder="$t('phone')" type="text"
|
||||||
class="text-sm sm:text-xl placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0" />
|
class="text-sm sm:text-xl placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0" />
|
||||||
</div>
|
</div>
|
||||||
@ -112,14 +112,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="py-[25px] sm:py-12 border-b border-gray flex justify-center w-full">
|
<div class="py-[25px] sm:py-12 border-b border-gray flex justify-center w-full">
|
||||||
<UiButtonArrow type="fill" :arrow="true">{{ $t('login') }}</UiButtonArrow>
|
<UiButtonArrow type="fill" :arrow="true">{{ $t('sign_up') }}</UiButtonArrow>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-[25px] sm:mt-[30px] w-full flex justify-center gap-3">
|
<div class="mt-[25px] sm:mt-[30px] w-full flex justify-center gap-3">
|
||||||
<p class="cursor-pointer hover:underline transition-all">{{
|
<p class="cursor-pointer hover:underline transition-all">{{
|
||||||
$t('is_account')
|
$t('is_account')
|
||||||
}}</p>
|
}}</p>
|
||||||
<p class="text-button cursor-pointer hover:text-button-hover">{{
|
<p @click="menuStore.navigateToItem(menuStore.menuItems?.find((item) => item.id === 11))"
|
||||||
$t('login')
|
class="text-button cursor-pointer hover:text-button-hover">{{
|
||||||
|
$t('login')
|
||||||
}}</p>
|
}}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import type { GenericResponse } from "~/types";
|
import type { GenericResponse, GenericResponseItems, UserCart } from "~/types";
|
||||||
import type { AddressesList, UserAddressOfficial } from "~/types/checkout";
|
import type { AddressesList, UserAddressOfficial } from "~/types/checkout";
|
||||||
import { validation } from "../utils/validation";
|
import { validation } from "../utils/validation";
|
||||||
import { REGEX_PHONE } from "../utils/regex";
|
import { REGEX_PHONE } from "../utils/regex";
|
||||||
|
import type { CartProduct } from "~/types/product";
|
||||||
|
|
||||||
export const useCheckoutStore = defineStore("checkoutStore", () => {
|
export const useCheckoutStore = defineStore("checkoutStore", () => {
|
||||||
const { $toast } = useNuxtApp();
|
const { $toast } = useNuxtApp();
|
||||||
const menuStore = useMenuStore();
|
const menuStore = useMenuStore();
|
||||||
const selectedIso = ref(menuStore.selectedCountry);
|
const selectedIso = ref(menuStore.selectedCountry);
|
||||||
|
const showSummary = ref(false);
|
||||||
|
|
||||||
// get address list
|
// get address list
|
||||||
const addressesList = ref<AddressesList[]>();
|
const addressesList = ref<AddressesList[]>();
|
||||||
@ -185,6 +187,7 @@ export const useCheckoutStore = defineStore("checkoutStore", () => {
|
|||||||
dangerouslyHTMLString: true,
|
dangerouslyHTMLString: true,
|
||||||
});
|
});
|
||||||
// redirectToSummary();
|
// redirectToSummary();
|
||||||
|
showSummary.value = true;
|
||||||
} else {
|
} else {
|
||||||
$toast.error("Failed to send form. Please try again.", {
|
$toast.error("Failed to send form. Please try again.", {
|
||||||
autoClose: 5000,
|
autoClose: 5000,
|
||||||
@ -222,6 +225,108 @@ export const useCheckoutStore = defineStore("checkoutStore", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get user cart
|
||||||
|
const products = ref<CartProduct[]>();
|
||||||
|
const fullPrice = ref();
|
||||||
|
const fullProductsPrice = ref();
|
||||||
|
async function getUserCart() {
|
||||||
|
try {
|
||||||
|
const { data } = await useMyFetch<GenericResponse<UserCart>>(
|
||||||
|
`/api/public/user/cart`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
onErrorOccured: async (_, status) => {
|
||||||
|
throw createError({
|
||||||
|
statusCode: status,
|
||||||
|
statusMessage: `HTTP error: ${status}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
products.value = data.cart_items;
|
||||||
|
fullPrice.value = data.total_value;
|
||||||
|
fullProductsPrice.value = data.total_value;
|
||||||
|
fullPrice.value = Number(fullPrice.value) + Number(shippingPrice.value);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("getUserCart error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get delivery options
|
||||||
|
const deliveryOption = ref();
|
||||||
|
const currentDelivery = ref();
|
||||||
|
const shippingPrice = ref();
|
||||||
|
async function getDeliveryOptions() {
|
||||||
|
try {
|
||||||
|
const { data } = await useMyFetch<
|
||||||
|
GenericResponseItems<{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
country_iso: string;
|
||||||
|
country_name: string;
|
||||||
|
delivery_supplier_id: number;
|
||||||
|
delivery_supplier_name: string;
|
||||||
|
id: number;
|
||||||
|
shippment_price: string;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}>
|
||||||
|
>(`/api/restricted/cart/checkout/delivery-options`, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
onErrorOccured: async (_, status) => {
|
||||||
|
throw createError({
|
||||||
|
statusCode: status,
|
||||||
|
statusMessage: `HTTP error: ${status}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
deliveryOption.value = data.items;
|
||||||
|
currentDelivery.value = data.items[0];
|
||||||
|
shippingPrice.value = data.items[0].shippment_price;
|
||||||
|
fullPrice.value = Number(fullPrice.value) + Number(shippingPrice.value);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("getUserCart error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultAddress = ref();
|
||||||
|
async function getDefAddress() {
|
||||||
|
try {
|
||||||
|
const { data } = await useMyFetch<
|
||||||
|
GenericResponse<{
|
||||||
|
addresses: [
|
||||||
|
{
|
||||||
|
is_default: string;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}>
|
||||||
|
>(`/api/public/user`, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
onErrorOccured: async (_, status) => {
|
||||||
|
throw createError({
|
||||||
|
statusCode: status,
|
||||||
|
statusMessage: `HTTP error: ${status}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
defaultAddress.value = data.addresses.find(
|
||||||
|
(el: any) => el.is_default === true
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("getUserCart error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
addressesList,
|
addressesList,
|
||||||
activeAddress,
|
activeAddress,
|
||||||
@ -246,6 +351,15 @@ export const useCheckoutStore = defineStore("checkoutStore", () => {
|
|||||||
vNewAddressCode,
|
vNewAddressCode,
|
||||||
vNewAddressCity,
|
vNewAddressCity,
|
||||||
vNewAddressCountry,
|
vNewAddressCountry,
|
||||||
|
showSummary,
|
||||||
|
|
||||||
|
products,
|
||||||
|
fullPrice,
|
||||||
|
fullProductsPrice,
|
||||||
|
deliveryOption,
|
||||||
|
currentDelivery,
|
||||||
|
shippingPrice,
|
||||||
|
defaultAddress,
|
||||||
|
|
||||||
changePrefix,
|
changePrefix,
|
||||||
getCheckout,
|
getCheckout,
|
||||||
@ -254,5 +368,8 @@ export const useCheckoutStore = defineStore("checkoutStore", () => {
|
|||||||
changeActive,
|
changeActive,
|
||||||
uploadAddress,
|
uploadAddress,
|
||||||
sendForm,
|
sendForm,
|
||||||
|
getUserCart,
|
||||||
|
getDeliveryOptions,
|
||||||
|
getDefAddress,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -11,3 +11,22 @@ export interface Product {
|
|||||||
applied_tax_rate: number;
|
applied_tax_rate: number;
|
||||||
tax_name: string;
|
tax_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CartProduct {
|
||||||
|
cart_item_id: number;
|
||||||
|
link_rewrite: string;
|
||||||
|
name: string;
|
||||||
|
picture_uuid: string;
|
||||||
|
product_id: number;
|
||||||
|
quantity: number;
|
||||||
|
single_item_price: number;
|
||||||
|
total_price: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserCart {
|
||||||
|
cart_items: CartProduct[];
|
||||||
|
checkout_in_progress: boolean;
|
||||||
|
currency_iso: string;
|
||||||
|
id: number;
|
||||||
|
total_value: number;
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user