28 Commits

Author SHA1 Message Date
de61e08ac9 sync 2025-07-04 16:34:43 +02:00
af0a3df068 product page 2025-07-04 13:00:53 +02:00
cd9420ba49 Merge pull request 'fix: fix links' (#3) from linter into main
Reviewed-on: #3
2025-07-04 10:27:43 +00:00
de4518367b fix: fix links 2025-07-04 12:26:32 +02:00
f80bc3e3ba some changeds 2025-07-04 10:01:46 +02:00
8a4d835c33 Merge pull request 'fix hydration errors' (#2) from linter into main
Reviewed-on: #2
2025-07-03 16:36:04 +00:00
3b02ccbe80 fix hydration errors 2025-07-03 18:34:02 +02:00
9063846282 fix eslint 2025-07-03 15:24:28 +02:00
e935e9f911 conflicts 2025-07-03 13:24:45 +02:00
5d59059474 summary checkout 2025-07-03 13:21:24 +02:00
90e1d70f64 product page and linter 2025-07-03 11:13:42 +02:00
fbb6c071af change footer information, add lei certificate 2025-07-02 15:17:12 +02:00
687e92429f remove pocketbase 2025-07-01 15:56:40 +02:00
29f8582e47 button with image 2025-07-01 15:52:48 +02:00
ea72074559 registration 2025-07-01 13:35:37 +02:00
fd4b122936 login/checkout 2025-06-30 15:27:46 +02:00
012058b998 login 2025-06-27 16:02:00 +02:00
96dbc38c3a button with image 2025-06-26 21:04:01 +02:00
8bab93274b cart 2025-06-26 13:25:30 +02:00
10b9610918 translations 2025-06-26 09:31:08 +02:00
edf3036e6a fix langs 2025-06-26 03:21:46 +02:00
9d7fd3d52a fix langs 2025-06-25 15:55:50 +02:00
4fc12ff9bf translations 2025-06-25 13:53:36 +02:00
9407253e69 fix data 2025-06-25 12:43:42 +02:00
98a4125804 cart pop-up 2025-06-24 15:53:07 +02:00
5de09aa13b Merge branch 'fix_fetch' 2025-06-24 14:18:12 +02:00
7cc292296b fix shop 2025-06-24 14:14:40 +02:00
a000f966eb fix fetch 2025-06-24 12:05:43 +02:00
105 changed files with 17193 additions and 7980 deletions

13
.husky/hooks/pre-commit Executable file
View File

@ -0,0 +1,13 @@
#!/bin/sh
command_name_bun="bun"
if ! command -v "$command_name_bun" &> /dev/null; then
echo "Command '$command_name_bun' not found. Installing..."
npm install -g "$command_name_bun"@latest
else
"$command_name_bun" lint || exit 1 && cd ..
fi

View File

@ -9,4 +9,9 @@
</UApp>
</div>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
const userStore = useUserStore()
await userStore.checkIsLogged()
</script>

View File

@ -1,7 +0,0 @@
@font-face {
font-family: "Bounded";
src: url("/fonts/Bounded/Bounded-Variable.ttf") format("truetype");
font-weight: 100 900;
font-style: normal;
font-display: swap;
}

View File

@ -1,11 +1,11 @@
@import "@nuxt/ui";
@import "tailwindcss";
@import '@nuxt/ui';
@import 'tailwindcss';
@import '@iconscout/unicons/css/line.css';
@theme {
/* fonts */
--font-inter: "Inter", sans-serif;
--font-bounded: "Bounded", sans-serif;
--font-inter: 'Inter', sans-serif;
--font-bounded: 'Bounded', sans-serif;
/* colors */
--color-bg-light: #fffefb;
@ -16,12 +16,12 @@
--color-text-light: #1a1a1a;
--color-text-dark: #fffefb;
--color-block: #E8E7E0;
--color-block: #e8e7e0;
--color-accent-green-light: #004F3D;
--color-accent-green-light: #004f3d;
--color-accent-green-dark: #008567;
--color-gray: #6B6B6B;
--color-gray: #6b6b6b;
/* button */
--color-button: #9a7f62;
--color-button-hover: #b7946d;
@ -54,19 +54,19 @@
}
.space-25-55-75 {
@apply space-y-[25px] sm:space-y-[55px] md:space-y-[75px]
@apply space-y-[25px] sm:space-y-[55px] md:space-y-[75px];
}
.space-25-55 {
@apply space-y-[25px] sm:space-y-[55px]
@apply space-y-[25px] sm:space-y-[55px];
}
.space-25-75 {
@apply space-y-[25px] sm:space-y-[75px]
@apply space-y-[25px] sm:space-y-[75px];
}
.space-55-75 {
@apply space-y-[55px] sm:space-y-[75px]
@apply space-y-[55px] sm:space-y-[75px];
}
}

View File

@ -0,0 +1,51 @@
/* ===== Base toast style (applies to all toasts) ===== */
.Toastify__toast {
font-family: var(--font-inter);
background-color: var(--color-bg-light);
border: 2px solid var(--color-block);
color: var(--color-bg-dark);
border-radius: 8px;
box-shadow: none;
}
/* ===== Base toast style for dark mode ===== */
.dark .Toastify__toast {
background-color: var(--color-bg-dark);
border: 1px solid var(--color-block);
color: #ffffff;
}
/* ===== Error toast: red background and white text ===== */
.Toastify__toast--error {
background-color: var(--color-bg-light);
}
/* ===== Error toast in dark mode: darker red background ===== */
.dark .Toastify__toast--error {
background-color: var(--color-bg-dark);
}
/* ===== Default progress bar color (used for success and others) ===== */
.Toastify__progress-bar {
background-color: var(--color-accent-green-dark);
}
/* ===== Error toast: custom progress bar color ===== */
.Toastify__toast--error .Toastify__progress-bar {
background-color: #dc3545;
}
/* ===== Success toast: icon color ===== */
.Toastify__toast--success .Toastify__toast-icon svg {
fill: var(--color-accent-green-dark);
}
/* ===== Error toast: icon color ===== */
.Toastify__toast--error .Toastify__toast-icon svg {
fill: #dc3545;
}
/* ===== Close button color in dark mode ===== */
.dark .Toastify__close-button {
color: #fff;
}

View File

@ -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);
}
}

View File

@ -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
}

View File

@ -1,8 +0,0 @@
package cloudflare
import (
"embed"
)
//go:embed TurnStilleCaptcha.js
var JS embed.FS

View File

@ -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
}

View File

@ -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
}

View File

@ -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);
}
}

View File

@ -1,8 +0,0 @@
package gtm
import (
"embed"
)
//go:embed gtm.js
var JS embed.FS

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}
}
}()
}

View File

@ -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,
})
})
}

View File

@ -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
}

View File

@ -1,9 +0,0 @@
package webpconverter
import "bytes"
type readSeekCloser struct {
*bytes.Reader
}
func (r readSeekCloser) Close() error { return nil }

View File

@ -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
}

View File

@ -1,42 +0,0 @@
module pocketbase
go 1.24.0
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
)

View File

@ -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=

View File

@ -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)
}
}

3108
bun.lock Executable file

File diff suppressed because it is too large Load Diff

BIN
bun.lockb Executable file

Binary file not shown.

View File

@ -1,31 +1,111 @@
<template>
<div class="w-full p-[50px] bg-bg-light dark:bg-bg-dark border border-button rounded-2xl h-full">
<div class="flex items-center h-[205px]" v-if="productStore.productList">
<div class="w-[205px] h-full flex items-center justify-center">
<img :src="`https://www.yourgold.cz/api/public/file/${productStore.productList[0]?.cover_picture_uuid}.webp`"
alt="pics" class="w-auto h-full" />
<div ref="dropdownRef">
<div class="relative cursor-pointer" @click="openCart = !openCart">
<i class="uil uil-shopping-cart text-[31px]"></i>
<div v-if="checkoutStore.products && checkoutStore.products.length > 0"
class="w-[15px] h-[15px] rounded-full bg-accent-green-light dark:bg-accent-green-light text-white flex items-center justify-center text-[9px] absolute top-1 right-0">
{{ checkoutStore.products.length }}
</div>
</div>
<div v-if="openCart"
class="absolute left-1/2 transform -translate-x-1/2 w-full px-4 sm:max-w-[768px] sm:px-[17px] md:max-w-[1000px] md:px-6 xl:max-w-[1920px] xl:px-20 right-0 z-50 flex items-center justify-end top-[100px] sm:top-[100px] md:top-[140px]"
@click.self="openCart = !openCart">
<div class="xl:w-[55%] md:w-[90%] w-full px-4 md:px-0">
<div v-if="checkoutStore.products && checkoutStore.products.length > 0"
class="w-full p-[25px] sm:p-[50px] bg-bg-light dark:bg-bg-dark border border-button rounded-xl sm:rounded-[32px] h-full space-25-55">
<div>
<div v-for="(item, index) in checkoutStore.products" :key="index"
class="py-[13px] sm:py-[25px] first:pt-0 border-b border-block">
<div class="flex items-center h-[100px] sm:h-[205px]">
<div class="min-w-[100px] sm:min-w-[205px] flex items-center justify-center h-[100px] sm:h-[205px]">
<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 h-full w-full gap-[7px] sm:gap-[15px] max-w-[250px]"
@click="productStore.addToCart(productStore.productList[0])">
<h3
class="text-[10px] sm:text-base md:text-lg text-xl font-bold leading-[130%] sm:leading-[150%] text-bg-dark">
{{ productStore.productList[0]?.name }}
</h3>
<div class="">
<p
class="text-accent-green-light font-inter text-[12px] sm:text-[21px] md:text-2xl leading-[150%] font-bold">
{{ productStore.productList[0]?.formatted_price }}
</p>
<div class="">
<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>
<i class="uil uil-trash-alt text-lg sm:text-2xl cursor-pointer"
@click="productStore.deleteCartItem(item.cart_item_id)"></i>
</div>
<div class="flex flex-col 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">
{{ menuStore.formatPrice(Number(item.total_price)) }}
</p>
<div class="flex items-center gap-[2px] sm:gap-2 text-xl">
<div
class="w-5 min-h-5 sm:w-11 sm:min-h-11 text-[10px] sm:text-lg flex items-center justify-center">
<i class="uil uil-minus cursor-pointer text-gray dark:text-button-disabled hover:text-gray-200 transition-all"
@click="productStore.decrementCartItem(item.cart_item_id)"></i>
</div>
<div
class="w-5 min-h-5 sm:w-10 sm:min-h-11 text-[10px] sm:text-xl border border-button flex items-center justify-center rounded-[4px]">
{{ item.quantity }}
</div>
<div
class="w-5 min-h-5 sm:w-11 sm:min-h-11 text-[10px] sm:text-lg flex items-center justify-center">
<i class="uil uil-plus cursor-pointer hover:text-gray-200 transition-all"
@click="productStore.incrementCartItem(item.product_id)"></i>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="flex items-center justify-between">
<h4 class="font-inter text-[12px] leading-[150%] font-bold uppercase sm:text-[24px]">
{{
$t('total_amount') }}
</h4>
<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">
{{ menuStore.formatPrice(Number(checkoutStore.fullProductsPrice)) }}
</p>
</div>
<UiButtonArrow class="w-full" type="fill" :arrow="true" :full="true" @click="() => {
if (userStore.isLogged) {
menuStore.navigateToItem(menuStore.menuItems?.find((item) => item.id === 12))
}
else {
menuStore.navigateToItem(menuStore.menuItems?.find((item) => item.id === 11))
}
openCart = false
}">
{{ userStore.isLogged
? $t('to_checkout') : $t('login') }}
</UiButtonArrow>
</div>
<div v-else
class="w-full p-[25px] sm:p-[50px] bg-bg-light dark:bg-bg-dark border border-button rounded-xl sm:rounded-[32px] flex items-center justify-center">
<div
class="border border-block inline-flex items-center justify-center h-[140px] sm:h-[200px] text-center rounded-[8px]">
<h4 class="font-inter text-base leading-[150%] font-bold uppercase sm:text-[20px] px-4 sm:px-10 md:text-xl">
{{ $t('empty_cart') }}
</h4>
</div>
</div>
</div>
</div>
<UiButtonArrow class="w-full" type="fill" :arrow="true">Přejít k pokladně</UiButtonArrow>
</div>
</template>
<script lang="ts" setup>
import { onClickOutside } from '@vueuse/core'
const productStore = useProductStore()
const checkoutStore = useCheckoutStore()
const openCart = ref(false)
const menuStore = useMenuStore()
const userStore = useUserStore()
const dropdownRef = ref(null)
onClickOutside(dropdownRef, () => {
openCart.value = false
})
</script>

View File

@ -1,34 +1,64 @@
<template>
<div class="flex flex-col gap-2 relative" ref="dropdownRef">
<UButton color="neutral" variant="subtle" trailing-icon="i-lucide-chevron-down"
<div ref="dropdownRef"
class="flex flex-col gap-2 relative"
>
<UButton
color="neutral"
variant="subtle"
trailing-icon="i-lucide-chevron-down"
class="bg-bg-light dark:bg-bg-dark m-0 ring-0 text-xl font-medium uppercase cursor-pointer hover:bg-inherit text-text-light dark:text-text-dark"
block @click="isOpen = !isOpen">
{{ menuStore.selectedCountry }}/{{ menuStore.selectedCurrency?.iso_code }}
block
@click="isOpen = !isOpen"
>
{{ $session.cookieData.value.country.iso_code }}/{{
$session.cookieData.value.currency.iso_code
}}
</UButton>
<div class="absolute ring-0 top-12 p-0 m-0 border-none w-48" v-if="isOpen">
<div class="border border-button px-4 py-[10px] rounded-[5px] bg-bg-light dark:bg-bg-dark">
<div
v-if="isOpen"
class="absolute ring-0 top-12 p-0 m-0 border-none w-48 z-50"
>
<div
class="border border-button px-4 py-[10px] rounded-[5px] bg-bg-light dark:bg-bg-dark"
>
<div class="p-0 flex flex-col gap-4 bg-bg-light dark:bg-bg-dark">
<div class="flex flex-col items-start gap-1">
<p>{{ $t("country") }}</p>
<div
class="bg-inherit w-full ring-0 cursor-pointer focus:ring-0 outline-none focus-visible:ring-0 space-y-1">
<div class="p-0" @click="openDrop('country')">
<div class="flex items-center gap-2 text-xl font-medium uppercase text-text-light dark:text-text-dark">
{{ menuStore.selectedCountry }} <span> <i
class="uil uil-angle-down text-2xl font-light cursor-pointer"></i></span>
class="w-full ring-0 cursor-pointer focus:ring-0 outline-none focus-visible:ring-0 space-y-1"
>
<div class="p-0"
@click="openDrop('country')"
>
<div
class="flex items-center gap-2 text-xl font-medium uppercase text-text-light dark:text-text-dark"
>
{{ $session.currentCountryIso }}
<span>
<i
class="uil uil-angle-down text-2xl font-light cursor-pointer"
/></span>
</div>
</div>
<div v-if="dropCountry"
class="rounded-[5px] data-highlighted:not-data-disabled:before:bg-button/50 bg-inherit ring-0 cursor-pointer w-full focus:ring-0 outline-none focus-visible:ring-0 border border-button py-[10px] px-[5px]">
<div
v-if="dropCountry"
class="bg-bg-light dark:bg-bg-dark rounded-[5px] data-highlighted:not-data-disabled:before:bg-button/50 ring-0 cursor-pointer w-full focus:ring-0 outline-none focus-visible:ring-0 border border-button py-[10px] px-[5px]"
>
<div class="overflow-auto h-[200px] w-full">
<p @click="() => {
menuStore.selectedCountry = item.iso_code
dropCountry = false
}"
<p
v-for="item in menuStore.countries"
:key="item.iso_code"
class="w-full hover:bg-block dark:hover:bg-button pl-2 py-2 text-base text-text-light dark:text-text-dark rounded-[5px]"
v-for="item in menuStore.countryList">
@click="
() => {
$session.setCountry(item.iso_code);
$session.loadSession();
dropCountry = false;
}
"
>
{{ item?.name }}
</p>
</div>
@ -38,23 +68,39 @@
<div class="flex flex-col items-start gap-[6px]">
<p>{{ $t("currency") }}</p>
<div
class="bg-inherit w-full ring-0 cursor-pointer focus:ring-0 outline-none focus-visible:ring-0 space-y-1">
<div class="p-0" @click="openDrop()">
<div class="flex items-center gap-2 text-xl font-medium uppercase text-text-light dark:text-text-dark">
{{ menuStore.selectedCurrency?.iso_code }}<span> <i
class="uil uil-angle-down text-2xl font-light cursor-pointer"></i></span>
class="w-full ring-0 cursor-pointer focus:ring-0 outline-none focus-visible:ring-0 space-y-1"
>
<div class="p-0"
@click="openDrop()"
>
<div
class="flex items-center gap-2 text-xl font-medium uppercase text-text-light dark:text-text-dark"
>
{{ $session.currentCurrencyIso
}}<span>
<i
class="uil uil-angle-down text-2xl font-light cursor-pointer"
/></span>
</div>
</div>
<div v-if="dropCurrency"
class="rounded-[5px] data-highlighted:not-data-disabled:before:bg-button/50 bg-inherit ring-0 cursor-pointer w-full focus:ring-0 outline-none focus-visible:ring-0 border border-button py-[10px] px-[5px]">
<div
v-if="dropCurrency"
class="bg-bg-light dark:bg-bg-dark rounded-[5px] data-highlighted:not-data-disabled:before:bg-button/50 ring-0 cursor-pointer w-full focus:ring-0 outline-none focus-visible:ring-0 border border-button py-[10px] px-[5px]"
>
<div class="overflow-auto w-full">
<p @click="() => {
menuStore.selectedCurrency = item
dropCurrency = false
}"
<p
v-for="item in menuStore.currencies"
:key="item.iso_code"
class="w-full hover:bg-block dark:hover:bg-button pl-1 py-2 text-base text-text-light dark:text-text-dark rounded-[5px]"
v-for="item in menuStore.currencies">
@click="
() => {
$session.setCurrency(item.iso_code);
$session.loadSession();
dropCurrency = false;
}
"
>
{{ item?.name }}
</p>
</div>
@ -68,11 +114,14 @@
</template>
<script setup lang="ts">
import { onClickOutside } from "@vueuse/core";
const isOpen = ref(false);
const menuStore = useMenuStore();
import { onClickOutside } from '@vueuse/core'
const dropdownRef = ref(null);
const { $session } = useNuxtApp()
const isOpen = ref(false)
const menuStore = useMenuStore()
const dropdownRef = ref(null)
const dropCountry = ref(false)
const dropCurrency = ref(false)
@ -81,32 +130,16 @@ function openDrop(type?: string) {
if (type === 'country') {
dropCurrency.value = false
dropCountry.value = !dropCountry.value
} else {
}
else {
dropCountry.value = false
dropCurrency.value = !dropCurrency.value
}
}
watchEffect(() => {
if (
!menuStore.selectedCountry &&
menuStore.countryList &&
menuStore.countryList.length > 0
) {
menuStore.selectedCountry = menuStore.countryList[0].iso_code;
}
if (
!menuStore.selectedCurrency &&
menuStore.currencies &&
menuStore.currencies.length > 0
) {
menuStore.selectedCurrency = menuStore.currencies[0];
}
});
onClickOutside(dropdownRef, () => {
isOpen.value = false
dropCurrency.value = false
dropCountry.value = false
});
})
</script>

View File

@ -4,81 +4,154 @@
<div class="w-full border-b border-border">
<UiContainer class="relative">
<div class="hidden h-[120px] w-full items-center gap-[145px] xl:flex">
<ul class="flex items-center justify-between whitespace-nowrap w-full">
<li v-for="(item, index) in menuStore.menu" @click="menuStore.navigateToItem(item)" :key="index"
:class="['hover:text-accent-green-light dark:hover:text-accent-green-dark cursor-pointer text-lg transition-all text-inter', route.params.slug === item.link_rewrite && 'text-accent-green-light dark:text-accent-green-dark font-bold underline']">
0{{ index + 1 }} <br />
{{ item.name }}
<ul
class="flex items-center justify-between gap-5 whitespace-nowrap w-full"
>
<li
v-for="(item, index) in menuStore.menu"
:key="item.id"
:class="[
'hover:text-accent-green-light dark:hover:text-accent-green-dark cursor-pointer text-lg transition-all text-inter',
route.params.id == item.id.toString()
? 'text-accent-green-light dark:text-accent-green-dark font-bold underline'
: false,
]"
@click="menuStore.navigateToItem(item)"
>
0{{ index + 1 }} <br>
{{ item.front_menu_lang[0].name }}
</li>
</ul>
<ClientOnly v-if="!colorMode?.forced">
<img class="cursor-pointer" :src="isDark ? '/logo-dark.svg' : '/logo.svg'" alt="logo"
@click="menuStore.navigateToItem()" />
<img
class="cursor-pointer"
:src="isDark ? '/logo-dark.svg' : '/logo.svg'"
alt="logo"
@click="menuStore.navigateToItem()"
>
</ClientOnly>
<div class="w-full flex items-center justify-between">
<div class="flex items-center gap-[30px]">
<i class="uil uil-user text-[31px] cursor-pointer"></i>
<i @click="openCart = !openCart" class="uil uil-shopping-cart text-[31px] cursor-pointer"></i>
<div>
<i v-if="!userStore.isLogged"
data-testid="login-icon"
class="uil uil-user text-[31px] cursor-pointer"
@click="menuStore.navigateToItem(menuStore.menuItems?.find((item) => item.id === 11))"></i>
<div v-else class="py-[6px] px-3 border border-block rounded-sm">
{{ userStore.user }}
</div>
</div>
<CartPopup />
</div>
<div class="flex">
<LangSwitcher />
<CountryCurrencySelector />
</div>
<ThemeSwitcher />
<button @click="menuStore.navigateToShop"
class="hover:bg-button-hover bg-button cursor-pointer rounded-xl px-6 py-3 font-medium text-white transition-all text-inter">
{{ $t('eshop') }}
<button
:class="[
'cursor-pointer transition-all text-inter whitespace-nowrap',
route.params.id == '5'
? 'text-accent-green-light dark:text-accent-green-dark font-bold pb-1 border-b-2'
: 'hover:bg-button-hover bg-button text-white font-medium rounded-xl px-6 py-3',
]"
@click="menuStore.navigateToShop"
>
{{ $t("eshop") }}
</button>
</div>
<div v-if="openCart" class="max-w-[1067px] w-full absolute top-[130px] z-50 right-20">
<CartPopup />
</div>
</div>
</UiContainer>
</div>
<!-- md -->
<div class="hidden w-full md:flex md:flex-col xl:hidden items-center justify-center">
<div
class="hidden w-full md:flex md:flex-col xl:hidden items-center justify-center"
>
<div class="w-full border-border border-b">
<UiContainer class="h-[116px] flex items-center justify-between">
<ClientOnly v-if="!colorMode?.forced">
<img class="cursor-pointer" :src="isDark ? '/logo-dark.svg' : '/logo.svg'" alt="logo"
@click="menuStore.navigateToItem()" />
<img
class="cursor-pointer"
:src="isDark ? '/logo-dark.svg' : '/logo.svg'"
alt="logo"
@click="menuStore.navigateToItem()"
>
</ClientOnly>
<div class="flex items-center gap-6">
<div class="flex items-center gap-[30px]">
<i class="uil uil-user text-[31px] cursor-pointer"></i>
<i class="uil uil-shopping-cart text-[31px] cursor-pointer"></i>
<div>
<i
v-if="!userStore.isLogged"
class="uil uil-user text-[31px] cursor-pointer"
@click="
menuStore.navigateToItem(
menuStore.menuItems?.find((item) => item.id === 11),
)
"
/>
<div
v-else
class="py-[6px] px-3 border border-block rounded-sm"
>
{{ userStore.user }}
</div>
</div>
<CartPopup />
</div>
<div class="flex">
<LangSwitcher />
<CountryCurrencySelector />
</div>
<ThemeSwitcher />
<i variant="subtle" block class="uil uil-apps text-[33px] cursor-pointer" @click="open = !open"></i>
<i
variant="subtle"
block
class="uil uil-apps text-[33px] cursor-pointer"
@click="open = !open"
/>
</div>
</UiContainer>
</div>
<UCollapsible :ui="{ content: 'w-full' }" v-model:open="open" class="w-full">
<UCollapsible
v-model:open="open"
:ui="{ content: 'w-full' }"
class="w-full"
>
<template #content>
<div class="w-full border-border border-b pt-6 pb-8">
<UiContainer class="flex flex-col gap-[30px]">
<div v-for="(item, index) in menuStore.menu" @click="
<div
v-for="(item, index) in menuStore.menu"
:key="index"
:class="[
'flex items-center justify-between transition-all hover:text-accent-green-light dark:hover:text-accent-green-dark cursor-pointer',
route.params.slug === item.front_menu_lang[0].link_rewrite
&& 'text-accent-green-light dark:text-accent-green-dark font-bold underline',
]"
@click="
() => {
menuStore.navigateToItem(item);
open = false;
}
" :key="index"
:class="['flex items-center justify-between transition-all hover:text-accent-green-light dark:hover:text-accent-green-dark cursor-pointer', route.params.slug === item.link_rewrite && 'text-accent-green-light dark:text-accent-green-dark font-bold underline']">
"
>
<div class="leading-[70%] text-inter">
<span class="mr-4">0{{ index + 1 }}</span>
{{ item.name }}
{{ item.front_menu_lang[0].name }}
</div>
<!-- <i class="uil uil-arrow-up-right text-[35px]"></i> -->
<svg class="" width="20" height="20" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
width="20"
height="20"
viewBox="0 0 26 26"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M25.1274 1.87258C25.1274 1.3203 24.6797 0.872582 24.1274 0.872584L15.1274 0.872583C14.5751 0.872583 14.1274 1.3203 14.1274 1.87258C14.1274 2.42487 14.5751 2.87258 15.1274 2.87258L23.1274 2.87258L23.1274 10.8726C23.1274 11.4249 23.5751 11.8726 24.1274 11.8726C24.6797 11.8726 25.1274 11.4249 25.1274 10.8726L25.1274 1.87258ZM1.5 24.5L2.20711 25.2071L24.8345 2.57969L24.1274 1.87258L23.4203 1.16548L0.792893 23.7929L1.5 24.5Z"
fill="currentColor" />
fill="currentColor"
/>
</svg>
</div>
</UiContainer>
@ -88,42 +161,88 @@
</div>
<!-- sm -->
<div class="hidden w-full items-center justify-between sm:flex sm:flex-col md:hidden">
<div
class="hidden w-full items-center justify-between sm:flex sm:flex-col md:hidden"
>
<div class="w-full border-border border-b">
<UiContainer class="h-[84px] flex items-center justify-between">
<ClientOnly v-if="!colorMode?.forced">
<img class="cursor-pointer" :src="isDark ? '/logo-dark.svg' : '/logo.svg'" alt="logo"
@click="menuStore.navigateToItem()" />
<img
class="cursor-pointer"
:src="isDark ? '/logo-dark.svg' : '/logo.svg'"
alt="logo"
@click="menuStore.navigateToItem()"
>
</ClientOnly>
<div class="flex items-center gap-6">
<div class="flex items-center gap-[30px]">
<i class="uil uil-user text-[31px] cursor-pointer"></i>
<i class="uil uil-shopping-cart text-[31px] cursor-pointer"></i>
<div>
<i
v-if="!userStore.isLogged"
class="uil uil-user text-[31px] cursor-pointer"
@click="
menuStore.navigateToItem(
menuStore.menuItems?.find((item) => item.id === 11),
)
"
/>
<div
v-else
class="py-[6px] px-3 border border-block rounded-sm"
>
{{ userStore.user }}
</div>
<i variant="subtle" block class="uil uil-apps text-[30px] cursor-pointer" @click="open = !open"></i>
</div>
<CartPopup />
</div>
<i
variant="subtle"
block
class="uil uil-apps text-[30px] cursor-pointer"
@click="open = !open"
/>
</div>
</UiContainer>
</div>
<UCollapsible :ui="{ content: 'w-full' }" v-model:open="open" class="w-full">
<UCollapsible
v-model:open="open"
:ui="{ content: 'w-full' }"
class="w-full"
>
<template #content>
<div class="w-full border-border border-b pt-6 pb-8">
<UiContainer class="flex flex-col gap-[30px]">
<div v-for="(item, index) in menuStore.menu" @click="
<div
v-for="(item, index) in menuStore.menu"
:key="index"
:class="[
'flex items-center justify-between transition-all hover:text-accent-green-light dark:hover:text-accent-green-dark cursor-pointer',
route.params.slug === item.front_menu_lang[0].link_rewrite
&& 'text-accent-green-light dark:text-accent-green-dark font-bold underline',
]"
@click="
() => {
menuStore.navigateToItem(item);
open = false;
}
" :key="index"
:class="['flex items-center justify-between transition-all hover:text-accent-green-light dark:hover:text-accent-green-dark cursor-pointer', route.params.slug === item.link_rewrite && 'text-accent-green-light dark:text-accent-green-dark font-bold underline']">
"
>
<div class="leading-[70%] text-inter">
<span class="mr-4">0{{ index + 1 }}</span>
{{ item.name }}
{{ item.front_menu_lang[0].name }}
</div>
<!-- <i class="uil uil-arrow-up-right text-[35px]"></i> -->
<svg class="" width="20" height="20" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
width="20"
height="20"
viewBox="0 0 26 26"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M25.1274 1.87258C25.1274 1.3203 24.6797 0.872582 24.1274 0.872584L15.1274 0.872583C14.5751 0.872583 14.1274 1.3203 14.1274 1.87258C14.1274 2.42487 14.5751 2.87258 15.1274 2.87258L23.1274 2.87258L23.1274 10.8726C23.1274 11.4249 23.5751 11.8726 24.1274 11.8726C24.6797 11.8726 25.1274 11.4249 25.1274 10.8726L25.1274 1.87258ZM1.5 24.5L2.20711 25.2071L24.8345 2.57969L24.1274 1.87258L23.4203 1.16548L0.792893 23.7929L1.5 24.5Z"
fill="currentColor" />
fill="currentColor"
/>
</svg>
</div>
<div class="flex items-center justify-between">
@ -134,7 +253,7 @@
</div>
<div class="flex items-center justify-between">
<p class="leading-[70%] text-inter">
{{ $t("change_currency_country") }}
{{ $t("change_currency_and_country") }}
</p>
<CountryCurrencySelector />
</div>
@ -155,35 +274,74 @@
<div class="w-full border-border border-b">
<UiContainer class="h-[84px] flex items-center justify-between">
<ClientOnly v-if="!colorMode?.forced">
<img class="cursor-pointer" :src="isDark ? '/logo-dark.svg' : '/logo.svg'" alt="logo"
@click="menuStore.navigateToItem()" />
<img
class="cursor-pointer"
:src="isDark ? '/logo-dark.svg' : '/logo.svg'"
alt="logo"
@click="menuStore.navigateToItem()"
>
</ClientOnly>
<div class="flex items-center gap-6">
<i class="uil uil-shopping-cart text-[31px] cursor-pointer"></i>
<i variant="subtle" block class="uil uil-apps text-[30px] cursor-pointer" @click="open = !open"></i>
<div>
<i
class="uil uil-user text-[30px] cursor-pointer"
@click="
!userStore.isLogged
&& menuStore.navigateToItem(
menuStore.menuItems?.find((item) => item.id === 11),
)
"
/>
</div>
<CartPopup />
<i
variant="subtle"
block
class="uil uil-apps text-[30px] cursor-pointer"
@click="open = !open"
/>
</div>
</UiContainer>
</div>
<UCollapsible :ui="{ content: 'w-full' }" v-model:open="open" class="w-full">
<UCollapsible
v-model:open="open"
:ui="{ content: 'w-full' }"
class="w-full"
>
<template #content>
<div class="w-full border-border border-b pt-6 pb-8">
<UiContainer class="flex flex-col gap-[30px]">
<div v-for="(item, index) in menuStore.menu" @click="
<div
v-for="(item, index) in menuStore.menu"
:key="index"
:class="[
'flex items-center justify-between transition-all hover:text-accent-green-light dark:hover:text-accent-green-dark cursor-pointer',
route.params.slug === item.front_menu_lang[0].link_rewrite
&& 'text-accent-green-light dark:text-accent-green-dark font-bold underline',
]"
@click="
() => {
menuStore.navigateToItem(item);
open = false;
}
" :key="index"
:class="['flex items-center justify-between transition-all hover:text-accent-green-light dark:hover:text-accent-green-dark cursor-pointer', route.params.slug === item.link_rewrite && 'text-accent-green-light dark:text-accent-green-dark font-bold underline']">
"
>
<div class="leading-[70%] text-inter">
<span class="mr-4">0{{ index + 1 }}</span>
{{ item.name }}
{{ item.front_menu_lang[0].name }}
</div>
<!-- <i class="uil uil-arrow-up-right text-[35px]"></i> -->
<svg class="" width="20" height="20" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
width="20"
height="20"
viewBox="0 0 26 26"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M25.1274 1.87258C25.1274 1.3203 24.6797 0.872582 24.1274 0.872584L15.1274 0.872583C14.5751 0.872583 14.1274 1.3203 14.1274 1.87258C14.1274 2.42487 14.5751 2.87258 15.1274 2.87258L23.1274 2.87258L23.1274 10.8726C23.1274 11.4249 23.5751 11.8726 24.1274 11.8726C24.6797 11.8726 25.1274 11.4249 25.1274 10.8726L25.1274 1.87258ZM1.5 24.5L2.20711 25.2071L24.8345 2.57969L24.1274 1.87258L23.4203 1.16548L0.792893 23.7929L1.5 24.5Z"
fill="currentColor" />
fill="currentColor"
/>
</svg>
</div>
<div class="flex items-center justify-between">
@ -194,7 +352,7 @@
</div>
<div class="flex items-center justify-between">
<p class="leading-[70%] text-lg text-inter">
{{ $t("change_currency_country") }}
{{ $t("change_currency_and_country") }}
</p>
<CountryCurrencySelector />
</div>
@ -211,26 +369,27 @@
</div>
</div>
</template>
<script lang="ts" setup>
import CartPopup from "./CartPopup.vue";
import CountryCurrencySelector from "./CountryCurrencySelector.vue";
import LangSwitcher from "./LangSwitcher.vue";
import CartPopup from './CartPopup.vue'
import CountryCurrencySelector from './CountryCurrencySelector.vue'
import LangSwitcher from './LangSwitcher.vue'
const menuStore = useMenuStore();
const productStore = useProductStore();
const open = ref(false);
const openCart = ref(false);
const colorMode = useColorMode();
const userStore = useUserStore()
const open = ref(false)
const colorMode = useColorMode()
const menuStore = useMenuStore()
const checkoutStore = useCheckoutStore()
// productStore.getCart()
await checkoutStore.getUserCart()
const route = useRoute()
const isDark = computed({
get() {
return colorMode.value === "dark";
return colorMode.value === 'dark'
},
set(_isDark) {
colorMode.preference = _isDark ? "dark" : "light";
colorMode.preference = _isDark ? 'dark' : 'light'
},
});
})
</script>

View File

@ -34,14 +34,20 @@
/>
</defs>
</svg> -->
<img class="image" src="/block.webp" alt="">
<img class="image"
src="/block.webp"
alt=""
>
<slot />
</div>
</template>
<script lang="ts" setup></script>
<style>
.image {
clip-path: path("M0 20C0 8.9543 8.95431 0 20 0H847.666C858.641 0 867.565 8.84318 867.665 19.817L869.815 254.817C869.917 265.934 860.933 275 849.816 275H653C641.954 275 633 283.954 633 295V330C633 341.046 624.046 350 613 350H20C8.95431 350 0 341.046 0 330V20Z");
clip-path: path(
'M0 20C0 8.9543 8.95431 0 20 0H847.666C858.641 0 867.565 8.84318 867.665 19.817L869.815 254.817C869.917 265.934 860.933 275 849.816 275H653C641.954 275 633 283.954 633 295V330C633 341.046 624.046 350 613 350H20C8.95431 350 0 341.046 0 330V20Z'
);
}
</style>

View File

@ -14,11 +14,15 @@
group: 'px-[5px] py-[10px]',
item: 'hover:bg-block dark:hover:bg-button rounded-[5px] data-highlighted:not-data-disabled:before:bg-button/50',
}"
@update:model-value="setLocale"
@update:model-value="
setLocale($event);
$session.setLanguage($event);
$session.loadSession();
"
>
<template #leading="{ modelValue }">
<div class="flex items-center gap-2 text-xl font-medium uppercase">
{{ locales.find((item) => item.code === modelValue)?.code }}
{{ locales.find((item: any) => item.code === modelValue)?.code }}
</div>
</template>
@ -39,20 +43,8 @@
</template>
<script setup lang="ts">
import { useI18n } from "#imports";
const { locale, locales, setLocale } = useI18n()
const { $session } = useNuxtApp()
const { locale, locales, setLocale } = useI18n();
const isOpen = ref(false);
const selectedLocaleCode = ref(locale.value);
const selectedIcon = ref(
locales.value.find((item) => item.code === locale.value)?.icon
);
watch(selectedLocaleCode, async (newCode) => {
if (newCode !== locale.value) {
await setLocale(newCode);
}
});
const selectedLocaleCode = ref(locale.value)
</script>

File diff suppressed because one or more lines are too long

View File

@ -2,12 +2,12 @@
<ClientOnly v-if="!colorMode?.forced">
<div class="flex h-8 w-8 cursor-pointer items-center justify-center">
<i
@click="isDark = !isDark"
:class="[
'uil text-[32px] cursor-pointer',
isDark ? 'uil-moon' : 'uil-sun',
]"
></i>
@click="isDark = !isDark"
/>
</div>
<template #fallback>
<div class="size-20" />
@ -16,14 +16,14 @@
</template>
<script setup>
const colorMode = useColorMode();
const colorMode = useColorMode()
const isDark = computed({
get() {
return colorMode.value === "dark";
return colorMode.value === 'dark'
},
set(_isDark) {
colorMode.preference = _isDark ? "dark" : "light";
colorMode.preference = _isDark ? 'dark' : 'light'
},
});
})
</script>

View File

@ -2,21 +2,35 @@
<UiContainer class="space-y-[55px] sm:space-y-[75px] md:space-y-[100px]">
<div class="space-25-55-75">
<div class="grid sm:grid-cols-7 md:grid-cols-4 xl:grid-cols-2">
<h2 class="sm:col-start-2 sm:col-end-8 md:col-end-5 xl:col-start-2 h2-bold-bounded">
{{ component.section_lang_data.section_1.title }}
<h2
class="sm:col-start-2 sm:col-end-8 md:col-end-5 xl:col-start-2 h2-bold-bounded"
>
{{ component.front_section_lang[0].data.section_1.title }}
</h2>
</div>
<div class="space-y-[40px] sm:space-55-75">
<div v-for="(item, index) in component.section_lang_data.section_1
.text_blocks" :key="index" class="grid grid-cols-7 md:grid-cols-4 xl:grid-cols-2">
<div
v-for="(item, index) in component.front_section_lang[0].data.section_1
.text_blocks"
:key="index"
class="grid grid-cols-7 md:grid-cols-4 xl:grid-cols-2"
>
<div class="flex gap-[200px]">
<h4 class="h4-uppercase-bold-inter">0{{ index + 1 }}</h4>
<h4 class="hidden xl:block h4-uppercase-bold-inter w-full whitespace-nowrap">
<h4 class="h4-uppercase-bold-inter">
0{{ index + 1 }}
</h4>
<h4
class="hidden xl:block h4-uppercase-bold-inter w-full whitespace-nowrap"
>
{{ item.block_title }}
</h4>
</div>
<div class="col-start-2 col-end-8 md:col-end-5 space-y-[25px] sm:space-y-[45px] md:space-y-[55px]">
<h4 class="xl:hidden h4-uppercase-bold-inter w-full whitespace-nowrap">
<div
class="col-start-2 col-end-8 md:col-end-5 space-y-[25px] sm:space-y-[45px] md:space-y-[55px]"
>
<h4
class="xl:hidden h4-uppercase-bold-inter w-full whitespace-nowrap"
>
{{ item.block_title }}
</h4>
<p>{{ item.block_text }}</p>
@ -26,94 +40,129 @@
</div>
<div class="space-25-55-75">
<h2 class="h2-bold-bounded">
{{ component.section_lang_data.section_2.title }}
{{ component.front_section_lang[0].data.section_2.title }}
</h2>
<div class="grid sm:grid-cols-2 xl:grid-cols-4 gap-[30px] auto-cols-fr auto-rows-fr">
<div class="border border-border rounded-2xl p-8 flex flex-col justify-between gap-[55px]" v-for="(item, index) in component.section_lang_data.section_2
.text_blocks" :key="index">
<div
class="grid sm:grid-cols-2 xl:grid-cols-4 gap-[30px] auto-cols-fr auto-rows-fr"
>
<div
v-for="(item, index) in component.front_section_lang[0].data.section_2
.text_blocks"
:key="index"
class="border border-border rounded-2xl p-8 flex flex-col justify-between gap-[55px]"
>
<div>
<h4 class="h4-uppercase-bold-inter">0{{ index + 1 }}</h4>
<h4 class="h4-uppercase-bold-inter">{{ item.block_title }}</h4>
<h4 class="h4-uppercase-bold-inter">
0{{ index + 1 }}
</h4>
<h4 class="h4-uppercase-bold-inter">
{{ item.block_title }}
</h4>
</div>
<p>{{ item.block_text }}</p>
</div>
<div
class="row-end-4 sm:row-end-3 col-start-1 col-end-2 xl:row-end-1 xl:col-start-3 xl:col-end-4 flex flex-col gap-[10px] md:gap-[30px]">
<div class="w-full h-full md:h-[211px] rounded-2xl" :style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[0]}?thumb=1200x0')`,
class="row-end-4 sm:row-end-3 col-start-1 col-end-2 xl:row-end-1 xl:col-start-3 xl:col-end-4 flex flex-col gap-[10px] md:gap-[30px]"
>
<div
class="w-full h-full md:h-[211px] rounded-2xl"
:style="{
backgroundImage: `url('/api/public/file/${component.img[0]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}" />
<div class="w-full h-full md:h-[211px] rounded-2xl" :style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[1]}?thumb=1200x0')`,
}"
/>
<div
class="w-full h-full md:h-[211px] rounded-2xl"
:style="{
backgroundImage: `url('/api/public/file/${component.img[1]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}" />
}"
/>
</div>
</div>
</div>
<div class="grid space-25-55 xl:grid-cols-2">
<h2 class="h2-bold-bounded">
{{ component.section_lang_data.section_3.title }}
{{ component.front_section_lang[0].data.section_3.title }}
</h2>
<div class="flex flex-col space-25-55">
<p v-for="(item, index) in component.section_lang_data.section_3
.text_blocks" :key="index">
<p
v-for="(item, index) in component.front_section_lang[0].data.section_3
.text_blocks"
:key="index"
>
{{ item }}
</p>
</div>
</div>
<div class="grid xl:space-25-55 grid-cols-1 xl:grid-cols-2 xl:gap-[30px]">
<div class="flex flex-col gap-[25px] sm:gap-[55px] md:gap-[75px] xl:justify-between">
<div
class="flex flex-col gap-[25px] sm:gap-[55px] md:gap-[75px] xl:justify-between"
>
<h2 class="h2-bold-bounded">
{{ component.section_lang_data.section_4.title }}
{{ component.front_section_lang[0].data.section_4.title }}
</h2>
<p>{{ component.section_lang_data.section_4.description }}</p>
<p>{{ component.front_section_lang[0].data.section_4.description }}</p>
</div>
<div class="mb-[25px] mt-5 md:mb-[55px] xl:m-0 w-full h-[222px] sm:h-[371px] rounded-2xl" :style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[2]}?thumb=1200x0')`,
<div
class="mb-[25px] mt-5 md:mb-[55px] xl:m-0 w-full h-[222px] sm:h-[371px] rounded-2xl"
:style="{
backgroundImage: `url('/api/public/file/${component.img[2]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}" />
<p class="xl:col-start-2 xl:col-end-3">{{ component.section_lang_data.section_4.description_second }}</p>
}"
/>
<p class="xl:col-start-2 xl:col-end-3">
{{ component.front_section_lang[0].data.section_4.description_second }}
</p>
</div>
</UiContainer>
</template>
<script setup lang="ts">
defineProps<{ component: Component }>();
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: {
defineProps<{
component: {
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: {
data: {
section_1: {
title: string;
title: string
text_blocks: [
{
block_title: string;
block_text: string;
block_title: string
block_text: string
},
]
}
];
};
section_2: {
title: string;
title: string
text_blocks: [
{
block_title: string;
block_text: string;
block_title: string
block_text: string
},
]
}
];
};
section_3: {
title: string;
text_blocks: [];
};
title: string
text_blocks: []
}
section_4: {
title: string;
description: string;
description_second: string;
};
};
};
title: string
description: string
description_second: string
}
}
id_front_section: number
id_lang: number
}[]
}
}>()
</script>

View File

@ -2,93 +2,93 @@
<UiContainer class="space-y-[55px] sm:space-y-[75px] md:space-y-[100px]">
<div class="grid grid-cols-1 xl:grid-cols-2 gap-[10px] sm:gap-[30px]">
<div class="flex flex-col gap-[10px] sm:gap-[30px]">
<div
class="h-[240px] sm:h-[400px] md:h-[490px] bg-block p-[25px] sm:p-[50px] rounded-2xl flex flex-col justify-between">
<div class="h-[240px] sm:h-[400px] md:h-[490px] bg-block p-[25px] sm:p-[50px] rounded-2xl flex flex-col justify-between">
<div>
<h1 class="h1-big text-accent-green-light mb-6 md:mb-10">
{{ component.section_lang_data.title }}
{{ component.front_section_lang[0].data.title }}
</h1>
<p class="text-text-light sm:text-xl md:text-2xl font-medium">{{
component.section_lang_data.description }}</p>
<p class="text-text-light sm:text-xl md:text-2xl font-medium">
{{ component.front_section_lang[0].data.description }}
</p>
</div>
<UiButtonArrow type="fill" :arrow="true">{{ $t('buy_gold') }}</UiButtonArrow>
<nuxt-link :to="{ name: `id-slug___${$i18n.locale}`, params: { id: menuStore.getShopMenu()?.id, slug: menuStore.getShopMenu()?.front_menu_lang.at(0)?.link_rewrite } }">
<UiButtonArrow type="fill" :arrow="true">
{{ $t("buy_gold") }}
</UiButtonArrow>
</nuxt-link>
</div>
<div class="flex gap-[10px] sm:gap-[30px]">
<div class="rounded-2xl h-[155px] sm:h-[225px] md:h-[280px] w-full" :style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[1]}?thumb=1200x0')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}" />
<div class="rounded-2xl h-[155px] sm:h-[225px] md:h-[280px] w-full" :style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[2]}?thumb=1200x0')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}" />
<div class="hidden sm:block xl:hidden h-[225px] md:h-[280px] rounded-2xl w-full border border-block"
:style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[0]}?thumb=1200x0')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}" />
<div class="rounded-2xl h-[155px] sm:h-[225px] md:h-[280px] w-full" :style="{ backgroundImage: `url('/api/public/file/${component.img[1]}_l.webp')`, backgroundSize: 'cover', backgroundPosition: 'center' }" />
<div class="rounded-2xl h-[155px] sm:h-[225px] md:h-[280px] w-full" :style="{ backgroundImage: `url('/api/public/file/${component.img[2]}_l.webp')`, backgroundSize: 'cover' }" />
<div
class="hidden sm:block xl:hidden h-[225px] md:h-[280px] rounded-2xl w-full border border-block" :style="{ backgroundImage: `url('/api/public/file/${component.img[0]}_l.webp')`, backgroundSize: 'cover', backgroundPosition: 'center' }" />
</div>
</div>
<div class="sm:hidden xl:block h-[326px] md:h-auto min-w-[40%] xl:min-w-[60%] rounded-2xl border border-block"
:style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[0]}?thumb=1200x0')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}" />
<div class="sm:hidden xl:block h-[326px] md:h-auto min-w-[40%] xl:min-w-[60%] rounded-2xl border border-block" :style="{ backgroundImage: `url('/api/public/file/${component.img[0]}_l.webp')`, backgroundSize: 'cover', backgroundPosition: 'center' }" />
</div>
<div class="grid md:grid-cols-1 xl:grid-cols-2 gap-[20px] md:auto-rows-fr sm:gap-[30px]">
<div class="space-25-55-75">
<h2 class="h2-bold-bounded">{{ component.section_lang_data.section_2.title }}</h2>
<h2 class="h2-bold-bounded">
{{ component.front_section_lang[0].data.section_2.title }}
</h2>
<div class="space-y-[40px] sm:space-y-[50px]">
<p v-for="(item, index) in component.section_lang_data.section_2.text_blocks" :key="index">{{ item
}}</p>
<p v-for="(item, index) in component.front_section_lang[0].data .section_2.text_blocks" :key="index">
{{ item }}
</p>
</div>
</div>
<div class="w-full md:h-full rounded-2xl h-[327px] sm:h-[550px]" :style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[3]}?thumb=1200x0')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}" />
<div class="w-full md:h-full rounded-2xl h-[327px] sm:h-[550px]" :style="{ backgroundImage: `url('/api/public/file/${component.img[3]}_l.webp')`, backgroundSize: 'cover', backgroundPosition: 'center' }" />
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-y-[25px] sm:gap-y-[55px] xl:gap-y-0 gap-x-[30px]">
<h2 class="h2-bold-bounded sm:col-start-1 sm:col-end-3">{{ component.section_lang_data.section_3.title }}</h2>
<div v-for="(item, index) in component.section_lang_data.section_3.text_blocks" :key="index"
class="flex flex-col justify-between space-25-55">
<p class="md:px-4 xl:p-0">{{ item }}</p>
<div class=" rounded-2xl h-[227px] sm:h-[281px]" :style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[index + 4]}?thumb=1200x0')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}" />
<h2 class="h2-bold-bounded sm:col-start-1 sm:col-end-3">
{{ component.front_section_lang[0].data.section_3.title }}
</h2>
<div v-for="(item, index) in component.front_section_lang[0].data.section_3 .text_blocks" :key="index" class="flex flex-col justify-between space-25-55">
<p class="md:px-4 xl:p-0">
{{ item }}
</p>
<div class="rounded-2xl h-[227px] sm:h-[281px]" :style="{ backgroundImage: `url('/api/public/file/${component.img[index + 4]}_l.webp')`, backgroundSize: 'cover', backgroundPosition: 'center' }" />
</div>
<div class="sm:col-start-1 sm:col-end-3 xl:col-start-3 xl:col-end-5 flex justify-center xl:items-end xl:justify-end">
<UiButtonArrow type="fill" :arrow="true">{{ $t('buy_gold') }}</UiButtonArrow>
<div
class="sm:col-start-1 sm:col-end-3 xl:col-start-3 xl:col-end-5 flex justify-center xl:items-end xl:justify-end"
>
<nuxt-link :to="{ name: `id-slug___${$i18n.locale}`, params: { id: menuStore.getShopMenu()?.id, slug: menuStore.getShopMenu()?.front_menu_lang.at(0)?.link_rewrite } }">
<UiButtonArrow type="fill" :arrow="true">
{{ $t("buy_gold") }}
</UiButtonArrow>
</nuxt-link>
</div>
</div>
</UiContainer>
</template>
<script setup lang="ts">
const props = defineProps<{ component: Component }>();
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: {
title: string;
description: string;
button_label: string;
defineProps<{
component: {
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: {
data: {
title: string
description: string
section_2: {
title: string;
text_blocks: []
},
section_3: {
title: string;
text_blocks: []
title: string
text_blocks: string[]
}
};
};
section_3: {
title: string
text_blocks: string[]
}
}
id_front_section: number
id_lang: number
}[]
}
}>()
const menuStore = useMenuStore()
</script>

View File

@ -3,40 +3,45 @@
<div class="grid xl:grid-cols-2">
<div class="space-25-75 xl:col-start-2">
<h2 class="h2-bold-bounded">
{{ component.section_lang_data.title }}
{{ component.front_section_lang[0].data.title }}
</h2>
<p>{{ component.section_lang_data.description }}</p>
<p>{{ component.front_section_lang[0].data.description }}</p>
</div>
</div>
<div
class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 grid-rows-5 md:grid-rows-3 xl:grid-rows-2 gap-8"
>
<div
v-for="(item, index) in component.section_lang_data.feature_blocks"
v-for="(item, index) in component.front_section_lang[0].data
.feature_blocks"
:key="index"
:class="[
'p-8 rounded-2xl border border-block flex flex-col justify-between gap-4',
index === 0 && 'bg-block dark:text-bg-dark',
index === 1 && 'xl:col-start-2 xl:col-end-3',
index === 2 && 'xl:col-start-4 xl:col-end-5',
index === 3 &&
'md:col-start-1 md:col-end-3 xl:col-start-2 xl:col-end-4',
index === 3
&& 'md:col-start-1 md:col-end-3 xl:col-start-2 xl:col-end-4',
]"
>
<h4 class="h4-uppercase-bold-inter">
<span>0{{ index + 1 }} <br /></span>{{ item.block_title }}
<span>0{{ index + 1 }} <br></span>{{ item.block_title }}
</h4>
<p>{{ item.block_description }}</p>
</div>
<div
class="sm:text-white xl:row-start-2 md:col-start-1 md:col-end-3 xl:col-end-2 w-full md:h-full flex justify-center xl:items-end"
>
<UiButtonArrow :arrow="true" type="fill">Zakoupit zlato</UiButtonArrow>
<UiButtonArrow :arrow="true"
type="fill"
>
Zakoupit zlato
</UiButtonArrow>
</div>
<div
class="rounded-2xl row-start-4 md:row-start-2 md:col-start-2 md:col-end-3 xl:col-start-4 xl:col-end-5"
:style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[0]}?thumb=1200x0')`,
backgroundImage: `url('/api/public/file/${component.img[0]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'top',
}"
@ -46,23 +51,29 @@
</template>
<script lang="ts" setup>
import { UiButtonArrow } from "#components";
import { UiButtonArrow } from '#components'
const props = defineProps<{ component: Component }>();
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: {
title: string;
description: string;
feature_blocks: [
{
block_title: string;
block_description: string;
fill: boolean;
defineProps<{
component: {
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: {
data: {
title: string
description: string
feature_blocks: {
block_title: string
block_description: string
fill?: boolean
}[]
}
];
};
};
id_front_section: number
id_lang: number
}[]
}
}>()
</script>

View File

@ -2,13 +2,13 @@
<UiContainer class="space-55-75">
<div class="space-25-55-75">
<h2 class="h2-bold-bounded">
{{ component.section_lang_data.section_1.title }}
{{ component.front_section_lang[0].data.section_1.title }}
</h2>
<div class="flex flex-col xl:flex-row gap-12">
<div
class="rounded-2xl h-[255px] sm:h-[435px] md:h-[500px] w-full xl:min-w-[700px]"
:style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[0]}?thumb=1200x0')`,
backgroundImage: `url('/api/public/file/${component.img[0]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'bottom',
}"
@ -16,36 +16,43 @@
<div class="flex flex-col justify-between space-25-55">
<div class="flex flex-col gap-4">
<h4 class="h4-uppercase-bold-inter">
{{ component.section_lang_data.section_1.sub_title }}
{{ component.front_section_lang[0].data.section_1.sub_title }}
</h4>
<div class="flex flex-col">
<p
v-for="item in component.section_lang_data.section_1
.sub_description"
v-for="(item, index) in component.front_section_lang[0].data
.section_1.sub_description"
:key="index"
>
{{ item }}
</p>
</div>
</div>
<h4 class="h4-uppercase-bold-inter">
{{ component.section_lang_data.section_1.sub_title_second }}
{{
component.front_section_lang[0].data.section_1.sub_title_second
}}
</h4>
</div>
</div>
</div>
<div class="space-25-55-75">
<h2 class="h2-bold-bounded sm:text-center">
{{ component.section_lang_data.section_2.title }}
{{ component.front_section_lang[0].data.section_2.title }}
</h2>
<div class="space-25-55">
<div class="flex flex-col w-full xl:w-[55%]">
<p v-for="item in component.section_lang_data.section_2.description">
<p
v-for="(item, index) in component.front_section_lang[0].data
.section_2.description"
:key="index"
>
{{ item }}
</p>
</div>
<div class="w-full md:ml-10 xl:m-0 xl:grid xl:grid-cols-2">
<h4 class="col-start-2 col-end-3 h4-uppercase-bold-inter">
{{ component.section_lang_data.section_3.title }}
{{ component.front_section_lang[0].data.section_3.title }}
</h4>
</div>
</div>
@ -54,7 +61,7 @@
<div
class="rounded-2xl h-[230px] sm:h-[300px] w-full xl:h-[770px]"
:style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[1]}?thumb=1200x0')`,
backgroundImage: `url('/api/public/file/${component.img[1]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}"
@ -62,7 +69,7 @@
<div
class="rounded-2xl xl:hidden w-full h-[230px] sm:h-[300px] xl:w-full xl:h-[328px]"
:style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[2]}?thumb=1200x0')`,
backgroundImage: `url('/api/public/file/${component.img[2]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}"
@ -72,28 +79,36 @@
<div
class="hidden xl:block rounded-2xl max-full min-h-[330px]"
:style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[2]}?thumb=1200x0')`,
backgroundImage: `url('/api/public/file/${component.img[2]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}"
/>
<div class="h-full flex flex-col gap-5 xl:gap-0 justify-between">
<h4 class="col-start-2 col-end-3 h4-uppercase-bold-inter">
{{ component.section_lang_data.section_4.title }}
{{ component.front_section_lang[0].data.section_4.title }}
</h4>
<div class="flex flex-col">
<p
v-if="component.section_lang_data.section_4.description"
v-html="component.section_lang_data.section_4.description[0]"
></p>
<br />
v-if="
component.front_section_lang[0].data.section_4.description
"
v-html="
component.front_section_lang[0].data.section_4.description[0]
"
/>
<br>
<p
v-if="component.section_lang_data.section_4.description"
v-html="component.section_lang_data.section_4.description[1]"
></p>
v-if="
component.front_section_lang[0].data.section_4.description
"
v-html="
component.front_section_lang[0].data.section_4.description[1]
"
/>
</div>
<h4 class="col-start-2 col-end-3 h4-uppercase-bold-inter">
{{ component.section_lang_data.section_5.title }}
{{ component.front_section_lang[0].data.section_5.title }}
</h4>
</div>
</div>
@ -103,24 +118,40 @@
</template>
<script lang="ts" setup>
const props = defineProps<{ component: Component }>();
interface ContentSection {
title: string;
sub_title?: string;
sub_title_second?: string;
sub_description?: string[];
description?: string[];
defineProps<{
component: {
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: {
data: {
section_1: {
title: string
sub_title: string
sub_description: string[]
sub_title_second: string
}
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: {
section_1: ContentSection;
section_2: ContentSection;
section_3: ContentSection;
section_4: ContentSection;
section_5: ContentSection;
};
};
section_2: {
title: string
description: string[]
}
section_3: {
title: string
}
section_4: {
title: string
description: string[]
}
section_5: {
title: string
}
}
id_front_section: number
id_lang: number
}[]
}
}>()
</script>

View File

@ -4,58 +4,53 @@
<div class="flex flex-col justify-between w-full gap-4 xl:w-[45%]">
<div class="space-y-[25px]">
<h1 class="h1-big text-accent-green-light dark:text-accent-green-dark">
{{ component.section_lang_data.main_title }}
{{ component.front_section_lang[0].data.main_title }}
</h1>
<p>
{{ component.section_lang_data.main_description }}
{{ component.front_section_lang[0].data.main_description }}
</p>
</div>
<h4 class="h4-uppercase-bold-inter">
{{ component.section_lang_data.main_subtitle }}
{{ component.front_section_lang[0].data.main_subtitle }}
</h4>
</div>
<div
class="rounded-2xl h-[340px] sm:h-[380px] md:min-w-[324px] xl:h-[800px] xl:min-w-[740px] m-0 sm:mx-10 md:m-0"
<div class="rounded-2xl h-[340px] sm:h-[380px] md:min-w-[324px] xl:h-[800px] xl:min-w-[740px] m-0 sm:mx-10 md:m-0"
:style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[0]}?thumb=1200x0')`,
backgroundImage: `url('/api/public/file/${component.img[0]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}"
/>
}"></div>
</div>
<div class="space-25-55-75">
<h2 class="h2-bold-bounded">
{{ component.section_lang_data.story_title }}
{{ component.front_section_lang[0].data.story_title }}
</h2>
<div class="flex flex-col-reverse md:flex-row w-full gap-6">
<div
class="rounded-2xl h-[390px] md:h-auto min-w-[40%] xl:min-w-[60%]"
:style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[1]}?thumb=1200x0')`,
<div class="rounded-2xl h-[390px] md:h-auto min-w-[40%] xl:min-w-[60%]" :style="{
backgroundImage: `url('/api/public/file/${component.img[1]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}"
/>
}"></div>
<div class="flex flex-col">
<p v-for="(item, index) in component.section_lang_data.story_description">
{{ item }}
<div v-if="index < component.section_lang_data.story_description.length-1">
<div v-for="(item, index) in component.front_section_lang[0].data.story_description" :key="index">
<p>{{ item }}</p>
<div v-if="index < component.front_section_lang[0].data.story_description.length - 1">
<br>
</div>
</p>
</div>
</div>
</div>
<div class="flex flex-col w-full gap-6">
<h4 class="h4-uppercase-bold-inter">
{{ component.section_lang_data.story_subtitle }}
{{ component.front_section_lang[0].data.story_subtitle }}
</h4>
<div class="flex flex-col justify-between xl:max-w-[70%]">
<p v-for="(el, indexEl) in component.section_lang_data.story_sub_description">
{{ el }}
<div v-if="indexEl < component.section_lang_data.story_sub_description.length-1">
<div v-for="(el, indexEl) in component.front_section_lang[0].data.story_sub_description" :key="indexEl">
<p>{{ el }}</p>
<div v-if="indexEl < component.front_section_lang[0].data.story_sub_description.length - 1">
<br>
</div>
</p>
</div>
</div>
</div>
</div>
@ -63,20 +58,27 @@
</template>
<script lang="ts" setup>
const props = defineProps<{ component: Component }>();
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: {
main_title: string;
main_description: string;
main_subtitle: string;
story_title: string;
story_description: [];
story_subtitle: string;
story_sub_description: [];
};
};
defineProps<{
component: {
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: {
data: {
main_title: string
main_description: string
main_subtitle: string
story_title: string
story_description: string[]
story_subtitle: string
story_sub_description: string[]
}
id_front_section: number
id_lang: number
}[]
}
}>()
</script>

View File

@ -3,7 +3,7 @@
<div class="space-y-10 sm:space-y-[55px] md:space-y-14">
<h1 class="h1 text-center">
<span
v-for="(item, index) in component.section_lang_data.title"
v-for="(item, index) in component.front_section_lang[0].data.title"
:key="index"
:class="[
item.highlight
@ -13,35 +13,39 @@
]"
>
{{ item.text }}
<span v-if="index !== component.section_lang_data.title.length - 1">
</span>
<span
v-if="
index !== component.front_section_lang[0].data.title.length - 1
"
/>
</span>
</h1>
<div
class="h-[180px] sm:h-[330px] md:h-[500px] rounded-2xl"
:style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[0]}?thumb=640x0')`,
backgroundImage: `url('/api/public/file/${component.img[0]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}"
/>
<div class="md:w-full xl:w-[70%] space-y-14">
<p>{{ component.section_lang_data.main_description }}</p>
<p>{{ component.front_section_lang[0].data.main_description }}</p>
<h4 class="h4-uppercase-bold-inter">
{{ component.section_lang_data.secondary_description }}
{{ component.front_section_lang[0].data.secondary_description }}
</h4>
</div>
</div>
<div class="grid space-25-75 xl:space-0 grid-cols-1 xl:grid-cols-2">
<h2 class="h2-bold-bounded">
{{ component.section_lang_data.section_1_title }}
{{ component.front_section_lang[0].data.section_1_title }}
</h2>
<p v-html="component.section_lang_data.section_1_description"></p>
<p v-html="component.front_section_lang[0].data.section_1_description" />
</div>
<div class="grid space-25-75 xl:space-0 grid-cols-1 xl:grid-cols-2">
<h2 class="h2-bold-bounded">
<span
v-for="(item, index) in component.section_lang_data.section_2_title"
v-for="(item, index) in component.front_section_lang[0].data
.section_2_title"
:key="index"
:class="[
item.highlight
@ -51,24 +55,29 @@
]"
>
{{ item.text }}
<span v-if="index !== component.section_lang_data.title.length - 1">
</span>
<span
v-if="
index !== component.front_section_lang[0].data.title.length - 1
"
/>
</span>
</h2>
<p v-html="component.section_lang_data.section_2_description"></p>
<p v-html="component.front_section_lang[0].data.section_2_description" />
</div>
<div class="space-25-75">
<h2 class="h2-bold-bounded">
{{ component.section_lang_data.section_3_title }}
{{ component.front_section_lang[0].data.section_3_title }}
</h2>
<div
class="grid grid-cols-1 space-25-55 md:space-0 xl:gap-2 md:grid-cols-2"
>
<div class="space-y-[25px] sm:space-y-[45px]">
<p>{{ component.section_lang_data.section_3_description }}</p>
<p>
{{ component.front_section_lang[0].data.section_3_description }}
</p>
<div class="">
<p
v-for="(item, index) in component.section_lang_data
v-for="(item, index) in component.front_section_lang[0].data
.section_3_items"
:key="index"
>
@ -80,7 +89,7 @@
<div
class="h-[315px] xl:h-full rounded-2xl"
:style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[1]}?thumb=640x0')`,
backgroundImage: `url('/api/public/file/${component.img[1]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}"
@ -90,43 +99,49 @@
<div class="grid grid-cols-1 xl:grid-cols-2">
<div class="col-start-2 col-end-3 space-25-75 xl:space-0">
<h2 class="h2-bold-bounded">
{{ component.section_lang_data.section_4_title }}
{{ component.front_section_lang[0].data.section_4_title }}
</h2>
<p v-html="component.section_lang_data.section_4_description"></p>
<p
v-html="component.front_section_lang[0].data.section_4_description"
/>
</div>
</div>
</UiContainer>
</template>
<script lang="ts" setup>
const props = defineProps<{ component: Component }>();
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: {
title: [
{
text: string;
highlight: boolean;
defineProps<{
component: {
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: {
data: {
title: {
text: string
highlight: boolean
}[]
main_description: string
secondary_description: string
section_1_title: string
section_1_description: string
section_2_title: {
text: string
highlight: boolean
}[]
section_2_description: string
section_3_title: string
section_3_description: string
section_3_items: string[]
section_4_title: string
section_4_description: string
}
];
main_description: string;
secondary_description: string;
section_1_title: string;
section_1_description: string;
section_2_title: [
{
text: string;
highlight: boolean;
id_front_section: number
id_lang: number
}[]
}
];
section_2_description: string;
section_3_title: string;
section_3_description: string;
section_3_items: [];
section_4_title: string;
section_4_description: string;
};
};
}>()
</script>

View File

@ -2,40 +2,59 @@
<UiContainer class="space-55-75 xl:!space-y-[100px]">
<div class="space-25-75">
<h2 class="h2-bold-bounded max-w-[95%]">
<span v-for="(item, index) in component.section_lang_data
.reasons_section_title" :key="index" :class="[
<span
v-for="(item, index) in component.front_section_lang[0].data
.reasons_section_title"
:key="index"
:class="[
item.highlight
? 'text-accent-green-light dark:text-accent-green-dark'
: '',
'inline',
]">
]"
>
{{ item.text }}
<span v-if="
index !==
component.section_lang_data.reasons_section_title.length - 1
">
</span>
<span
v-if="
index
!== component.front_section_lang[0].data.reasons_section_title
.length
- 1
"
/>
</span>
</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 md:px-10 xl:p-0 gap-8 auto-rows-fr">
<div v-for="(item, index) in component.section_lang_data.reason_blocks" :key="index" :class="[
<div
class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 md:px-10 xl:p-0 gap-8 auto-rows-fr"
>
<div
v-for="(item, index) in component.front_section_lang[0].data
.reason_blocks"
:key="index"
:class="[
'p-[25px] rounded-2xl border border-block flex flex-col justify-between gap-20',
index === 1 && 'xl:col-start-2 xl:col-end-3',
index === 2 && 'xl:col-start-4 xl:col-end-5',
index === 3 && 'xl:col-start-2 xl:col-end-3',
]">
]"
>
<h4 class="h4-uppercase-bold-inter">
<span>0{{ index + 1 }} <br /></span>{{ item.title }}
<span>0{{ index + 1 }} <br></span>{{ item.title }}
</h4>
<p>{{ item.description }}</p>
</div>
<div class="row-end-7 sm:row-auto rounded-2xl flex items-center justify-center min-h-[200px]">
<div class="w-full h-full rounded-2xl" :style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[0]}?thumb=1200x0')`,
<div
class="row-end-7 sm:row-auto rounded-2xl flex items-center justify-center min-h-[200px]"
>
<div
class="w-full h-full rounded-2xl"
:style="{
backgroundImage: `url('/api/public/file/${component.img[0]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'top',
}" />
}"
/>
</div>
</div>
</div>
@ -43,68 +62,106 @@
<div>
<div class="space-25-75">
<h2 class="h2-bold-bounded">
{{ component.section_lang_data.cta_section_title }}
{{ component.front_section_lang[0].data.cta_section_title }}
</h2>
<div class="hidden xl:grid grid-cols-2 gap-6">
<div class="flex flex-col gap-20">
<p>{{ component.section_lang_data.cta_description_intro }}</p>
<p>{{ component.section_lang_data.cta_description_details }}</p>
<p>
{{ component.front_section_lang[0].data.cta_description_intro }}
</p>
<p>
{{
component.front_section_lang[0].data.cta_description_details
}}
</p>
</div>
<h4 class="h4-uppercase-bold-inter">
{{ component.section_lang_data.main_call_to_action_statement }}
{{
component.front_section_lang[0].data
.main_call_to_action_statement
}}
</h4>
</div>
<div class="xl:hidden space-25-55">
<div class="grid grid-cols-1 md:grid-cols-2 md:gap-[55px] space-25-55">
<p>{{ component.section_lang_data.cta_description_intro }}</p>
<div
class="grid grid-cols-1 md:grid-cols-2 md:gap-[55px] space-25-55"
>
<p>
{{ component.front_section_lang[0].data.cta_description_intro }}
</p>
<h4 class="h4-uppercase-bold-inter">
{{ component.section_lang_data.main_call_to_action_statement }}
{{
component.front_section_lang[0].data
.main_call_to_action_statement
}}
</h4>
</div>
<p>{{ component.section_lang_data.cta_description_details }}</p>
<p>
{{ component.front_section_lang[0].data.cta_description_details }}
</p>
</div>
</div>
</div>
<div class="space-y-[45px]">
<h4 class="h4-uppercase-bold-inter">
{{ component.section_lang_data.form_section_title }}
{{ component.front_section_lang[0].data.form_section_title }}
</h4>
<div class="flex flex-col md:flex-row gap-8 md:gap-[30px] xl:gap-0">
<div class="p-[25px] md:p-[50px] bg-block rounded-2xl space-y-[30px] xl:ml-40 xl:w-[65%]">
<div
class="p-[25px] md:p-[50px] bg-block rounded-2xl space-y-[30px] xl:ml-40 xl:w-[65%]"
>
<div class="flex gap-[30px]">
<input :placeholder="$t('first_name')" type="text"
class="border border-text-dark placeholder:text-button rounded-lg px-3 py-1.5 w-full focus:outline-none focus:ring-0 focus:border-2 text-button" />
<input :placeholder="$t('email')" type="text"
class="border border-text-dark placeholder:text-button rounded-lg px-3 py-1.5 w-full focus:outline-none focus:ring-0 focus:border-2 text-button" />
<input
:placeholder="$t('first_name')"
type="text"
class="border border-text-dark placeholder:text-button rounded-lg px-3 py-1.5 w-full focus:outline-none focus:ring-0 focus:border-2 text-button"
>
<input
:placeholder="$t('email')"
type="text"
class="border border-text-dark placeholder:text-button rounded-lg px-3 py-1.5 w-full focus:outline-none focus:ring-0 focus:border-2 text-button"
>
</div>
<textarea :placeholder="$t('form_question')"
class="border h-[145px] border-text-dark placeholder:text-button rounded-lg px-3 py-1.5 w-full focus:outline-none focus:ring-0 focus:border-2 text-button" />
<textarea
:placeholder="$t('form_question')"
class="border h-[145px] border-text-dark placeholder:text-button rounded-lg px-3 py-1.5 w-full focus:outline-none focus:ring-0 focus:border-2 text-button"
/>
<div class="w-full flex justify-center sm:justify-start">
<UiButtonArrow type="border">{{
<UiButtonArrow type="border">
{{
$t("submit_form")
}}</UiButtonArrow>
}}
</UiButtonArrow>
</div>
</div>
<div class="space-y-[30px] xl:px-[50px] sm:px-10">
<h4 class="h4-uppercase-bold-inter">
{{ $t('contact_info') }}
{{ $t("contact_info") }}
</h4>
<div class="flex flex-col sm:flex-row items-center sm:items-start md:flex-col justify-between gap-[30px]">
<div
class="flex flex-col sm:flex-row items-center sm:items-start md:flex-col justify-between gap-[30px]"
>
<div>
<p class="text-gray">{{ $t("phone") }}</p>
<p class="text-gray">
{{ $t("phone") }}
</p>
<p>+420 608 428 782</p>
</div>
<div>
<p class="text-gray">{{ $t("email") }}</p>
<p class="text-gray">
{{ $t("email") }}
</p>
<p>web@yourgold.cz</p>
</div>
<div>
<p class="text-gray">{{ $t("office_address") }}</p>
<p class="text-gray">
{{ $t("office_address") }}
</p>
<p>
Floriána Nováka 3 <br />
796 01 Prostějov <br />
Czech Republic <br />
Floriána Nováka 3 <br>
796 01 Prostějov <br>
Czech Republic <br>
CZ 08435456
</p>
</div>
@ -113,29 +170,46 @@
</div>
</div>
<div class="flex flex-col xl:flex-row xl:h-[130px] gap-[45px]">
<div class="w-full xl:w-[560px] h-[130px] xl:h-full rounded-2xl" :style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[1]}?thumb=640x0')`,
<div
class="w-full xl:w-[560px] h-[130px] xl:h-full rounded-2xl"
:style="{
backgroundImage: `url('/api/public/file/${component.img[1]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}" />
<div class="flex flex-col h-full justify-between items-stretch space-y-[25px] sm:space-y-[55px] xl:space-y-0">
<p>{{ component.section_lang_data.closing_inspirational_block }}</p>
}"
/>
<div
class="flex flex-col h-full justify-between items-stretch space-y-[25px] sm:space-y-[55px] xl:space-y-0"
>
<p>
{{
component.front_section_lang[0].data.closing_inspirational_block
}}
</p>
<h4 class="h4-uppercase-bold-inter">
<span v-for="(item, index) in component.section_lang_data.title" :key="index" :class="[
<span
v-for="(item, index) in component.front_section_lang[0].data
.title"
:key="index"
:class="[
item.highlight
? 'text-accent-green-light dark:text-accent-green-dark'
: '',
'inline',
]">
]"
>
{{ item.text }}
<span v-if="
index !==
component.section_lang_data.reasons_section_title.length - 1
">
</span>
<span
v-if="
index
!== component.front_section_lang[0].data.reasons_section_title
.length
- 1
"
/>
</span>
</h4>
<p>{{ component.section_lang_data.final_tagline }}</p>
<p>{{ component.front_section_lang[0].data.final_tagline }}</p>
</div>
</div>
</div>
@ -143,39 +217,40 @@
</template>
<script lang="ts" setup>
const props = defineProps<{ component: Component }>();
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: {
reasons_section_title: [
{
text: string;
highlight: boolean;
defineProps<{
component: {
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: {
data: {
reasons_section_title: {
text: string
highlight: boolean
}[]
reason_blocks: {
title: string
description: string
}[]
cta_section_title: string
cta_description_intro: string
cta_description_details: string
main_call_to_action_statement: string
form_section_title: string
closing_inspirational_block: string
title: {
text: string
highlight: boolean
}[]
final_tagline: string
contact_info: string
}
];
reason_blocks: [
{
title: string;
description: string;
id_front_section: number
id_lang: number
}[]
}
];
cta_section_title: string;
cta_description_intro: string;
cta_description_details: string;
main_call_to_action_statement: string;
form_section_title: string;
closing_inspirational_block: string;
title: [
{
text: string;
highlight: boolean;
}
];
final_tagline: string;
};
};
}>()
</script>

View File

@ -1,19 +1,43 @@
<template>
<div v-if="hasMainCategory" :class="['flex flex-col gap-[25px]', isOpen && 'border-b border-block pb-[10px]']">
<div @click="toggle" class="flex items-center justify-between rounded-lg cursor-pointer">
<div
v-if="hasMainCategory"
:class="[
'flex flex-col gap-[25px]',
isOpen && 'border-b border-block pb-[10px]',
]"
>
<div
class="flex items-center justify-between rounded-lg cursor-pointer"
@click="toggle"
>
<div class="flex items-center justify-between w-full">
<p class="text-lg xl:text-2xl font-extrabold text-black dark:text-white">
<p
class="text-lg xl:text-2xl font-extrabold text-black dark:text-white"
>
{{ mainCategoryName }}
</p>
<span :class="['flex items-center justify-center',isOpen && 'rotate-180', 'transition-all']"><i
class="iconify i-lucide:chevron-down text-button shrink-0 size-6 ms-auto"></i></span>
<span
:class="[
'flex items-center justify-center',
isOpen && 'rotate-180',
'transition-all',
]"
><i
class="iconify i-lucide:chevron-down text-button shrink-0 size-6 ms-auto"
/></span>
</div>
</div>
<ul class="flex flex-col gap-[25px]" v-show="isOpen">
<li @click="$emit('change-category', child)" v-for="child in subcategories" :key="child.id"
<ul v-show="isOpen"
class="flex flex-col gap-[25px]"
>
<li
v-for="child in subcategories"
:key="child.id"
class="text-base xl:text-lg cursor-pointer flex justify-between items-center"
:class="child.id === props.active ? 'text-yellow' : ''">
:class="child.id === props.active ? 'text-yellow' : ''"
@click="$emit('change-category', child)"
>
<span>{{ child.langs[0].Name }}</span>
<span>12</span>
</li>
@ -25,22 +49,34 @@
</template>
<script setup>
import { ref, computed } from 'vue';
import { ref, computed } from 'vue'
// Define the props
const props = defineProps({
data: Array,
active: Number
});
active: Number,
})
const isOpen = ref(false);
defineEmits(['change-category'])
const isOpen = ref(false)
const toggle = () => {
isOpen.value = !isOpen.value;
};
isOpen.value = !isOpen.value
}
// Computed properties
const hasMainCategory = computed(() => props.data.length > 0 && props.data[0].langs && props.data[0].langs.length > 0);
const mainCategoryName = computed(() => hasMainCategory.value ? props.data[0].langs[0].Name : '');
const subcategories = computed(() => hasMainCategory.value && props.data[0].children ? props.data[0].children : []);
const hasMainCategory = computed(
() =>
props.data
&& props.data.length > 0
&& props.data[0].langs
&& props.data[0].langs.length > 0,
)
const mainCategoryName = computed(() =>
hasMainCategory.value ? props.data[0].langs[0].Name : '',
)
const subcategories = computed(() =>
hasMainCategory.value && props.data[0].children ? props.data[0].children : [],
)
</script>

View File

@ -0,0 +1,464 @@
<template>
<UiContainer>
<div class="xl:w-[85%] mx-auto">
<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="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("address") }}
</div>
<div class="sm:px-6 sm:py-3 mx-auto">
{{ $t("summary") }}
</div>
<div class="hidden sm:block sm:px-6 sm:py-3 sm:mx-auto">
{{ $t("order_placed") }}
</div>
</div>
</div>
<div class="space-y-[25px] sm:space-y-[30px]">
<h2 class="h2-bold-bounded">
{{ $t("Account address") }}
</h2>
<div class="flex flex-col gap-[30px] sm:flex-row">
<div class="flex flex-col sm:w-1/2 gap-[25px] sm:gap-[30px]">
<CheckoutInput :id="1"
v-model="checkoutStore.userName"
disabled
>
{{ $t("first_name") }}
</CheckoutInput>
<CheckoutInput :id="2"
v-model="checkoutStore.lastName"
disabled
>
{{ $t("surname") }}
</CheckoutInput>
<CheckoutInput :id="3"
v-model="checkoutStore.address"
disabled
>
{{ $t("address") }}
</CheckoutInput>
<CheckoutInput :id="4"
v-model="checkoutStore.postCode"
disabled
>
{{ $t("post_code") }}
</CheckoutInput>
</div>
<div class="flex flex-col sm:w-1/2 gap-[30px]">
<CheckoutInput :id="5"
v-model="checkoutStore.city"
disabled
>
{{ $t("city") }}
</CheckoutInput>
<CheckoutInput :id="6"
v-model="checkoutStore.country"
disabled
>
{{ $t("country") }}
</CheckoutInput>
<CheckoutInput
:id="7"
v-model="checkoutStore.accountPhoneNumber"
disabled
>
{{ $t("phone") }}
</CheckoutInput>
</div>
</div>
</div>
<div class="space-y-[30px]">
<h2 class="h2-bold-bounded">
{{ $t("Shipping details") }}
</h2>
<div class="flex flex-col gap-2">
<div
:class="[
'flex items-center gap-[10px] relative border border-block rounded-lg h-[50px] sm:h-[67px] w-full',
checkoutStore.vUseAccountPhoneNumber && 'px-6',
]"
>
<div
v-if="!checkoutStore.vUseAccountPhoneNumber"
class="flex items-center gap-5 sm:gap-[25px]"
>
<div class="flex flex-col items-start gap-[25px]">
<div
ref="dropdownIsoRef"
class="pl-5 sm:pl-[25px] relative w-full ring-0 cursor-pointer focus:ring-0 outline-none focus-visible:ring-0"
>
<div class="p-0"
@click="dropIso = !dropIso"
>
<div
class="flex items-center gap-2 text-base sm:text-xl font-medium uppercase text-text-light dark:text-text-dark"
>
<p class="hidden sm:block">
{{ checkoutStore.selectedIso.name }}
</p>
<p class="sm:hidden">
{{ checkoutStore.selectedIso.iso_code }}
</p>
<span>
<i
class="uil uil-angle-down text-2xl font-light cursor-pointer"
/></span>
</div>
</div>
<div
v-if="dropIso"
class="absolute w-[130px] sm:w-full mt-2 left-0 bg-bg-light dark:bg-bg-dark rounded-[5px] data-highlighted:not-data-disabled:before:bg-button/50 ring-0 cursor-pointer focus:ring-0 outline-none focus-visible:ring-0 border border-button py-[10px] px-[5px]"
>
<div
class="overflow-auto h-[200px] w-full overflow-x-hidden"
>
<p
v-for="item in menuStore.countries"
:key="item.iso_code"
class="w-full hover:bg-block dark:hover:bg-button pl-2 py-2 text-base text-text-light dark:text-text-dark rounded-[5px]"
@click="
() => {
checkoutStore.selectedIso = item;
dropIso = false;
checkoutStore.changePrefix(
item.call_prefix as string,
);
}
"
>
{{ item?.name }}
</p>
</div>
</div>
</div>
</div>
<p class="text-sm sm:text-xl border-r pr-[10px] border-block">
{{ checkoutStore.currentPrefix }}
</p>
</div>
<input
id="phone"
:value="
checkoutStore.vUseAccountPhoneNumber
? checkoutStore.accountPhoneNumber
: checkoutStore.phoneNumber
"
:disabled="checkoutStore.vUseAccountPhoneNumber"
type="tel"
placeholder="123 xxxx xxx"
class="placeholder:text-sm sm:placeholder:text-xl placeholder:text-gray placeholder:uppercase dark:placeholder:text-bg-light rounded-lg h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-0"
@input="
(e) => {
if (!checkoutStore.vUseAccountPhoneNumber) {
checkoutStore.phoneNumber = (
e.target as HTMLInputElement
).value;
}
}
"
>
</div>
<p
v-if="
checkoutStore.phoneValidation === false
&& !checkoutStore.vUseAccountPhoneNumber
"
class="text-red-500"
>
Invalid phone number
</p>
</div>
<div class="flex items-center gap-2">
<input
id="checkbox"
type="checkbox"
class="border border-button !bg-inherit"
@change="
(event: Event) => {
const target = event.target as HTMLInputElement;
target.checked
? (checkoutStore.vUseAccountPhoneNumber = true)
: (checkoutStore.vUseAccountPhoneNumber = false);
checkoutStore.phoneValidation = null;
}
"
>
<p>{{ $t("use_account_phone") }}</p>
</div>
</div>
<div class="space-y-[30px] h-full">
<h2 class="h2-bold-bounded">
{{ $t("Select delivery address") }}
</h2>
<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
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',
]"
>
<div
:class="[
'flex flex-col justify-between gap-[10px] h-full',
checkoutStore.activeAddress !== item
&& 'text-gray dark:text-button-disabled',
]"
>
<span>{{ item.address.name }} {{ item.address.surname }}</span>
<span>{{ item.address.street }}</span>
<span>{{ item.address.postcode }} {{ item.address.city }}</span>
<span>{{ item.address.country_iso }}</span>
</div>
<div
class="flex items-center gap-2 border-t pt-[15px] border-block"
>
<input
id="checkbox"
:checked="checkoutStore.activeAddress ? true : false"
type="checkbox"
class="border border-button !bg-inherit"
@change="
(event: Event) => {
const target = event.target as HTMLInputElement;
target.checked
? (checkoutStore.activeAddress = item)
: (checkoutStore.activeAddress = null);
checkoutStore.isOpen = false;
}
"
>
<p>{{ $t("choose_default_address") }}</p>
</div>
</div>
</div>
<p class="uppercase">
{{ $t("or") }}
</p>
<div
:class="[
'cursor-pointer w-full sm:w-[500px] py-[15px] px-[25px] rounded-lg border-2 flex flex-col items-center justify-center min-h-[200px] md:h-full',
checkoutStore.isOpen
? 'border-button text-button'
: 'text-gray border-block ',
]"
@click="
() => {
checkoutStore.isOpen = !checkoutStore.isOpen;
checkoutStore.activeAddress = null;
}
"
>
<h4
:class="[
'font-inter text-base leading-[150%] uppercase text-[16px] sm:text-[20px] border-b',
checkoutStore.isOpen ? 'border-button' : 'border-gray',
]"
>
{{ $t("add_new_address") }}
</h4>
</div>
</div>
<div
v-if="checkoutStore.isOpen"
class="flex flex-col items-center gap-[30px] justify-center w-full"
>
<div class="flex flex-col gap-[30px] xl:flex-row w-full">
<div class="flex flex-col sm:w-1/2 gap-[25px] sm:gap-[30px]">
<CheckoutInput
:id="8"
v-model="checkoutStore.vNewAddressName"
:placeholder="$t('first_name')"
>
{{ $t("first_name") }}
</CheckoutInput>
<CheckoutInput
:id="9"
v-model="checkoutStore.vNewAddressAddress"
:placeholder="$t('address')"
>
{{ $t("address") }}
</CheckoutInput>
<CheckoutInput
:id="10"
v-model="checkoutStore.vNewAddressCity"
:placeholder="$t('city')"
>
{{ $t("city") }}
</CheckoutInput>
</div>
<div class="flex flex-col sm:w-1/2 gap-[25px] sm:gap-[30px]">
<CheckoutInput
:id="11"
v-model="checkoutStore.vNewAddressSurname"
:placeholder="$t('surname')"
>
{{ $t("surname") }}
</CheckoutInput>
<div class="space-y-[15px]">
<p class="pl-6">
{{ $t("country") }}
</p>
<div
ref="dropdownCountryRef"
class="relative w-full ring-0 cursor-pointer focus:ring-0 outline-none focus-visible:ring-0"
>
<div
class="border border-block placeholder:text-gray dark:placeholder:text-button-disabled rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2 flex items-center justify-start"
@click="dropCountry = !dropCountry"
>
<div
class="flex items-center gap-2 text-base sm:text-xl font-medium uppercase text-text-light dark:text-text-dark"
>
{{
checkoutStore.vNewAddressCountry
? checkoutStore.vNewAddressCountry.name
: "-"
}}
<span>
<i
class="uil uil-angle-down text-2xl font-light cursor-pointer"
/></span>
</div>
</div>
<div
v-if="dropCountry"
class="absolute z-50 w-full mt-2 left-0 bg-bg-light dark:bg-bg-dark rounded-[5px] data-highlighted:not-data-disabled:before:bg-button/50 ring-0 cursor-pointer focus:ring-0 outline-none focus-visible:ring-0 border border-button py-[10px] px-[5px]"
>
<div class="overflow-auto h-[200px] w-full">
<p
v-for="item in menuStore.countries"
:key="item.iso_code"
class="w-full hover:bg-block dark:hover:bg-button pl-2 py-2 text-base text-text-light dark:text-text-dark rounded-[5px]"
@click="
() => {
checkoutStore.vNewAddressCountry = item;
dropCountry = false;
}
"
>
{{ item?.name }}
</p>
</div>
</div>
</div>
</div>
<CheckoutInput
:id="13"
v-model="checkoutStore.vNewAddressCode"
:placeholder="$t('post_code')"
>
{{ $t("post_code") }}
</CheckoutInput>
</div>
</div>
<div>
<span v-if="addressValidation === false"
class="text-red"
>
{{ $t("Remember to select a shipping address") }}</span>
<div
class="group flex cursor-pointer items-center justify-start gap-2 whitespace-nowrap"
>
<button
:class="[
'h-[40px] cursor-pointer min-w-40 rounded-[10px] px-[22px] transition-all sm:h-[50px] md:h-[65px] md:rounded-[15px] md:px-[42px] bg-button text-text-dark group-hover:bg-button-hover',
]"
@click="checkoutStore.uploadAddress()"
>
{{ $t("add_new_address") }}
</button>
</div>
</div>
</div>
</div>
<div class="flex justify-center">
<div
class="group flex cursor-pointer items-center justify-start gap-2 whitespace-nowrap"
>
<button
:disabled="!checkoutStore.activeAddress"
:class="[
'h-[40px] cursor-pointer min-w-40 rounded-[10px] px-[22px] transition-all sm:h-[50px] md:h-[65px] md:rounded-[15px] md:px-[42px]',
checkoutStore.activeAddress
? 'bg-button text-text-dark group-hover:bg-button-hover'
: ' bg-button-disabled text-gray',
]"
@click="checkoutStore.sendForm()"
>
{{ $t("continue") }}
</button>
<div
:class="[
'flex h-[40px] w-[40px] items-center justify-center rounded-[10px] p-2.5 transition-all sm:h-[50px] sm:w-[50px] md:h-[65px] md:w-[65px] md:rounded-[15px]',
checkoutStore.activeAddress
? 'bg-button text-text-dark group-hover:bg-button-hover'
: ' bg-button-disabled text-gray',
]"
>
<svg
class=""
width=" 26"
height="26"
viewBox="0 0 26 26"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M25.1274 1.87258C25.1274 1.3203 24.6797 0.872582 24.1274 0.872584L15.1274 0.872583C14.5751 0.872583 14.1274 1.3203 14.1274 1.87258C14.1274 2.42487 14.5751 2.87258 15.1274 2.87258L23.1274 2.87258L23.1274 10.8726C23.1274 11.4249 23.5751 11.8726 24.1274 11.8726C24.6797 11.8726 25.1274 11.4249 25.1274 10.8726L25.1274 1.87258ZM1.5 24.5L2.20711 25.2071L24.8345 2.57969L24.1274 1.87258L23.4203 1.16548L0.792893 23.7929L1.5 24.5Z"
fill="currentColor"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
</UiContainer>
</template>
<script setup lang="ts">
import { onClickOutside } from '@vueuse/core'
import CheckoutInput from '../ui/CheckoutInput.vue'
const checkoutStore = useCheckoutStore()
const menuStore = useMenuStore()
const dropIso = ref(false)
const dropCountry = ref(false)
const addressValidation = ref<null | boolean>(null)
const dropdownIsoRef = ref(null)
const dropdownCountryRef = ref(null)
onClickOutside(dropdownIsoRef, () => {
dropIso.value = false
})
onClickOutside(dropdownCountryRef, () => {
dropCountry.value = false
})
checkoutStore.getCheckout()
checkoutStore.getAddressList()
checkoutStore.getUserData()
</script>

View File

@ -0,0 +1,252 @@
<template>
<UiContainer v-if="checkoutStore.modalMadeOrder">
<NuxtLink to="/" class="mt-6 text-blue-500 underline" @click="checkoutStore.modalMadeOrder = false">Go back home
</NuxtLink>
</UiContainer>
<UiContainer v-else>
<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-1 sm:grid-cols-2 xl:grid-cols-3 gap-[30px]">
<div class="col-start-1 col-end-2 sm:col-end-3 space-y-5">
<h4 class="h4-uppercase-bold-inter">
{{ component.front_section_lang[0].data.product_list }}
</h4>
<div class="border-2 border-block rounded-[15px] p-[25px] sm:p-[50px] space-25-55">
<div v-for="(item, index) in checkoutStore.products" :key="index">
<div class="flex items-center h-[100px] sm:h-[150px]">
<div class="min-w-[100px] sm:min-w-[150px] flex items-center justify-center h-[100px] sm: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="col-start-1 col-end-2 sm:col-end-3 xl:col-auto flex flex-col sm:flex-row xl:flex-col items-center gap-[30px]">
<div class="space-y-5 w-full">
<h4 class="h4-uppercase-bold-inter">
{{ component.front_section_lang[0].data.account_address }}
</h4>
<div
class="border-2 border-block rounded-[8px] px-[25px] py-[15px] flex flex-col gap-1 text-sm sm:text-lg md:text-xl">
<span>{{ checkoutStore.defaultAddress?.name }} {{
checkoutStore.defaultAddress?.surname }}</span>
<span>{{ checkoutStore.defaultAddress?.street }}</span>
<span>{{ checkoutStore.defaultAddress?.postcode }} {{
checkoutStore.defaultAddress?.city }}</span>
</div>
</div>
<div class="flex flex-col space-y-5 w-full h-full">
<h4 class="h4-uppercase-bold-inter">
{{ component.front_section_lang[0].data.note }}
</h4>
<textarea id="1" v-model="checkoutStore.vNote"
class="border-2 border-block rounded-[8px] p-[15px] w-full flex-1 resize-none placeholder:text-button"
name="rty" :placeholder="component.front_section_lang[0].data.note"></textarea>
</div>
</div>
<div class="col-start-1 col-end-2 xl:col-end-3 space-y-5">
<h4 class="h4-uppercase-bold-inter">
{{ component.front_section_lang[0].data.delivery_type }}
</h4>
<div
class="border-2 border-block rounded-[15px] p-[25px] sm:p-[50px] space-y-[25px] text-sm sm:text-lg md:text-xl">
<div v-for="(delivery, index) in checkoutStore.deliveryOption" :key="index"
class="flex flex-col cursor-pointer" @click="checkoutStore.setCurrentDelivery(delivery)">
<div class="flex items-end justify-between">
<div class="flex gap-[15px]">
<div class="mt-1 w-4 h-4 border-2 border-button rounded-full">
<div v-if="checkoutStore.currentDelivery === delivery"
class="w-full h-full border-3 border-bg-light dark:border-bg-dark rounded-full bg-button">
</div>
</div>
<div class="flex flex-col">
<span>{{ delivery.delivery_supplier_name }}</span>
<span>{{ delivery.country_name }}</span>
</div>
</div>
<p class="font-inter text-lg sm:text-[21px] md:text-2xl leading-[150%] font-bold">
{{ menuStore.formatPrice(Number(delivery.shippment_price)) }}
</p>
</div>
</div>
</div>
</div>
<div class="space-y-5 w-full flex flex-col h-full">
<h4 class="h4-uppercase-bold-inter">
{{ component.front_section_lang[0].data.payment_method }}
</h4>
<div
class="border-2 border-block rounded-[8px] px-[25px] py-[25px] sm:py-[15px] flex-1 flex flex-col gap-1 text-sm sm:text-xl overflow-auto">
<UCarousel :prev-icon="'i-lucide-chevron-left'" :next-icon="'i-lucide-chevron-right'" :ui="{
viewport: 'h-full w-full',
container: 'h-full w-full',
item: 'h-full w-full pl-7',
prev: '-start-8 sm:-start-12 ring-0 text-button disable:text-block p-0 ring-inset ring-accented bg-inherit disabled:bg-inherit',
next: '-end-8 sm:-end-12 ring-0 text-button disable:text-block p-0 text-[30px] bg-inherit disabled:bg-inherit',
arrows: '',
}" :prev="{ size: 'xl' }" :next="{ size: 'xl' }" arrows :items="checkoutStore.paymentMethods"
class="relative sm:max-w-full mx-4 sm:mx-10 h-full">
<template #default="{ item }">
<div class="flex flex-col items-start justify-between h-full leading-[250%] sm:leading-[150%]">
<span>{{ item.bank_name }}</span>
<span>{{ item.country_name }}</span>
<span>{{ item.street_and_number }}</span>
<span>{{ item.iban }}</span>
<span>{{ item.swift }}</span>
</div>
</template>
</UCarousel>
</div>
</div>
</div>
<div class="w-full border-y-2 border-block p-[25px] flex flex-col gap-[15px]">
<div class="flex items-center justify-between">
<p class="text-sm sm:text-xl">
{{ component.front_section_lang[0].data.subtotal }}
</p>
<p class="text-lg sm:text-xl font-medium">
{{
menuStore.formatPrice(Number(checkoutStore.fullProductsPrice)) }}
</p>
</div>
<div class="flex items-center justify-between">
<p class="text-sm sm:text-xl">
{{ component.front_section_lang[0].data.shipping_cost }}
</p>
<p class="text-lg sm:text-xl font-medium">
{{
menuStore.formatPrice(Number(checkoutStore.shippingPrice)) }}
</p>
</div>
<div class="flex items-center justify-between uppercase text-lg sm:text-xl">
<p>{{ component.front_section_lang[0].data.total }}</p>
<p
class="text-accent-green-light dark:text-accent-green-dark font-inter text-xl sm:text-2xl leading-[150%] font-bold">
{{ menuStore.formatPrice(Number(checkoutStore.fullPrice)) }}
</p>
</div>
</div>
<div class="flex flex-col sm:flex-row items-center justify-between mt-1 gap-[55px]">
<div class="flex flex-col gap-1 text-white w-full">
<div class="flex gap-3 text-black dark:text-white items-center">
<input id="first" v-model="checkoutStore.vLegal" type="checkbox" name="first" />
<!-- <label for="first" class="text-sm leading-snug">
{{ $t("I accept ") }}
<NuxtLink target="_blank"
:to="{ name: 'lang-info-name', params: { lang: menuStore.selectedLanguage.iso_code, name: 'legal_statement.html' } }"
class="underline cursor-pointer">
{{ $t("the legal statement*") }}
</NuxtLink>
</label> -->
<p class="text-sm sm:text-lg">
{{ $t("I accept ") }} {{ $t("the legal statement*") }}
</p>
</div>
<span v-if="checkoutStore.legalValidation" class="text-xs text-red-600 ml-6">
{{ $t("You need to accept this") }}
</span>
<div class="flex gap-3 text-black dark:text-white items-center mt-2">
<input id="second" v-model="checkoutStore.vTerms" type="checkbox" name="second" />
<!-- <label for="second" class="text-sm leading-snug">
{{ $t("I accept ") }}
<NuxtLink target="_blank"
:to="{ name: 'lang-info-name', params: { lang: menuStore.selectedLanguage.iso_code, name: 'general_terms_and_conditions.html' } }"
class="underline cursor-pointer">
{{ $t("general terms and conditions*") }}
</NuxtLink>
</label> -->
<p class="text-sm sm:text-lg">
{{ $t("I accept ") }} {{ $t("general terms and conditions*") }}
</p>
</div>
<span v-if="checkoutStore.termsValidation" class="text-xs text-red-600 ml-6">
{{ $t("You need to accept this") }}
</span>
</div>
<UiButtonArrow type="fill" :arrow="true" @click="checkoutStore.sendSummaryForm">
{{ $t("Buy") }}
</UiButtonArrow>
</div>
</div>
</UiContainer>
</template>
<script setup lang="ts">
import { UCarousel } from '#components'
const checkoutStore = useCheckoutStore()
const menuStore = useMenuStore()
await checkoutStore.getOrder()
await checkoutStore.getBankAccount()
await checkoutStore.getUserCart()
await checkoutStore.getDeliveryOptions()
await checkoutStore.getDefAddress()
defineProps<{
component: {
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: {
data: {
product_list: string
account_address: string
note: string
delivery_type: string
payment_method: string
subtotal: string
shipping_cost: string
total: string
accept: string
legal: string
terms: string
}
id_front_section: number
id_lang: number
}[]
}
}>()
</script>

View File

@ -1,46 +1,65 @@
<template>
<UiContainer class="space-y-[45px]">
<div class="xl:w-[70%] space-y-[25px]">
<h1 class="h1">{{ component.section_lang_data.title }}</h1>
<p v-html="component.section_lang_data.description"></p>
<h1 class="h1">
{{ component.front_section_lang[0].data.title }}
</h1>
<p v-html="component.front_section_lang[0].data.description" />
</div>
<div class="flex flex-col md:flex-row gap-8 md:gap-[30px] xl:gap-0">
<div class="p-[25px] md:p-[50px] bg-block rounded-2xl space-y-[30px] xl:ml-40 xl:w-[65%]">
<div
class="p-[25px] md:p-[50px] bg-block rounded-2xl space-y-[30px] xl:ml-40 xl:w-[65%]"
>
<div class="flex gap-[30px]">
<input :placeholder="$t('first_name')" type="text"
class="border border-text-dark placeholder:text-button rounded-lg px-3 py-1.5 w-full focus:outline-none focus:ring-0 focus:border-2 text-button" />
<input :placeholder="$t('email')" type="text"
class="border border-text-dark placeholder:text-button rounded-lg px-3 py-1.5 w-full focus:outline-none focus:ring-0 focus:border-2 text-button" />
<input
:placeholder="$t('first_name')"
type="text"
class="border border-button placeholder:text-button rounded-lg px-3 py-1.5 w-full focus:outline-none focus:ring-0 focus:border-2 text-button"
>
<input
:placeholder="$t('email')"
type="text"
class="border border-button placeholder:text-button rounded-lg px-3 py-1.5 w-full focus:outline-none focus:ring-0 focus:border-2 text-button"
>
</div>
<textarea :placeholder="$t('form_question')"
class="border h-[276px] border-text-dark placeholder:text-button rounded-lg px-3 py-1.5 w-full focus:outline-none focus:ring-0 focus:border-2 text-button" />
<textarea
:placeholder="$t('form_question')"
class="border h-[276px] border-button placeholder:text-button rounded-lg px-3 py-1.5 w-full focus:outline-none focus:ring-0 focus:border-2 text-button"
/>
<div class="w-full flex justify-center sm:justify-start">
<UiButtonArrow type="border">{{
$t("submit_form")
}}</UiButtonArrow>
<UiButtonArrow type="border">
{{ $t("submit_form") }}
</UiButtonArrow>
</div>
</div>
<div class="space-y-[30px] xl:px-[50px] sm:px-10">
<h4 class="h4-uppercase-bold-inter">
{{ $t('contact_info') }}
{{ $t("contact_info") }}
</h4>
<div
class="flex flex-col sm:flex-row items-center sm:items-start md:flex-col justify-between gap-[15px] sm:gap-[30px]">
class="flex flex-col sm:flex-row items-center sm:items-start md:flex-col justify-between gap-[15px] sm:gap-[30px]"
>
<div>
<p class="text-gray">{{ $t("phone") }}</p>
<p class="text-gray">
{{ $t("phone") }}
</p>
<p>+420 608 428 782</p>
</div>
<div>
<p class="text-gray">{{ $t("email") }}</p>
<p class="text-gray">
{{ $t("email") }}
</p>
<p>web@yourgold.cz</p>
</div>
<div>
<p class="text-gray">{{ $t("office_address") }}</p>
<p class="text-gray">
{{ $t("office_address") }}
</p>
<p>
Floriána Nováka 3 <br />
796 01 Prostějov <br />
Czech Republic <br />
Floriána Nováka 3 <br>
796 01 Prostějov <br>
Czech Republic <br>
CZ 08435456
</p>
</div>
@ -49,15 +68,24 @@
</div>
</UiContainer>
</template>
<script setup lang="ts">
defineProps<{ component: Component }>();
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: {
title: string;
description: string;
};
};
defineProps<{
component: {
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: {
data: {
title: string
description: string
}
id_front_section: number
id_lang: number
}[]
}
}>()
</script>

View File

@ -1,32 +1,26 @@
<template>
<div class="flex gap-24">
<div class="price-container">
<div class="slider" v-html="duplicatedContent">
</div>
<div class="slider"
v-html="duplicatedContent"
/>
</div>
</div>
</template>
<script setup lang="ts">
let res = "<span class=\"text-base sm:text-lg font-bold\">CZK price (to EUR). Kč 25,1720<span class=\"text-accent-green-light dark:text-accent-green-dark\"> Kč 0,0180 (0.07%) </span></span><span class=\"text-base sm:text-lg font-bold\">Gold price on market. 2 929,7250 €<span class=\"text-[#B72D2D]\"> -6,9560 € (-0.24%) </span></span><span class=\"text-base sm:text-lg font-bold\">Silver price on market. 31,5280 €<span class=\"text-accent-green-light dark:text-accent-green-dark\"> 0,1690 € (0.54%) </span></span><span class=\"text-base sm:text-lg font-bold\">PLN price (to EUR). zł 4,2660<span class=\"text-[#B72D2D]\"> zł -0,0050 (-0.12%) </span></span>"
const res
= '<span class="text-base sm:text-lg font-bold">CZK price (to EUR). Kč 25,1720<span class="text-accent-green-light dark:text-accent-green-dark"> Kč 0,0180 (0.07%) </span></span><span class="text-base sm:text-lg font-bold">Gold price on market. 2 929,7250 €<span class="text-[#B72D2D]"> -6,9560 € (-0.24%) </span></span><span class="text-base sm:text-lg font-bold">Silver price on market. 31,5280 €<span class="text-accent-green-light dark:text-accent-green-dark"> 0,1690 € (0.54%) </span></span><span class="text-base sm:text-lg font-bold">PLN price (to EUR). zł 4,2660<span class="text-[#B72D2D]"> zł -0,0050 (-0.12%) </span></span>'
const productStore = useProductStore()
const activeElement = ref(1);
productStore.getModules()
// Computed property to duplicate the content
const duplicatedContent = computed(() => {
const originalContent = res || '';
return originalContent + originalContent + originalContent + originalContent;
});
const changeActive = (item: number) => {
activeElement.value = item;
};
const originalContent = res || ''
return originalContent + originalContent + originalContent + originalContent
})
</script>
<style scoped>
.price-container {
/* display: flex; */

View File

@ -2,53 +2,84 @@
<div class="border-t border-border pt-[75px]">
<UiContainer class="flex flex-col gap-24">
<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">
<div v-for="(item, index) in component.section_lang_data" :key="index"
class="flex flex-col gap-[25px] sm:gap-8 max-w-[280px]">
<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"
v-for="(el, indexEl) in item.children" :key="indexEl">
{{ el.title }}
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>
<div
v-for="(item, key) in contact"
:key="key"
class="flex flex-col gap-[25px] sm:gap-8 max-w-[280px]"
>
<h3 v-if="key == 'header'"
class="h4-uppercase-bold-inter"
>
{{ item }}
</h3>
<div v-else
class="transition-all text-inter"
>
{{ item }}
</div>
</div>
<div class="text-red-500">
arina - doadj tu icone i style
</div>
<a
href="/lei_certificate_aurrie.pdf"
target="_blank"
rel="noopener noreferrer"
>{{ $t("Footer.Lei") }}</a>
</div>
<div
v-for="(section, index) in docs"
:key="index"
class="flex flex-col gap-[25px] sm:gap-8 max-w-[280px]"
>
<h3 class="h4-uppercase-bold-inter">
{{ section.translation }}
</h3>
<div v-for="(item, key) in section.data"
:key="key"
>
<div
class="cursor-pointer hover:text-text-light/80 dark:hover:text-text-dark/70 transition-all text-inter"
>
<a target="_blank"
:href="`/api/public/document/${item.name}`"
>{{
item.translation
}}</a>
</div>
</div>
</div>
</div>
<ClientOnly v-if="!colorMode?.forced">
<img class="cursor-pointer w-[70%] sm:w-[50%] xl:w-[30%]"
:src="isDark ? '/logo-footer-dark.svg' : '/logo-footer.svg'" alt="logo" @click="menuStore.navigateToItem()" />
<img
class="cursor-pointer w-[70%] sm:w-[50%] xl:w-[30%]"
:src="isDark ? '/logo-footer-dark.svg' : '/logo-footer.svg'"
alt="logo"
@click="menuStore.navigateToItem()"
>
</ClientOnly>
</UiContainer>
</div>
</template>
<script lang="ts" setup>
const props = defineProps<{ component: Component }>();
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: {
title: string;
children: {
title: string;
value: 'email' | 'address' | 'certificate' | 'document' | 'form' | 'info';
link?: string;
}[];
}[];
};
import type { GenericResponse, Footer } from '~/types'
const menuStore = useMenuStore()
const colorMode = useColorMode()
const store = useStore();
const open = ref(false);
const menuStore = useMenuStore();
const colorMode = useColorMode();
const { data } = await useMyFetch<GenericResponse<Footer>>(
'/api/public/front/footer',
{},
)
const isDark = computed({
get() {
return colorMode.value === "dark";
},
set(_isDark) {
colorMode.preference = _isDark ? "dark" : "light";
},
});
const contact = ref(data.data.contact)
const docs = ref(data.data.docs)
const isDark = computed(() => colorMode.value === 'dark')
</script>

View File

@ -1,25 +1,35 @@
<template>
<UiContainer>
<div class="space-25-55">
<h2 class="h2-bold-bounded">{{ component.section_lang_data.title }}</h2>
<h2 class="h2-bold-bounded">
{{ component.front_section_lang[0].data.title }}
</h2>
<div class="flex flex-col gap-10">
<div v-for="(item, index) in component.section_lang_data.faq" :key="index"
<div
v-for="(item, index) in component.front_section_lang[0].data.faq"
:key="index"
class="flex gap-8 sm:gap-20 md:gap-40 xl:gap-60 cursor-pointer"
@click="active = active === index ? 0 : index"
class="flex gap-8 sm:gap-20 md:gap-40 xl:gap-60 cursor-pointer">
>
<h4 class="h4-uppercase-bold-inter">
<span v-if="index + 1 < 10">0</span>{{ index + 1 }}
</h4>
<div :class="[
<div
:class="[
'flex justify-between w-full transition-all duration-300 gap-2 sm:gap-10 md:gap-2',
active === index && 'pb-10 border-b border-bg-dark dark:border-bg-light',
]">
active === index
&& 'pb-10 border-b border-bg-dark dark:border-bg-light',
]"
>
<div class="max-w-[1200px] flex flex-col gap-6">
<h4 :class="[
<h4
:class="[
'h4-uppercase-bold-inter transition-colors duration-300',
active === index &&
'text-accent-green-light dark:text-accent-green-dark',
]">
active === index
&& 'text-accent-green-light dark:text-accent-green-dark',
]"
>
{{ item.label }}
</h4>
@ -30,21 +40,36 @@
</transition>
</div>
<svg class="min-w-5 h-5 dark:text-bg-light" viewBox="0 0 20 21" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path :d="active === index
<svg
class="min-w-5 h-5 dark:text-bg-light"
viewBox="0 0 20 21"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
:d="
active === index
? 'M1.29289 17.4628L0.585786 18.1699L2 19.5841L2.70711 18.877L1.29289 17.4628ZM19.9706 1.19936C19.9706 0.647074 19.5228 0.199359 18.9706 0.199359L9.97056 0.19936C9.41828 0.199359 8.97056 0.647075 8.97056 1.19936C8.97056 1.75164 9.41828 2.19936 9.97056 2.19936L17.9706 2.19936L17.9706 10.1994C17.9706 10.7516 18.4183 11.1994 18.9706 11.1994C19.5228 11.1994 19.9706 10.7516 19.9706 10.1994L19.9706 1.19936ZM2 18.1699L2.70711 18.877L19.6777 1.90647L18.9706 1.19936L18.2635 0.492253L1.29289 17.4628L2 18.1699Z'
: 'M2.7364 1.49211L2.0293 0.785005L0.615083 2.19922L1.32219 2.90633L2.7364 1.49211ZM18.9999 20.1698C19.5521 20.1698 19.9999 19.7221 19.9999 19.1698L19.9999 10.1698C19.9999 9.6175 19.5521 9.16978 18.9999 9.16978C18.4476 9.16978 17.9999 9.6175 17.9999 10.1698L17.9999 18.1698L9.99986 18.1698C9.44758 18.1698 8.99986 18.6175 8.99986 19.1698C8.99986 19.7221 9.44757 20.1698 9.99986 20.1698L18.9999 20.1698ZM2.0293 2.19922L1.32219 2.90633L18.2928 19.8769L18.9999 19.1698L19.707 18.4627L2.7364 1.49211L2.0293 2.19922Z'
" fill="currentColor" />
"
fill="currentColor"
/>
</svg>
</div>
</div>
</div>
<h4 class="h4-uppercase-bold-inter sm:text-start md:text-center xl:text-start xl:ml-66">
<span v-for="(item, index) in component.section_lang_data.sub_title" :key="index" :class="{
<h4
class="h4-uppercase-bold-inter sm:text-start md:text-center xl:text-start xl:ml-66"
>
<span
v-for="(item, index) in component.front_section_lang[0].data
.sub_title"
:key="index"
:class="{
'text-accent-green-light dark:text-accent-green-dark':
item.highlight,
}">
}"
>
{{ item.text }}
</span>
</h4>
@ -53,27 +78,35 @@
</template>
<script lang="ts" setup>
const props = defineProps<{ component: Component }>();
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: {
title: string;
defineProps<{
component: {
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: {
data: {
title: string
faq: [
{
label: string;
content: string;
}
];
label: string
content: string
},
]
sub_title: [
{
text: string;
highlight: boolean;
text: string
highlight: boolean
},
]
}
];
};
};
id_front_section: number
id_lang: number
}[]
}
}>()
const active = ref<number>(0);
const active = ref<number>(0)
</script>

View File

@ -6,7 +6,7 @@
>
<h1 class="h1">
<span
v-for="(item, index) in component.section_lang_data.title"
v-for="(item, index) in component.front_section_lang[0].data.title"
:key="index"
:class="[
item.highlight
@ -16,13 +16,17 @@
]"
>
{{ item.text }}
<span v-if="index !== component.section_lang_data.title.length - 1">
</span>
<span
v-if="
index !== component.front_section_lang[0].data.title.length - 1
"
/>
</span>
</h1>
<h4 class="h4-uppercase-bold-inter">
<span
v-for="(item, index) in component.section_lang_data.sub_title"
v-for="(item, index) in component.front_section_lang[0].data
.sub_title"
:key="index"
:class="{
'text-accent-green-light dark:text-accent-green-dark':
@ -32,12 +36,12 @@
{{ item.text }}
</span>
</h4>
<p>{{ component.section_lang_data.description }}</p>
<p>{{ component.front_section_lang[0].data.description }}</p>
</div>
<div
class="w-full xl:max-w-[570px] h-[390px] sm:h-[506px] block rounded-2xl"
:style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[0]}?thumb=640x0')`,
backgroundImage: `url('/api/public/file/${component.img[0]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}"
@ -47,7 +51,7 @@
<div
class="w-full xl:max-w-[570px] h-[225px] block rounded-2xl"
:style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[1]}?thumb=640x0')`,
backgroundImage: `url('/api/public/file/${component.img[1]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}"
@ -55,9 +59,9 @@
<div
class="flex flex-col justify-between items-center space-25-55 xl:gap-4 w-auto h-auto"
>
<p>{{ component.section_lang_data.description_second }}</p>
<p>{{ component.front_section_lang[0].data.description_second }}</p>
<h4 class="h4-uppercase-bold-inter">
{{ component.section_lang_data.sub_title_second }}
{{ component.front_section_lang[0].data.sub_title_second }}
</h4>
</div>
</div>
@ -65,27 +69,31 @@
</template>
<script lang="ts" setup>
const props = defineProps<{ component: Component }>();
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: {
title: [
{
text: string;
highlight: boolean;
defineProps<{
component: {
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: {
data: {
title: {
text: string
highlight: boolean
}[]
sub_title: {
text: string
highlight: boolean
}[]
description: string
description_second: string
sub_title_second: string
}
];
sub_title: [
{
text: string;
highlight: boolean;
id_front_section: number
id_lang: number
}[]
}
];
description: string;
description_second: string;
sub_title_second: string;
};
};
}>()
</script>

View File

@ -3,7 +3,7 @@
<div class="space-25-55">
<div class="space-25-75">
<h4 class="h4-uppercase-bold-inter">
{{ component.section_lang_data.main_title }}
{{ component.front_section_lang[0].data.main_title }}
</h4>
<div
class="w-full h-full flex flex-col items-stretch gap-4 xl:flex-row space-25-55 xl:!space-y-0"
@ -11,14 +11,14 @@
<div
class="flex flex-col space-y-[55px] xl:space-y-0 xl:justify-between"
>
<p>{{ component.section_lang_data.main_description }}</p>
<p>{{ component.section_lang_data.section_title }}</p>
<p>{{ component.front_section_lang[0].data.main_description }}</p>
<p>{{ component.front_section_lang[0].data.section_title }}</p>
<ul>
<li
class=""
v-for="(item, index) in component.section_lang_data
v-for="(item, index) in component.front_section_lang[0].data
.section_items"
:key="index"
class=""
>
<span>{{ index + 1 }}. </span>{{ item }}
</li>
@ -27,50 +27,38 @@
<div
class="w-full xl:max-w-[690px] h-[200px] sm:h-[390px] block rounded-2xl"
:style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[0]}?thumb=640x0')`,
backgroundImage: `url('/api/public/file/${component.img[0]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}"
/>
</div>
</div>
<div class="flex items-center justify-center">
<div
class="flex flex-col items-center p-3 sm:p-6 md:p-8 xl:p-11 border border-block rounded-2xl gap-5"
>
<h4
class="h4-uppercase-bold-inter text-accent-green-light dark:text-accent-green-dark"
>
<div class="flex items-center justify-center w-[100%] xl:w-[90%] mx-auto">
<div class="flex flex-col items-center p-3 sm:p-6 md:p-8 xl:p-11 border border-block rounded-2xl gap-5">
<h4 class="h4-uppercase-bold-inter text-accent-green-light dark:text-accent-green-dark">
20 {{ $t("years") }}
</h4>
<img
class="dark:hidden"
:src="`/api/files/${component.image_collection}/${component.section_id}/${component.section_img[2]}?thumb=640x0')`"
alt=""
/>
<img
class="hidden dark:block"
:src="`/api/files/${component.image_collection}/${component.section_id}/${component.section_img[3]}?thumb=640x0')`"
alt=""
/>
<img class="dark:hidden" :src="`/api/public/chart?second_color=000&base_color=c7c7c7&width=1800&height=480`" alt="">
<img class="hidden dark:block" :src="`/api/public/chart?second_color=fff&base_color=9a7f62&width=1800&height=480`" alt="">
</div>
</div>
</div>
<div class="flex flex-col gap-4 xl:flex-row space-25-55">
<div class="flex flex-col space-25-55 xl:justify-between">
<p>{{ component.section_lang_data.info_description }}</p>
<p>{{ component.front_section_lang[0].data.info_description }}</p>
<h4 class="h4-uppercase-bold-inter">
{{ component.section_lang_data.info_title }}
{{ component.front_section_lang[0].data.info_title }}
</h4>
<div
v-html="component.section_lang_data.info_description_second"
class=""
></div>
v-html="component.front_section_lang[0].data.info_description_second"
/>
</div>
<div
class="w-full xl:max-w-[690px] h-[170px] sm:h-[360px] block rounded-2xl"
:style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[1]}?thumb=640x0')`,
backgroundImage: `url('/api/public/file/${component.img[1]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}"
@ -80,20 +68,27 @@
</template>
<script lang="ts" setup>
const props = defineProps<{ component: Component }>();
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: {
main_title: string;
main_description: string;
section_title: string;
section_items: [];
info_description: string;
info_title: string;
info_description_second: string;
};
};
defineProps<{
component: {
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: {
data: {
main_title: string
main_description: string
section_title: string
section_items: string[]
info_title: string
info_description: string
info_description_second: string
}
id_front_section: number
id_lang: number
}[]
}
}>()
</script>

View File

@ -3,7 +3,7 @@
<div class="space-25-55-75 max-w-[1000px] mx-auto">
<h1 class="h2-bold-bounded">
<span
v-for="(item, index) in component.section_lang_data.title"
v-for="(item, index) in component.front_section_lang[0].data.title"
:key="index"
:class="[
item.highlight
@ -13,31 +13,40 @@
]"
>
{{ item.text }}
<span v-if="index !== component.section_lang_data.title.length - 1">
</span>
<span
v-if="
index !== component.front_section_lang[0].data.title.length - 1
"
/>
</span>
</h1>
<h4 class="h4-uppercase-bold-inter">
{{ component.section_lang_data.description }}
{{ component.front_section_lang[0].data.description }}
</h4>
</div>
</UiContainer>
</template>
<script lang="ts" setup>
const props = defineProps<{ component: Component }>();
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: {
title: [
{
text: string;
highlight: boolean;
defineProps<{
component: {
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: {
data: {
title: {
text: string
highlight: boolean
}[]
description: string
}
];
description: string;
};
};
id_front_section: number
id_lang: number
}[]
}
}>()
</script>

View File

@ -1,113 +1,133 @@
<template>
<UiContainer class="space-25-75">
<h2 class="h2-bold-bounded">
<span v-for="(item, index) in component.section_lang_data.main_title" :key="index" :class="[
<span
v-for="(item, index) in component.front_section_lang[0].data.main_title"
:key="index"
:class="[
item.highlight
? 'text-accent-green-light dark:text-accent-green-dark'
: '',
'inline',
]">
]"
>
{{ item.text }}
<span v-if="index !== component.section_lang_data.main_title.length - 1">
</span>
<span
v-if="
index !== component.front_section_lang[0].data.main_title.length - 1
"
/>
</span>
</h2>
<div class="space-25-55-75">
<p>{{ component.section_lang_data.main_description }}</p>
<p>{{ component.front_section_lang[0].data.main_description }}</p>
<h4 class="h4-uppercase-bold-inter">
{{ component.section_lang_data.section_title }}
{{ component.front_section_lang[0].data.section_title }}
</h4>
</div>
<!-- products -->
<div class="space-25-55-75 flex flex-col items-center">
<div :class="[
<div
:class="[
'sm:mx-[50px] md:mx-0 xl:mx-[92px] flex items-stretch',
itemCount === 1 ? 'justify-center' : 'justify-between gap-2',
]">
<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">
<img :src="`https://www.yourgold.cz/api/public/file/${item.cover_picture_uuid}.webp`" alt="pics"
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 gap-[10px] sm:gap-[15px] w-full">
<h3 class="text-[13px] sm:text-base md:text-lg text-xl font-bold leading-[150%] text-bg-dark">
{{ item.name }}
</h3>
<p class="text-[10px] sm:text-[12px] text-sm text-bg-dark">
{{ item.tax_name }}
</p>
</div>
<div class="flex items-center justify-between">
<p class="text-accent-green-light text-bold-24">
{{ item.formatted_price }}
</p>
<button
class="w-9 h-9 md:w-12 md:h-12 rounded-xl bg-button cursor-pointer hover:bg-button-hover transition-all flex items-center justify-center">
<i class="uil uil-shopping-cart text-[25px] md:text-[24px] text-bg-light"></i>
</button>
</div>
</div>
</div>
]"
>
<Product v-for="product in productStore.productList" :key="product.id" :product="product" />
</div>
<UiButtonArrow :arrow="true" class="mx-auto" type="fill">{{ $t('eshop') }}</UiButtonArrow>
<nuxt-link :to="{ name: `id-slug___${$i18n.locale}`, params: { id: menuStore.getShopMenu()?.id, slug: menuStore.getShopMenu()?.front_menu_lang.at(0)?.link_rewrite } }">
<UiButtonArrow type="fill" :arrow="true">
{{ $t("eshop") }}
</UiButtonArrow>
</nuxt-link>
</div>
<!-- calculator-block -->
<div class="flex flex-col xl:flex-row items-stretch gap-6 sm:gap-2 pt-5 sm:p-0 space-25-55">
<div
class="flex flex-col xl:flex-row items-stretch gap-6 sm:gap-2 pt-5 sm:p-0 space-25-55"
>
<div class="flex flex-col space-y-[55px] sm:justify-between">
<div class="space-25-55">
<p>{{ component.section_lang_data.section_description }}</p>
<p>{{ component.front_section_lang[0].data.section_description }}</p>
<h4 class="h4-uppercase-bold-inter">
{{ component.section_lang_data.info_title }}
{{ component.front_section_lang[0].data.info_title }}
</h4>
<p>{{ component.section_lang_data.info_description }}</p>
<p>{{ component.front_section_lang[0].data.info_description }}</p>
</div>
<h4 class="h4-uppercase-bold-inter">
{{ component.section_lang_data.cta_title }}
{{ component.front_section_lang[0].data.cta_title }}
</h4>
</div>
<!-- calculator -->
<div class="w-full md:min-w-[680px] p-[25px] md:p-[50px] border border-button rounded-2xl block">
<div
class="w-full md:min-w-[680px] p-[25px] md:p-[50px] border border-button rounded-2xl block"
>
<h2 class="h2-bold-bounded text-center mb-10 sm:mb-20">
{{ component.section_lang_data.calculator_title }}
{{ component.front_section_lang[0].data.calculator_title }}
</h2>
<div class="mb-14 flex flex-col gap-8 sm:gap-14">
<div class="flex flex-col gap-4">
<div class="flex justify-between">
<p>{{ $t("monthly_savings") }}</p>
<p class="text-accent-green-light dark:text-accent-green-dark font-bold">
{{ store.monthlySavings }} {{ menuStore.selectedCurrency?.sign }}
<p
class="text-accent-green-light dark:text-accent-green-dark font-bold"
>
{{ store.monthlySavings }}
{{ menuStore.selectedCurrency?.sign }}
</p>
</div>
<input v-model="store.monthlySavings" type="range" max="600" :min="store.minValue"
class="w-full accent-button cursor-pointer" @mouseup="store.getCalculator()"
@touchend="store.getCalculator()" />
<input
v-model="store.monthlySavings"
type="range"
max="600"
:min="store.minValue"
class="w-full accent-button cursor-pointer"
@mouseup="store.getCalculator()"
@touchend="store.getCalculator()"
>
</div>
<div class="flex flex-col gap-4">
<div class="flex justify-between">
<p>{{ $t("storage_period_years") }}</p>
<p class="text-accent-green-light dark:text-accent-green-dark font-bold">
<p
class="text-accent-green-light dark:text-accent-green-dark font-bold"
>
{{ store.storagePeriod }}
</p>
</div>
<input v-model="store.storagePeriod" type="range" max="20" class="w-full accent-button cursor-pointer"
@mouseup="store.getCalculator()" @touchend="store.getCalculator()" />
<input
v-model="store.storagePeriod"
type="range"
max="20"
class="w-full accent-button cursor-pointer"
@mouseup="store.getCalculator()"
@touchend="store.getCalculator()"
>
</div>
</div>
<div class="flex flex-col items-start sm:flex-row gap-6 sm:gap-1 justify-between sm:items-center">
<div
class="flex flex-col items-start sm:flex-row gap-6 sm:gap-1 justify-between sm:items-center"
>
<div class="">
<p>{{ $t("expected_savings_value") }}</p>
<h2 class="h2-bold-bounded text-accent-green-light dark:text-accent-green-dark">
<h2
class="h2-bold-bounded text-accent-green-light dark:text-accent-green-dark"
>
{{ menuStore.selectedCurrency?.sign }} {{ store.totalInvestment }}
</h2>
</div>
<UiButtonArrow :arrow="true" type="fill" class="mx-auto sm:m-0">{{
component.section_lang_data.button
}}</UiButtonArrow>
<UiButtonArrow :arrow="true"
type="fill"
class="mx-auto sm:m-0"
>
{{
component.front_section_lang[0].data.button
}}
</UiButtonArrow>
</div>
</div>
</div>
@ -115,54 +135,62 @@
</template>
<script lang="ts" setup>
const props = defineProps<{ component: Component }>();
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: {
main_title: [
{
text: string;
highlight: boolean;
import Product from './Product.vue'
defineProps<{
component: {
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: {
data: {
main_title: {
text: string
highlight: boolean
}[]
main_description: string
section_title: string
section_description: string
info_title: string
info_description: string
cta_title: string
calculator_title: string
button: string
}
];
main_description: string;
section_title: string;
section_description: string;
info_title: string;
info_description: string;
cta_title: string;
calculator_title: string;
button: string;
};
};
id_front_section: number
id_lang: number
}[]
}
}>()
const store = useStore();
const menuStore = useMenuStore();
const store = useStore()
const menuStore = useMenuStore()
const itemCount = ref(4);
const productStore = useProductStore();
const itemCount = ref(4)
const productStore = useProductStore()
async function updateItemCount() {
const width = window.innerWidth;
if (width >= 1800) itemCount.value = 5;
else if (width >= 1200) itemCount.value = 4;
else if (width >= 768) itemCount.value = 3;
else if (width >= 640) itemCount.value = 2;
else itemCount.value = 1;
const width = window.innerWidth
if (width >= 1800) itemCount.value = 5
else if (width >= 1600) itemCount.value = 4
else if (width >= 1200) itemCount.value = 3
else if (width >= 640) itemCount.value = 2
else itemCount.value = 1
}
watch(itemCount, async () => {
await productStore.getList(itemCount.value);
});
await productStore.getList(itemCount.value)
})
onMounted(async () => {
await updateItemCount();
window.addEventListener("resize", updateItemCount);
});
await updateItemCount()
window.addEventListener('resize', updateItemCount)
})
onBeforeUnmount(() => {
window.removeEventListener("resize", updateItemCount);
});
window.removeEventListener('resize', updateItemCount)
})
</script>

View File

@ -1,54 +1,117 @@
<template>
<UiContainer class="flex py-20 sm:py-14">
<UiContainer v-if="!userStore.vCodeVerify" class="flex py-20 sm:py-14">
<div class="hidden xl:block rounded-2xl min-w-[60%] h-[830px]" :style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[0]}?thumb=1200x0')`,
backgroundImage: `url('/api/public/file/${component.img[0]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}" />
<div class="w-full sm:w-[80%] mx-auto my-auto xl:w-full xl:px-12">
<div class="space-25-55">
<div class="flex flex-wrap-reverse gap-y-4 justify-between">
<h2 class="h2-bold-bounded">{{ $t('login') }}</h2>
<h2 class="h2-bold-bounded">
{{ $t("login") }}
</h2>
<button
class="h-[40px] sm:h-[43px] px-[10px] sm:px-[17px] border border-gray dark:border-button-disabled text-gray dark:text-button-disabled hover:bg-gray transition-all hover:text-white rounded-[8px] cursor-pointer">{{
$t('back_to_home') }}</button>
class="h-[40px] sm:h-[43px] px-[10px] sm:px-[17px] border border-gray dark:border-button-disabled text-gray dark:text-button-disabled hover:bg-gray transition-all hover:text-white rounded-[8px] cursor-pointer"
@click="menuStore.navigateToItem()">
{{ $t("back_to_home") }}
</button>
</div>
<div class="space-y-[15px]">
<p class="pl-6">{{ $t('email') }}</p>
<input v-model="store.email" :placeholder="$t('email')" type="text"
class="border-2 border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2" />
<p class="pl-6">
{{ $t("email") }}
</p>
<input data-testid="login-email" v-model="userStore.email" :placeholder="$t('email')" type="text"
class="border-2 border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2">
</div>
<div class="space-y-[15px]">
<p class="pl-6">{{ $t('password') }}</p>
<input v-model="store.password" :placeholder="$t('placeholder_password')" type="text"
class="border-2 border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2" />
<p class="pl-6">
{{ $t("password") }}
</p>
<input data-testid="login-password" v-model="userStore.password" :placeholder="$t('placeholder_password')" type="text"
class="border-2 border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2">
</div>
</div>
<p class="text-button hover:text-button-hover transition-all font-medium mt-[30px] cursor-pointer">{{
$t('forgot_password_question')
}}</p>
<p class="text-button hover:text-button-hover transition-all font-medium mt-[30px] cursor-pointer">
{{ $t("forgot_password_question") }}
</p>
<div class="py-[25px] sm:py-12 border-b border-gray flex justify-center w-full">
<UiButtonArrow @click="store.logIn()" type="fill" :arrow="true">{{ $t('login') }}</UiButtonArrow>
<UiButtonArrow data-testid="login-btn" type="fill" :arrow="true" @click="userStore.logIn()">
{{
$t("login")
}}
</UiButtonArrow>
</div>
<div class="mt-[25px] sm:mt-[30px] w-full flex justify-center gap-3">
<p class="cursor-pointer hover:underline transition-all">{{
$t('no_account')
}}</p>
<p class="text-button cursor-pointer hover:text-button-hover">{{
$t('sign_up_now')
}}</p>
<p>
{{ $t("no_account") }}
</p>
<nuxt-link :to="{ name: `id-slug___${$i18n.locale}`, params: { id: menuStore.getRegistrationMenu()?.id, slug: menuStore.getRegistrationMenu()?.front_menu_lang.at(0)?.link_rewrite } }">
<p class="text-button cursor-pointer hover:text-button-hover">
{{ $t("sign_up_now") }}
</p>
</nuxt-link>
</div>
</div>
</UiContainer>
<UiContainer v-if="userStore.vCodeVerify" class="flex py-20 sm:py-14">
<div class="hidden xl:block rounded-2xl min-w-[60%] h-[830px]" :style="{
backgroundImage: `url('/api/public/file/${component.img[0]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}" />
<div class="w-full sm:w-[80%] mx-auto my-auto xl:w-full xl:px-12">
<div class="space-25-55">
<div class="flex flex-wrap-reverse gap-y-4 justify-between">
<h2 class="h2-bold-bounded">
{{ $t("verify_login") }}
</h2>
<button
class="h-[40px] sm:h-[43px] px-[10px] sm:px-[17px] border border-gray dark:border-button-disabled text-gray dark:text-button-disabled hover:bg-gray transition-all hover:text-white rounded-[8px] cursor-pointer"
@click="menuStore.navigateToItem()">
{{ $t("back_to_home") }}
</button>
</div>
<div class="space-y-[30px]">
<p>
{{ $t("send_code") }}
<span class="text-button font-semibold">{{ userStore.email }}</span>
{{ $t("by_email") }}
</p>
<div class="space-y-[15px]">
<p class="pl-6">
{{ $t("code") }}
</p>
<input data-testid="code-input" v-model="userStore.vCode" :placeholder="$t('code')" type="text"
class="border-2 border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2">
</div>
</div>
</div>
<div class="py-[25px] sm:py-12 flex justify-center w-full">
<UiButtonArrow data-testid="code-confirm" type="fill" :arrow="true" @click="userStore.sendFormCode(true)">
{{ $t("confirm") }}
</UiButtonArrow>
</div>
</div>
</UiContainer>
</template>
<script lang="ts" setup>
defineProps<{ component: Component }>();
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: {};
};
const store = useStore()
defineProps<{
component: {
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: {
id_front_section: number
id_lang: number
}[]
}
}>()
const userStore = useUserStore()
const menuStore = useMenuStore()
</script>

View File

@ -1,62 +1,54 @@
<template>
<UiContainer
class="flex flex-wrap items-center justify-between gap-[30px] sm:gap-y-[55px] xl:gap-y-[100px]"
>
<div
class="w-full flex flex-col gap-[25px] xl:max-w-[48%] md:mx-10 xl:m-0"
v-for="(item, index) in component.section_lang_data"
:key="index"
>
<UiContainer class="flex flex-wrap items-center justify-between gap-[30px] sm:gap-y-[55px] xl:gap-y-[100px]">
<div v-for="(item, index) in component.front_section_lang[0].data" :key="index"
class="w-full flex flex-col gap-[25px] xl:max-w-[48%] md:mx-10 xl:m-0">
<!-- xl -->
<div class="hidden xl:flex xl:h-[330px] flex-col justify-between">
<div class="space-y-[55px]">
<h2 class="h2-bold-bounded">{{ item.title }}</h2>
<h2 class="h2-bold-bounded">
{{ item.title }}
</h2>
<p>{{ item.description }}</p>
</div>
<h4 class="h4-uppercase-bold-inter">{{ item.sub_title }}</h4>
<h4 class="h4-uppercase-bold-inter">
{{ item.sub_title }}
</h4>
</div>
<!-- sm/md -->
<div class="xl:hidden flex flex-col gap-y-[25px] sm:gap-y-[55px]">
<h2 class="h2-bold-bounded">{{ item.title }}</h2>
<h2 class="h2-bold-bounded">
{{ item.title }}
</h2>
<p>{{ item.description }}</p>
<h4 class="h4-uppercase-bold-inter">{{ item.sub_title }}</h4>
<h4 class="h4-uppercase-bold-inter">
{{ item.sub_title }}
</h4>
</div>
<div
class="h-[235px] sm:h-[350px] w-full rounded-[20px] bg-cover bg-center transition-transform duration-300 group-hover:scale-105 xl:block relative"
:style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[index]}?thumb=640x0')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}"
>
<div
class="hidden sm:block absolute bottom-0 right-0 pt-2 pl-2 bg-bg-light dark:bg-bg-dark rounded-tl-2xl"
>
<UiButtonArrow :arrow="true">{{ item.title }}</UiButtonArrow>
</div>
</div>
<UiButtonArrow :arrow="true" class="sm:hidden mx-auto">{{
item.title
}}</UiButtonArrow>
<UiImgWrapper :src="`/api/public/file/${component.img[index]}_l.webp`">
<template #button>
<nuxt-link :to="{ name: `id-slug___${$i18n.locale}`, params: { id: linksInfo[index].id, slug: linksInfo[index].slug } }">
<UiButtonArrow :arrow="true">
{{ item.title }}
</UiButtonArrow>
</nuxt-link>
</template>
</UiImgWrapper>
</div>
<!-- Map block with same layout rules -->
<div
class="w-full xl:max-w-[48%] md:mx-10 xl:m-0 flex flex-col gap-4 items-center"
>
<div class="w-full xl:max-w-[48%] md:mx-10 xl:m-0 flex flex-col gap-4 items-center">
<h1 class="text-sm sm:text-xl uppercase">
Jsme tu pro vás napříč Evropou
</h1>
<MapBlock />
<div class="flex items-center gap-4 w-full justify-end">
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-[2.8px] bg-button"></div>
<div class="w-3 h-3 rounded-[2.8px] bg-button" />
<p>Partners</p>
</div>
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-[2.8px] bg-block"></div>
<div class="w-3 h-3 rounded-[2.8px] bg-block" />
<p>Customers</p>
</div>
</div>
@ -66,21 +58,49 @@
<script lang="ts" setup>
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: [
{
title: string;
description: string;
sub_title: string;
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: {
data: {
title: string
description: string
sub_title: string
}[]
id_front_section: number
id_lang: number
}[]
}
];
};
const props = defineProps<{
component: Component;
data: Object;
infoData: Object;
}>();
defineProps<{
component: Component
}>()
const menuStore = useMenuStore()
const linksInfo = ref([
{
id: menuStore.getInvestitionMenu()?.id,
slug: menuStore.getInvestitionMenu()?.front_menu_lang.at(0)?.link_rewrite,
},
{
id: menuStore.getAboutGoldMenu()?.id,
slug: menuStore.getAboutGoldMenu()?.front_menu_lang.at(0)?.link_rewrite,
},
{
id: menuStore.getBusinessMenu()?.id,
slug: menuStore.getBusinessMenu()?.front_menu_lang.at(0)?.link_rewrite,
},
{
id: menuStore.getAboutUsMenu()?.id,
slug: menuStore.getAboutUsMenu()?.front_menu_lang.at(0)?.link_rewrite,
},
{
id: menuStore.getContactMenu()?.id,
slug: menuStore.getContactMenu()?.front_menu_lang.at(0)?.link_rewrite,
},
])
</script>

View File

@ -2,12 +2,12 @@
<UiContainer>
<div class="space-y-[30px] sm:space-y-[53px]">
<h1 class="h1">
{{ component.section_lang_data.title }}
{{ component.front_section_lang[0].data.title }}
</h1>
<div
class="h-[465px] sm:h-[509px] xl:h-[509px] w-full rounded-[20px] bg-cover bg-center transition-transform duration-300 group-hover:scale-105 xl:block"
:style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[0]}?thumb=1200x0')`,
backgroundImage: `url('/api/public/file/${component.img[0]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}"
@ -16,12 +16,18 @@
class="flex flex-col items-center space-y-[30px] sm:flex-row sm:items-start md:space-y-0 xl:items-center"
>
<h3 class="h4-uppercase-bold-inter sm:min-w-[45%]">
{{ component.section_lang_data.title_second }}
{{ component.front_section_lang[0].data.title_second }}
</h3>
<div class="flex w-full items-start justify-center sm:justify-end">
<UiButtonArrow :arrow="true" type="fill">{{
component.section_lang_data.button
}}</UiButtonArrow>
<nuxt-link :to="{ name: `id-slug___${$i18n.locale}`, params: { id: menuStore.getRegistrationMenu()?.id, slug: menuStore.getRegistrationMenu()?.front_menu_lang.at(0)?.link_rewrite } }">
<UiButtonArrow :arrow="true"
type="fill"
>
{{
component.front_section_lang[0].data.button
}}
</UiButtonArrow>
</nuxt-link>
</div>
</div>
</div>
@ -29,15 +35,25 @@
</template>
<script lang="ts" setup>
const props = defineProps<{ component: Component }>();
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: {
title: string;
title_second: string;
button: string;
};
};
defineProps<{
component: {
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: {
data: {
title: string
button: string
title_second: string
}
id_front_section: number
id_lang: number
}[]
}
}>()
const menuStore = useMenuStore()
</script>

View File

@ -1,85 +1,68 @@
<template>
<UiContainer class="space-y-[40px] sm:space-y-[55px] md:space-y-[75px]">
<!-- product -->
<div class="space-25-55-75 flex flex-col items-center">
<div :class="[
'sm:mx-[50px] md:mx-0 xl:mx-[92px] flex items-stretch',
itemCount === 1 ? 'justify-center' : 'justify-between gap-2',
]">
<!-- product -->
<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">
<img :src="`https://www.yourgold.cz/api/public/file/${item.cover_picture_uuid}.webp`" alt="pics"
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 gap-[10px] sm:gap-[15px] w-full">
<h3 class="text-[13px] sm:text-base md:text-lg text-xl font-bold leading-[150%] text-bg-dark">
{{ item.name }}
</h3>
<p class="text-[10px] sm:text-[12px] text-sm text-bg-dark">
{{ item.tax_name }}
</p>
</div>
<div class="flex items-center justify-between">
<p class="text-accent-green-light text-bold-24">
{{ item.formatted_price }}
</p>
<button
class="w-9 h-9 md:w-12 md:h-12 rounded-xl bg-button cursor-pointer hover:bg-button-hover transition-all flex items-center justify-center">
<i class="uil uil-shopping-cart text-[25px] md:text-[24px] text-bg-light"></i>
</button>
</div>
</div>
<Product v-for="product in productStore.productList" :key="product.id" :product="product" />
</div>
</div>
<div class="flex flex-col gap-6 md:flex-row items-center justify-between">
<h3 class="h4-uppercase-bold-inter w-full text-center md:text-start xl:max-w-[50%]">
Zlato je jistota, která nepodléhá času. Udělejte dnes rozhodnutí, které
vás ochrání zítra
</h3>
<UiButtonArrow @click="menuStore.navigateToShop" type="fill" :arrow="true">{{ $t('eshop') }}</UiButtonArrow>
<nuxt-link
:to="{ name: `id-slug___${$i18n.locale}`, params: { id: menuStore.getShopMenu()?.id, slug: menuStore.getShopMenu()?.front_menu_lang.at(0)?.link_rewrite } }">
<UiButtonArrow type="fill" :arrow="true">
{{ $t("eshop") }}
</UiButtonArrow>
</nuxt-link>
</div>
</UiContainer>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import { ref, onMounted, onBeforeUnmount } from 'vue'
import Product from './Product.vue'
defineProps<{ component: Component }>();
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: [
{
title: string;
description: string;
sub_title: string;
defineProps<{
component: {
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
}
];
};
}>()
const menuStore = useMenuStore()
const itemCount = ref(4);
const productStore = useProductStore();
const itemCount = ref(4)
const productStore = useProductStore()
async function updateItemCount() {
const width = window.innerWidth;
if (width >= 1800) itemCount.value = 5;
else if (width >= 1200) itemCount.value = 4;
else if (width >= 768) itemCount.value = 3;
else if (width >= 640) itemCount.value = 2;
else itemCount.value = 1;
const width = window.innerWidth
if (width >= 1800) itemCount.value = 5
else if (width >= 1600) itemCount.value = 4
else if (width >= 1200) itemCount.value = 3
else if (width >= 640) itemCount.value = 2
else itemCount.value = 1
}
watch(itemCount, async () => {
await productStore.getList(itemCount.value);
});
await productStore.getList(itemCount.value)
})
onMounted(async () => {
await updateItemCount();
window.addEventListener("resize", updateItemCount);
});
await updateItemCount()
window.addEventListener('resize', updateItemCount)
})
onBeforeUnmount(() => {
window.removeEventListener("resize", updateItemCount);
});
window.removeEventListener('resize', updateItemCount)
})
</script>

View File

@ -1,41 +1,65 @@
<template>
<div>
<nuxt-link :to="{
name: `id-slug___${$i18n.locale}`,
params: {
id: menuStore.getProductMenu()?.id,
slug: menuStore.getProductMenu()?.front_menu_lang.at(0)?.link_rewrite,
},
query: {
prod_id: product.id,
name: product.link_rewrite,
},
}">
<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]">
<img :src="`https://www.yourgold.cz/api/public/file/${props.product?.cover_picture_uuid}.webp`" alt="pics"
class="max-h-[95px] sm:max-h-[180px] md:max-h-[205px] rounded-[5px]" />
<div class="flex flex-col justify-between h-full w-full gap-[7px] sSm:gap-[15px]"
@click="productStore.addToCart(props.product)">
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] h-full">
<img :src="`/api/public/file/${product.cover_picture_uuid}.webp`" alt="Product Image"
class="h-[95px] sm:h-[180px] md:h-[205px] rounded-[5px]" @error="handleImageError" />
<div class="flex flex-col justify-between h-full w-full gap-[7px] sm:gap-[15px]">
<div class="flex flex-col gap-[7px] sm:gap-[15px] w-full">
<h3 class="text-[10px] sm:text-base md:text-lg text-xl font-bold leading-[130%] sm:leading-[150%] text-bg-dark">
{{ props.product?.name }}
<h3
class="text-[10px] sm:text-base md:text-lg text-xl font-bold leading-[130%] sm:leading-[150%] text-bg-dark">
{{ product.name }}
</h3>
<p class="text-[9px] sm:text-[12px] text-sm text-bg-dark">
{{ props.product?.tax_name }}
{{ product.tax_name }}
</p>
</div>
<div class="flex items-center justify-between">
<p class="text-accent-green-light font-inter text-[12px] sm:text-[21px] md:text-2xl leading-[150%] font-bold">
{{ props.product?.formatted_price }}
<p
class="text-accent-green-light font-inter text-[12px] sm:text-[21px] md:text-2xl leading-[150%] font-bold">
{{ product.formatted_price }}
</p>
<button
class="w-[22px] h-[22px] sm:w-9 sm:h-9 md:w-12 md:h-12 rounded-[5px] sm:rounded-xl bg-button cursor-pointer hover:bg-button-hover transition-all flex items-center justify-center p-1">
<i class="uil uil-shopping-cart text-[10px] sm:text-[25px] md:text-2xl cursor-pointer"></i>
class="w-[22px] h-[22px] sm:w-9 sm:h-9 md:w-12 md:h-12 rounded-[5px] text-bg-light sm:rounded-xl bg-button cursor-pointer hover:bg-button-hover transition-all flex items-center justify-center p-1"
@click.stop.prevent="productStore.incrementCartItem(product.id)">
<i class="uil uil-shopping-cart text-lg sm:text-2xl md:text-[31px]" />
</button>
</div>
</div>
</div>
</nuxt-link>
</div>
</template>
<script setup lang="ts">
// import imgUrl from "~/utils/imgUrl";
const props = defineProps({
product: Object,
});
interface Product {
id: number
name: string
link_rewrite: string
tax_name: string
formatted_price: string
cover_picture_uuid: string
}
defineProps<{ product: Product }>()
const productStore = useProductStore()
const menuStore = useMenuStore()
// const addToCartAndPreventNavigation = (event: any) => {
// event.preventDefault();
// useCartStore().addToCart(props.product);
// };
function handleImageError(event: Event) {
const img = event.target as HTMLImageElement
img.src = '/photo.svg'
}
</script>

View File

@ -0,0 +1,300 @@
<template>
<UiContainer>
<transition>
<div v-if="showPopup && curImage" ref="dropdownRef" tabindex="0"
class="fixed top-0 left-0 bg-[#000000cc] w-screen h-screen z-50 flex items-center justify-center cursor-pointer p-10"
@keyup.esc="showPopup = false" @keyup.left="goLeft" @keyup.right="goRight" @click.self="showPopup = false">
<div class="border border-white p-4 rounded-2xl shadow-2xl bg-white cursor-auto">
<img :src="`/api/public/file/${curImage}_l.webp`" class="max-h-[80vh] object-contain" alt=""
@load="focusOnImgLoad">
</div>
<button class="absolute top-8 p-8 right-8 text-4xl rotate-45 text-white" @click="showPopup = false">
+
</button>
</div>
</transition>
<div class="space-y-10 sm:space-y-[75px]">
<div class="flex flex-col items-start xl:flex-row gap-[40px] sm:gap-[100px] border-b-2 border-block pb-[25px]">
<!-- images block -->
<button
class="xl:hidden h-[40px] sm:h-[43px] px-[10px] sm:px-[17px] border border-gray dark:border-button-disabled text-gray dark:text-button-disabled hover:bg-gray transition-all hover:text-white rounded-[8px] cursor-pointer"
@click="menuStore.navigateToItem()">
{{ $t("back_to_home") }}
</button>
<div class="w-full flex-col-reverse sm:flex-row sm:w-[70%] mx-auto justify-center xl:w-auto flex gap-[25px]">
<div class="flex justify-center sm:justify-start sm:flex-col gap-[15px]">
<div v-for="image in product.picture_uuids" :key="image"
:class="['cursor-pointer hover:border-button-disabled transition-all h-[100px] w-[90px] border py-3 flex items-center justify-center rounded-[8px]', image === curImage ? 'border-block' : 'border-button']"
@click="curImage = image">
<img :src="`/api/public/file/${image}_m.webp`" :alt="product.name" srcset="" class="h-full w-auto">
</div>
</div>
<div class="cursor-pointer h-full flex justify-center sm:items-start" @click="showPopup = true">
<img :src="`/api/public/file/${curImage}_m.webp`"
class="sm:min-w-[400px] sm:max-w-[410px] xl:min-w-[300px] xl:max-w-[320px] w-full h-auto" alt="">
</div>
</div>
<!-- info block -->
<div class="w-full space-y-[50px]">
<div class="w-full space-y-[30px]">
<div class="space-y-[15px]">
<div class="flex items-center justify-between w-full">
<h1 class="h4-uppercase-bold-inter max-w-[450px]">
{{ product.name }}
</h1>
<button
class="hidden xl:block h-[40px] sm:h-[43px] px-[10px] sm:px-[17px] border border-gray dark:border-button-disabled text-gray dark:text-button-disabled hover:bg-gray transition-all hover:text-white rounded-[8px] cursor-pointer"
@click="menuStore.navigateToItem()">
{{ $t("back_to_home") }}
</button>
</div>
<p class="text-button font-inter text-base leading-[150%] uppercase sm:text-[20px]">
Title: {{
product.reference }}
</p>
</div>
<div class="flex flex-col gap-5" v-html="product.description" />
<div
class="flex flex-col sm:flex-row sm:items-end gap-[25px] sm:justify-between border-t-2 border-block pt-[15px]">
<div class="space-y-4">
<div class="space-y-2">
<h4 class="text-accent-green-light font-inter text-2xl leading-[150%] font-bold">
{{ formattedPrice }}
</h4>
<p>{{ product.tax_name }}</p>
</div>
<p class="font-medium">
{{ component.front_section_lang[0].data.purchase_price }}<span
class="text-button">{{ formattedBuyOutPrice
}}</span>
</p>
</div>
<div class="mx-auto sm:mx-0 group flex cursor-pointer items-center justify-start gap-2 whitespace-nowrap">
<button
class="h-[40px] cursor-pointer min-w-40 rounded-[10px] px-[22px] transition-all sm:h-[50px] md:h-[65px] md:rounded-[15px] md:px-[42px] bg-button text-text-dark group-hover:bg-button-hover">
Add to cart
</button>
<div :disabled="!product.sale_active" class="
flex h-[40px] w-[40px] items-center justify-center rounded-[10px] p-2.5 transition-all sm:h-[50px] sm:w-[50px] md:h-[65px] md:w-[65px] md:rounded-[15px] bg-button text-text-dark group-hover:bg-button-hover
" @click="productStore.incrementCartItem(product.id)">
<i class="uil uil-shopping-cart text-[23px] sm:text-[33px]"></i>
</div>
</div>
</div>
</div>
<div class="flex flex-col gap-5">
<h1 class="h4-uppercase-bold-inter max-w-[450px]">
{{ component.front_section_lang[0].data.product_details }}
</h1>
<div class="flex flex-col gap-5 mx-[50px]">
<div v-for="feature in product.features" :key="feature.feature" class="flex gap-4 justify-between">
<span class="mx-4">{{ feature.feature }}:</span>
<span class="mx-4">
{{ feature.value }}
</span>
</div>
</div>
</div>
</div>
</div>
<div class="space-y-[55px]">
<h1 class="h4-uppercase-bold-inter max-w-[450px]">
{{ component.front_section_lang[0].data.recommendations }}
</h1>
<!-- products -->
<div class="flex flex-wrap justify-center gap-5 sm:gap-10">
<div v-for="(item, index) in product.recommendations" :key="index">
<nuxt-link :to="{
name: `id-slug___${$i18n.locale}`,
params: {
id: menuStore.getProductMenu()?.id,
slug: menuStore.getProductMenu()?.front_menu_lang[0]
.link_rewrite,
},
query: { prod_id: item?.id, name: item?.link_rewrite },
}">
<div
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 h-full">
<img :src="`/api/public/file/${item.cover_picture_uuid}.webp`" alt="pics"
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 gap-[10px] sm:gap-[15px] w-full">
<h3 class="text-[13px] sm:text-base md:text-lg text-xl font-bold leading-[150%] text-bg-dark">
{{ item.name }}
</h3>
<p class="text-[10px] sm:text-[12px] text-sm text-bg-dark">
{{ item.tax_name }}
</p>
</div>
<div class="flex items-center justify-between">
<p class="text-accent-green-light text-bold-24">
{{ item.formatted_price }}
</p>
<button
class="w-9 h-9 md:w-12 md:h-12 rounded-xl bg-button cursor-pointer hover:bg-button-hover transition-all flex items-center justify-center"
@click="productStore.incrementCartItem(item.id)">
<i class="uil uil-shopping-cart text-[25px] md:text-[24px] text-bg-light" />
</button>
</div>
</div>
</div>
</nuxt-link>
</div>
</div>
</div>
</div>
</UiContainer>
</template>
<script setup lang="ts">
import type { GenericResponse, ProductItem } from '~/types'
defineProps<{
component: {
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: {
data: {
purchase_price: string
product_details: string
recommendations: string
}
id_front_section: number
id_lang: number
}[]
}
}>()
const menuStore = useMenuStore()
const { $session } = useNuxtApp()
const productStore = useProductStore()
const dropdownRef = ref()
// const addToCart = (p: ProductItem) => {
// alert('add to cart product: ' + p.name)
// }
const formatPrice = (p: number) => {
const formatdecimal = new Intl.NumberFormat(
$session.cookieData.value.country.iso_code,
{
style: 'decimal',
maximumFractionDigits: $session.cookieData.value.currency.precision,
},
).format(p)
return $session.cookieData.value.currency.suffix
? $session.cookieData.value.currency.sign + ' ' + formatdecimal
: formatdecimal + ' ' + $session.cookieData.value.currency.sign
}
const focusOnImgLoad = () => {
dropdownRef.value.focus()
}
const formattedPrice = computed(() => {
return formatPrice(product.value.price)
})
const formattedBuyOutPrice = computed(() => {
return formatPrice(product.value.buyout_price)
})
const route = useRoute()
// if (!route.query?.prod_id) {
// throw createError({
// statusCode: 404,
// statusMessage: 'Not Found',
// fatal: true
// });
// }
const getProduct = async (): Promise<ProductItem> => {
if (route.query?.prod_id) {
const data = await useMyFetch<GenericResponse<ProductItem>>(
`/api/public/product/${route.query?.prod_id}`,
{},
)
// await new Promise(resolve => setTimeout(resolve, 200));
return data.data
}
return {} as ProductItem
}
const product = ref(await getProduct())
watch(
() => route.query?.prod_id,
async () => {
const data = await getProduct()
product.value = data
if (curImage.value != data.picture_uuids[0]) {
curImage.value = data.cover_picture_uuid
}
},
)
watch($session.cookieData, async () => {
const oldId = product.value.id
const data = await getProduct()
product.value = data
if (oldId != data.id) {
curImage.value = data.cover_picture_uuid
}
})
const showPopup = ref(false)
const curImage = ref(product.value.cover_picture_uuid)
watch(showPopup, (newValue) => {
if (newValue) {
document.body.style.overflow = 'hidden'
}
else {
document.body.style.overflow = 'unset'
}
})
const goLeft = () => {
if (!curImage.value) return
const index = product.value.picture_uuids.indexOf(curImage.value)
if (index > 0) {
curImage.value = product.value.picture_uuids[index - 1]
}
else {
curImage.value
= product.value.picture_uuids[product.value.picture_uuids.length - 1]
}
}
const goRight = () => {
if (!curImage.value) return
const index = product.value.picture_uuids.indexOf(curImage.value)
if (index < product.value.picture_uuids.length - 1) {
curImage.value = product.value.picture_uuids[index + 1]
}
else {
curImage.value = product.value.picture_uuids[0]
}
}
</script>
<style>
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>

View File

@ -1,100 +1,170 @@
<template>
<UiContainer class="flex py-[15px] xl:py-20 sm:py-0">
<div class="hidden xl:block rounded-2xl min-w-[40%] h-[830px]" :style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[0]}?thumb=1200x0')`,
<div
class="hidden xl:block rounded-2xl min-w-[40%] h-[830px]"
:style="{
backgroundImage: `url('/api/public/file/${component.img[0]}_l.webp')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}" />
}"
/>
<div class="w-full sm:w-[80%] mx-auto my-auto xl:w-full xl:px-12">
<div class="space-25-55">
<div class="flex justify-between">
<h2 class="h2-bold-bounded">{{ $t('sign_up') }}</h2>
<div class="flex flex-wrap-reverse gap-y-4 justify-between">
<h2 class="h2-bold-bounded">
{{ $t("sign_up") }}
</h2>
<button
class="h-[40px] sm:h-[43px] px-[10px] sm:px-[17px] border border-gray dark:border-button-disabled text-gray dark:text-button-disabled hover:bg-gray transition-all hover:text-white rounded-[8px] cursor-pointer">{{
$t('back_to_home') }}</button>
class="h-[40px] sm:h-[43px] px-[10px] sm:px-[17px] border border-gray dark:border-button-disabled text-gray dark:text-button-disabled hover:bg-gray transition-all hover:text-white rounded-[8px] cursor-pointer"
@click="menuStore.navigateToItem()"
>
{{ $t("back_to_home") }}
</button>
</div>
<div class="space-y-[25px] sm:space-y-[30px]">
<p>Obecné informace</p>
<p>{{ $t("current_information") }}</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-[30px]">
<div class="space-y-[15px]">
<p class="pl-6">{{ $t('first_name') }}</p>
<input :placeholder="$t('first_name')" type="text"
class="text-sm sm:text-xl border-2 border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2" />
<p class="pl-6">
{{ $t("first_name") }}
</p>
<input
:placeholder="$t('first_name')"
type="text"
class="text-sm sm:text-xl border border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2"
>
</div>
<div class="space-y-[15px]">
<p class="pl-6">{{ $t('last_name') }}</p>
<input :placeholder="$t('last_name')" type="text"
class="text-sm sm:text-xl border-2 border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2" />
<p class="pl-6">
{{ $t("last_name") }}
</p>
<input
:placeholder="$t('last_name')"
type="text"
class="text-sm sm:text-xl border-2 border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2"
>
</div>
<div class="space-y-[15px]">
<p class="pl-6">{{ $t('email') }}</p>
<input :placeholder="$t('email')" type="text"
class="text-sm sm:text-xl border-2 border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2" />
<p class="pl-6">
{{ $t("email") }}
</p>
<input
:placeholder="$t('email')"
type="text"
class="text-sm sm:text-xl border border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2"
>
</div>
<div class="space-y-[15px]" ref="dropdownRef">
<p class="pl-6">{{ $t('phone') }}</p>
<div ref="dropdownRef"
class="space-y-[15px]"
>
<p class="pl-6">
{{ $t("phone") }}
</p>
<div class="flex items-center border-2 border-block rounded-lg">
<div
class="relative z-50 bg-inherit ring-0 cursor-pointer focus:ring-0 outline-none focus-visible:ring-0">
<div class="px-[25px]" @click="dropCountry = !dropCountry">
class="relative z-50 bg-inherit ring-0 cursor-pointer focus:ring-0 outline-none focus-visible:ring-0"
>
<div class="px-[25px]"
@click="dropCountry = !dropCountry"
>
<div
class="flex items-center gap-2 text-sm sm:text-xl uppercase text-text-light dark:text-text-dark">
<span :class="[dropCountry && 'rotate-180', 'transition-all']"> <i
class="uil uil-angle-down text-2xl font-light cursor-pointer"></i></span>
class="flex items-center gap-2 text-sm sm:text-xl uppercase text-text-light dark:text-text-dark"
>
<span
:class="[dropCountry && 'rotate-180', 'transition-all']"
>
<i
class="uil uil-angle-down text-2xl font-light cursor-pointer"
/></span>
<p class="text-sm sm:text-xl">
{{ menuStore.selectedPhoneCountry.iso_code }}
</p>
</div>
</div>
<div v-if="dropCountry"
class="mt-2 absolute bg-bg-light dark:bg-bg-dark rounded-[5px] ring-0 cursor-pointer w-full border border-button py-[10px] px-[5px] overflow-hidden">
<div
v-if="dropCountry"
class="mt-2 absolute bg-bg-light dark:bg-bg-dark rounded-[5px] ring-0 cursor-pointer w-[130px] sm:w-full border border-button py-[10px] px-[5px] overflow-hidden"
>
<div class="overflow-y-auto h-[200px] w-full">
<p v-for="item in menuStore.countryList" @click="() => {
menuStore.selectedPhoneCountry = item
dropCountry = false
}" class="w-full truncate whitespace-nowrap overflow-hidden hover:bg-block dark:hover:bg-button pl-2 py-2 text-base text-text-light dark:text-text-dark rounded-[5px]"
:title="item.name">
<p
v-for="item in menuStore.countries"
:key="item.iso_code"
class="w-full truncate whitespace-nowrap overflow-hidden hover:bg-block dark:hover:bg-button pl-2 py-2 text-base text-text-light dark:text-text-dark rounded-[5px]"
:title="item.name"
@click="
() => {
menuStore.selectedPhoneCountry = item;
dropCountry = false;
}
"
>
{{ item.name }}
</p>
</div>
</div>
</div>
<p class="text-sm sm:text-xl font-normal">{{ menuStore.selectedPhoneCountry.call_prefix
}}</p>
<input :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" />
<p class="text-sm sm:text-xl font-normal">
{{ menuStore.selectedPhoneCountry.call_prefix }}
</p>
<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"
>
</div>
</div>
<div class="space-y-[15px]">
<p class="pl-6">{{ $t('account_type') }}</p>
<USelect v-model="selectedType" :items="component.section_lang_data.account_types"
value-key="name" :searchable="false" :ui="{
<p class="pl-6">
{{ $t("account_type") }}
</p>
<USelect
v-model="selectedType"
:items="
component.front_section_lang
&& component.front_section_lang[0].data.account_types
"
value-key="name"
:searchable="false"
:ui="{
base: 'bg-inherit ring-0 cursor-pointer w-auto focus:ring-0 outline-none focus-visible:ring-0 h-[50px] sm:h-[67px] w-full p-0',
trailing: 'hidden w-full',
viewport: 'ring-0 min-w-full',
content: 'bg-bg-light dark:bg-bg-dark ring-0 border border-button',
content:
'bg-bg-light dark:bg-bg-dark ring-0 border border-button',
leading:
'left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 p-0 w-full',
group: 'px-[5px] py-[10px]',
item: 'hover:bg-block dark:hover:bg-button rounded-[5px] data-highlighted:not-data-disabled:before:bg-button/50 min-w-full',
}">
}"
>
<template #leading="{ modelValue }">
<div
class="flex items-center justify-between gap-2 uppercase text-sm sm:text-xl border-2 border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 w-full h-[50px] sm:h-[67px]">
class="flex items-center justify-between gap-2 uppercase text-sm sm:text-xl border-2 border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 w-full h-[50px] sm:h-[67px]"
>
<p class="truncate whitespace-nowrap">
{{component.section_lang_data.account_types.find((item) => item.name ===
modelValue)?.name}}</p>
<span> <i
class="uil uil-angle-down text-2xl font-light cursor-pointer"></i></span>
{{
component.front_section_lang
&& component.front_section_lang[0].data.account_types.find(
(item) => item.name === modelValue,
)?.name
}}
</p>
<span>
<i
class="uil uil-angle-down text-2xl font-light cursor-pointer"
/></span>
</div>
</template>
<template #item="{ item }">
<div class="flex items-center gap-2 cursor-pointer min-w-full">
<div
class="flex items-center gap-2 cursor-pointer min-w-full"
>
<p
class="truncate whitespace-nowrap text-sm sm:text-xl font-medium uppercase text-text-light dark:text-text-dark opacity-100">
class="truncate whitespace-nowrap text-sm sm:text-xl font-medium uppercase text-text-light dark:text-text-dark opacity-100"
>
{{ item.name }}
</p>
</div>
@ -102,49 +172,77 @@
</USelect>
</div>
<div class="space-y-[15px]">
<p class="pl-6">{{ $t('partner_code') }}</p>
<input :placeholder="$t('placeholder_password')" type="text"
class="border-2 border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2" />
<p class="pl-6">
{{ $t("partner_code") }}
</p>
<input
:placeholder="$t('placeholder_password')"
type="text"
class="border-2 border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2"
>
</div>
</div>
</div>
</div>
<div class="py-[25px] sm:py-12 border-b border-gray flex justify-center w-full">
<UiButtonArrow type="fill" :arrow="true">{{ $t('login') }}</UiButtonArrow>
<div
class="py-[25px] sm:py-12 border-b border-gray flex justify-center w-full"
>
<UiButtonArrow type="fill"
:arrow="true"
>
{{
$t("sign_up")
}}
</UiButtonArrow>
</div>
<div class="mt-[25px] sm:mt-[30px] w-full flex justify-center gap-3">
<p class="cursor-pointer hover:underline transition-all">{{
$t('is_account')
}}</p>
<p class="text-button cursor-pointer hover:text-button-hover">{{
$t('login')
}}</p>
<p class="cursor-pointer hover:underline transition-all">
{{ $t("is_account") }}
</p>
<nuxt-link :to="{ name: `id-slug___${$i18n.locale}`, params: { id: menuStore.getLoginMenu()?.id, slug: menuStore.getLoginMenu()?.front_menu_lang.at(0)?.link_rewrite } }">
<p class="text-button cursor-pointer hover:text-button-hover">
{{ $t("login") }}
</p>
</nuxt-link>
</div>
</div>
</UiContainer>
</template>
<script lang="ts" setup>
import { onClickOutside } from "@vueuse/core";
const props = defineProps<{ component: Component }>();
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: {
account_types: [{
id: number,
import { onClickOutside } from '@vueuse/core'
const props = defineProps<{
component: {
id: number
name: string
}]
};
};
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: {
data: {
account_types: {
id: number
name: string
}[]
}
id_front_section: number
id_lang: number
}[]
}
}>()
const menuStore = useMenuStore()
const dropdownRef = ref(null);
const dropdownRef = ref(null)
const dropCountry = ref()
const selectedType = ref(props.component.section_lang_data.account_types[0].name)
const selectedType = ref()
if (props.component.front_section_lang)
selectedType.value
= props.component.front_section_lang[0].data.account_types[0].name
onClickOutside(dropdownRef, () => {
dropCountry.value = false
});
})
</script>

View File

@ -1,41 +1,62 @@
<template>
<UiContainer class="flex py-10 sm:py-14 gap-10">
<div class="hidden xl:block rounded-2xl min-w-[50%] h-[830px]" :style="{
<div
class="hidden xl:block rounded-2xl min-w-[50%] h-[830px]"
:style="{
backgroundImage: `url('/api/files/${component.image_collection}/${component.section_id}/${component.section_img[0]}?thumb=1200x0')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}" />
}"
/>
<div class="w-full sm:w-[80%] mx-auto my-auto xl:w-full xl:px-12">
<div class="space-y-[55px]">
<div class="flex flex-col-reverse sm:flex-row justify-between items-start gap-7 sm:items-center">
<h2 class="h2-bold-bounded">{{ $t('reset_password') }}</h2>
<div
class="flex flex-col-reverse sm:flex-row justify-between items-start gap-7 sm:items-center"
>
<h2 class="h2-bold-bounded">
{{ $t("reset_password") }}
</h2>
<button
class="whitespace-nowrap h-[40px] sm:h-[43px] px-[10px] sm:px-[17px] border border-gray dark:border-button-disabled text-gray dark:text-button-disabled hover:bg-gray transition-all hover:text-white rounded-[8px] cursor-pointer">{{
$t('back_to_home') }}</button>
class="whitespace-nowrap h-[40px] sm:h-[43px] px-[10px] sm:px-[17px] border border-gray dark:border-button-disabled text-gray dark:text-button-disabled hover:bg-gray transition-all hover:text-white rounded-[8px] cursor-pointer"
>
{{ $t("back_to_home") }}
</button>
</div>
<div class="space-y-[30px]">
<p>{{ component.section_lang_data.reset_password_description }}</p>
<div class="space-y-[15px]">
<p class="pl-6">{{ $t('email') }}</p>
<input :placeholder="$t('email')" type="text"
class="border-2 border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2" />
<p class="pl-6">
{{ $t("email") }}
</p>
<input
:placeholder="$t('email')"
type="text"
class="border-2 border-block placeholder:text-gray dark:placeholder:text-button-disabled text-bg-dark dark:text-bg-light rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2"
>
</div>
</div>
</div>
<div class="pt-[25px] sm:pt-12 flex justify-center w-full">
<UiButtonArrow type="fill" :arrow="true">{{ $t('confirm') }}</UiButtonArrow>
<UiButtonArrow type="fill"
:arrow="true"
>
{{
$t("confirm")
}}
</UiButtonArrow>
</div>
</div>
</UiContainer>
</template>
<script lang="ts" setup>
defineProps<{ component: Component }>();
defineProps<{ component: Component }>()
type Component = {
image_collection: string;
section_id: string;
section_img: string;
image_collection: string
section_id: string
section_img: string
section_lang_data: {
reset_password_description: string
};
};</script>
}
}
</script>

View File

@ -1,11 +1,14 @@
<template>
<SectionShopPageCurrencyRatesBar class="mb-[25px] sm:mb-[55px] xl:mb-[75px]" />
<!-- <SectionShopPageCurrencyRatesBar
class="mb-[25px] sm:mb-[55px] xl:mb-[75px]"
/> -->
<UiContainer>
<div class="flex flex-col gap-[25px] sm:gap-10 xl:flex-row">
<!-- button to open categories -->
<div class="xl:hidden flex items-center w-full">
<button @click="openCategories = !openCategories"
class="h-[40px] w-full cursor-pointer rounded-[10px] px-[22px] transition-all sm:h-[50px] md:h-[65px] md:rounded-[15px] md:px-[42px] bg-button text-text-dark group-hover:bg-button-hover">
<button
class="h-[40px] w-full cursor-pointer rounded-[10px] px-[22px] transition-all sm:h-[50px] md:h-[65px] md:rounded-[15px] md:px-[42px] bg-button text-text-dark group-hover:bg-button-hover"
@click="openCategories = !openCategories">
Otevřené kategorie a filtry
</button>
</div>
@ -17,42 +20,41 @@
</h1>
<div class="flex flex-col gap-[25px]">
<div>
<div v-if="categoriesList.length < 1" class="animate-pulse">
<div
class="flex items-center justify-between mt-4 text-white rounded-lg cursor-pointer xl:pr-24">
<div class="w-32 h-4 bg-gray-200 rounded"></div>
<div class="w-4 h-4 bg-gray-200 rounded-full"></div>
<div v-if="categoriesList && categoriesList.length < 1" class="animate-pulse">
<div class="flex items-center justify-between mt-4 text-white rounded-lg cursor-pointer xl:pr-24">
<div class="w-32 h-4 bg-gray-200 rounded" />
<div class="w-4 h-4 bg-gray-200 rounded-full" />
</div>
</div>
<CategoryTree :data="categoriesList" @change-category="changeCategory($event)"
:active="categoryId" />
<CategoryTree :data="categoriesList" :active="categoryId" @change-category="changeCategory($event)" />
</div>
<div>
<p class="mb-[25px] text-lg font-extrabold text-black dark:text-white">
{{ $t("filtered_by") }}
</p>
<div v-for="(item, itemIndex) in filters" :key="itemIndex"
:class="['mb-[30px]', visibleFeatures[item.feature] && 'border-b border-block pb-2']">
<div v-for="(item, itemIndex) in filters" :key="itemIndex" :class="[
'mb-[30px]',
visibleFeatures[item.feature] && 'border-b border-block pb-2',
]">
<span class="flex justify-between items-center font-bold cursor-pointer mb-[25px] text-base"
@click="toggleFeature(item.feature)">
{{ item.feature }}
<span :class="[visibleFeatures[item.feature] && 'rotate-180', 'transition-all']"><i
class="iconify i-lucide:chevron-down text-button shrink-0 size-6 ms-auto"></i></span>
<span :class="[
visibleFeatures[item.feature] && 'rotate-180',
'transition-all',
]"><i class="iconify i-lucide:chevron-down text-button shrink-0 size-6 ms-auto" /></span>
</span>
<ul v-show="visibleFeatures[item.feature]" class="flex flex-col gap-5">
<li v-for="filter in item.feature_values" :key="filter.value_id"
class="flex items-center gap-[10px] cursor-pointer">
<input :id="`${filter.value_id}`" :value="`${filter.parent}.${filter.value_id}`"
v-model="selectedFilters" type="checkbox"
class="border-button !bg-inherit" />
<input :id="`${filter.value_id}`" v-model="selectedFilters"
:value="`${filter.parent}.${filter.value_id}`" type="checkbox" class="border-button !bg-inherit">
<label :for="`${filter.value_id}`"
class="cursor-pointer flex items-center justify-between w-full text-base">
<span>{{ filter.value }}</span>
<span>12</span>
</label>
</li>
</ul>
</div>
@ -68,16 +70,13 @@
</h1>
<div class="flex flex-col gap-12">
<div>
<div v-if="categoriesList.length < 1" class="animate-pulse">
<div
class="flex items-center justify-between mt-4 text-white rounded-lg cursor-pointer xl:pr-24">
<div class="w-32 h-4 bg-gray-200 rounded"></div>
<div class="w-4 h-4 bg-gray-200 rounded-full"></div>
<div v-if="categoriesList && categoriesList.length < 1" class="animate-pulse">
<div class="flex items-center justify-between mt-4 text-white rounded-lg cursor-pointer xl:pr-24">
<div class="w-32 h-4 bg-gray-200 rounded" />
<div class="w-4 h-4 bg-gray-200 rounded-full" />
</div>
</div>
<CategoryTree :data="categoriesList" @change-category="changeCategory($event)"
:active="categoryId" />
<CategoryTree :data="categoriesList" :active="categoryId" @change-category="changeCategory($event)" />
</div>
<div>
<p class="mb-10 text-2xl font-extrabold text-black dark:text-white">
@ -92,15 +91,17 @@
</div>
</div> -->
<div v-for="(item, itemIndex) in filters" :key="itemIndex"
:class="['mb-[30px]', visibleFeatures[item.feature] && 'border-b border-block pb-2']">
<div v-for="(item, itemIndex) in filters" :key="itemIndex" :class="[
'mb-[30px]',
visibleFeatures[item.feature] && 'border-b border-block pb-2',
]">
<span class="flex justify-between items-center font-bold cursor-pointer mb-[25px]"
@click="toggleFeature(item.feature)">
{{ item.feature }}
<span :class="[visibleFeatures[item.feature] && 'rotate-180', 'transition-all']"><i
class="iconify i-lucide:chevron-down text-button shrink-0 size-6 ms-auto"></i></span>
<span :class="[
visibleFeatures[item.feature] && 'rotate-180',
'transition-all',
]"><i class="iconify i-lucide:chevron-down text-button shrink-0 size-6 ms-auto" /></span>
</span>
<ul v-show="visibleFeatures[item.feature]" class="flex flex-col gap-5">
<li v-for="filter in item.feature_values" :key="filter.value_id"
@ -109,15 +110,12 @@
v-model="selectedFilters" type="checkbox" class="border-button !bg-inherit" />
<label :for="`${filter.value_id}`" class="cursor-pointer">{{ filter.value }}</label> -->
<input :id="`${filter.value_id}`" :value="`${filter.parent}.${filter.value_id}`"
v-model="selectedFilters" type="checkbox" class="border-button !bg-inherit" />
<label :for="`${filter.value_id}`"
class="cursor-pointer flex items-center justify-between w-full">
<input :id="`${filter.value_id}`" v-model="selectedFilters"
:value="`${filter.parent}.${filter.value_id}`" type="checkbox" class="border-button !bg-inherit">
<label :for="`${filter.value_id}`" class="cursor-pointer flex items-center justify-between w-full">
<span>{{ filter.value }}</span>
<span>12</span>
</label>
</li>
</ul>
</div>
@ -126,28 +124,26 @@
</div>
<div class="w-full space-y-10">
<!-- pop-up -->
<div v-if="isInfo"
class="w-full xl:w-[70%] mx-auto border-y border-block py-[15px] sm:p-[30px] flex gap-[55px] relative">
<UButton @click="closeElement()" size="xl" icon="i-lucide-x" variant="ghost"
class="p-0 absolute right-0 top-2 sm:right-2 sm:top-2 cursor-pointer text-button font-light hover:bg-inherit hover:text-button-hover" />
<UButton variant="ghost"
class="p-0 absolute right-0 top-2 sm:right-2 sm:top-2 cursor-pointer text-button font-light hover:bg-inherit hover:text-button-hover"
size="xl" icon="i-lucide-x" @click="closeElement()" />
<div class="flex flex-col sm:flex-row gap-[25px]">
<div class="flex flex-col justify-between gap-[25px]">
<h4 class="font-inter text-lg sm:text-[24px] leading-[150%] md:leading-[120%] font-bold">
{{ component.section_lang_data.title }}
{{ component.front_section_lang[0].data.title }}
</h4>
<p>{{ component.section_lang_data.description }}</p>
<p>{{ component.front_section_lang[0].data.description }}</p>
</div>
<img class="max-w-[150px] mx-auto"
:src="`/api/files/${component.image_collection}/${component.section_id}/${component.section_img[0]}?thumb=640x0')`"
alt="" />
<img class="max-w-[150px] mx-auto" :src="`/api/public/file/${component.img[0]}_m.webp')`">
</div>
</div>
<div v-if="products.length < 1" class="grid gap-12 pt-32 pb-16 md:grid-cols-2 2xl:grid-cols-3">
<TheProductSkeleton v-for="index in 6"></TheProductSkeleton>
<!-- <TheProductSkeleton v-for="index in 6" :key="index" /> -->
</div>
<!-- products -->
@ -164,261 +160,311 @@
</div>
</UiContainer>
</template>
<script setup lang="ts">
import { ref } from "vue";
import Product from "./Product.vue";
import type { Feature, ProductType } from "~/types";
import CategoryTree from "./CategoryTree.vue";
import { ref } from 'vue'
import Product from './Product.vue'
import CategoryTree from './CategoryTree.vue'
import type {
Feature,
GenericResponse,
GenericResponseChildren,
GenericResponseItems,
ProductType,
} from '~/types'
defineProps<{ component: Component }>();
type Component = {
image_collection: string;
section_id: string;
section_img: string;
section_lang_data: {
title: string;
const { $session } = useNuxtApp()
watch(
() => $session.cookieData,
async () => await getProducts(),
{ deep: true },
)
defineProps<{
component: {
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: {
data: {
title: string
description: string
};
};
}
id_front_section: number
id_lang: number
}[]
}
}>()
const openCategories = ref(false);
const isInfo = ref<boolean>(true);
const selectedFilters = ref<any>([]);
const categoryId = ref<number>(1);
const itemsCount = ref(0);
const openCategories = ref(false)
const isInfo = ref<boolean>(true)
const selectedFilters = ref<string[]>([])
const categoryId = ref<number>(1)
const loading = ref(false);
const reachedEnd = ref(false);
const loading = ref(false)
const reachedEnd = ref(false)
const loadingElement = ref<HTMLElement | null>(null);
const loadingElement = ref<HTMLElement | null>(null)
const page = ref(1);
const elems = ref(12);
const maxElements = ref(0);
const page = ref(1)
const elems = ref(12)
const maxElements = ref(0)
const products = ref([] as ProductType[]);
const products = ref([] as ProductType[])
async function getProducts() {
try {
const res = await fetch(
`http://127.0.0.1:4000/api/public/products/category/${categoryId.value}?p=${page.value}&elems=${elems.value}`,
const { data } = await useMyFetch<GenericResponseItems<ProductType[]>>(
`/api/public/products/category/${categoryId.value}?p=${page.value}&elems=${elems.value}`,
{
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`)
},
},
)
products.value = data.items
maxElements.value = data.items_count + 1
}
);
const data = await res.json();
products.value = data.data.items;
maxElements.value = data.data.items_count + 1;
} catch (error) {
console.error("getProducts error:", error);
catch (error) {
console.error('getProducts error:', error)
}
}
const filters = ref([] as Feature[]);
const filters = ref([] as Feature[])
async function getCategory() {
try {
const res = await fetch(
`http://127.0.0.1:4000/api/public/products/category/1/classification`,
const { data } = await useMyFetch<GenericResponse<object>>(
`/api/public/products/category/1/classification`,
{
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
}
);
onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`)
},
},
)
const data = await res.json();
filters.value = data.data as Feature[];
filters.value.forEach((el) => {
const parentId = el.feature_id;
filters.value = data as Feature[]
filters.value.forEach((el: Feature) => {
const parentId = el.feature_id
el.feature_values.forEach((el) => {
el.parent = parentId;
});
});
} catch (error) {
console.error("getCategory error:", error);
el.parent = parentId
})
})
}
catch (error) {
console.error('getCategory error:', error)
}
}
const categoriesList = ref([]);
const categoriesList = ref()
async function getCategoryTree() {
try {
const res = await fetch(
`http://127.0.0.1:4000/api/public/categories/tree`,
const { data } = await useMyFetch<GenericResponseChildren<ProductType[]>>(
`/api/public/categories/tree`,
{
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`)
},
},
)
categoriesList.value = data.children
}
);
const data = await res.json();
categoriesList.value = data.data.children;
} catch (error) {
console.error("getCategory error:", error);
catch (error) {
console.error('getCategory error:', error)
}
}
getProducts()
getCategory()
getCategoryTree()
const closeElement = () => {
isInfo.value = false;
};
isInfo.value = false
}
onMounted(() => {
window.addEventListener("scroll", scrollEvent);
});
window.addEventListener('scroll', scrollEvent)
})
onUnmounted(() => {
window.removeEventListener("scroll", scrollEvent);
});
window.removeEventListener('scroll', scrollEvent)
})
async function scrollEvent(e: Event) {
let maxScrollY = window.scrollY || document.documentElement.scrollHeight - document.documentElement.clientHeight;
async function scrollEvent() {
const maxScrollY
= window.scrollY
|| document.documentElement.scrollHeight
- document.documentElement.clientHeight
if (window.scrollY >= maxScrollY - 500 && !reachedEnd.value && !loading.value) {
loading.value = true;
await loadMoreProducts();
loading.value = false;
if (
window.scrollY >= maxScrollY - 500
&& !reachedEnd.value
&& !loading.value
) {
loading.value = true
await loadMoreProducts()
loading.value = false
}
}
filters.value.forEach((item) => {
visibleFeatures[item.feature] = false;
});
visibleFeatures[item.feature] = false
})
const visibleFeatures = reactive<any>({});
function toggleFeature(feature: any) {
if (visibleFeatures.hasOwnProperty(feature)) {
visibleFeatures[feature] = !visibleFeatures[feature];
} else {
visibleFeatures[feature] = true;
const visibleFeatures = reactive<Record<string, boolean>>({})
function toggleFeature(feature: string) {
if (feature in visibleFeatures) {
visibleFeatures[feature] = !visibleFeatures[feature]
}
else {
visibleFeatures[feature] = true
}
}
class FilteredQueryString extends URLSearchParams {
override append(name: string, value: string): void {
if (value == null) {
return;
return
}
super.append(name, value);
super.append(name, value)
}
}
async function loadMoreProducts() {
let qParams = new FilteredQueryString();
const qParams = new FilteredQueryString()
page.value = page.value + 1;
page.value = page.value + 1
qParams.append("p", `${page.value}`);
qParams.append("elems", `${elems.value}`);
qParams.append("features", selectedFilters.value.length > 0 ? selectedFilters.value : null);
qParams.append('p', `${page.value}`)
qParams.append('elems', `${elems.value}`)
qParams.append(
'features',
selectedFilters.value.length > 0 ? selectedFilters.value : null,
)
try {
const res = await fetch(
`http://127.0.0.1:4000/api/public/products/category/${categoryId.value}?${qParams.toString()}`,
const { data } = await useMyFetch<GenericResponseItems<ProductType[]>>(
`/api/public/products/category/${categoryId.value}?${qParams.toString()}`,
{
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`)
},
},
)
maxElements.value = data.items_count
if (data.items) {
products.value.push(...(data.items as ProductType[]))
}
);
const data = await res.json();
maxElements.value = data.data.items_count;
if (data.data.items) {
products.value.push(...(data.data.items as ProductType[]));
} else {
reachedEnd.value = true;
else {
reachedEnd.value = true
}
if (products.value.length >= maxElements.value) {
reachedEnd.value = true;
reachedEnd.value = true
}
} catch (error) {
console.error("getCategory error:", error);
}
catch (error) {
console.error('getCategory error:', error)
}
}
const changeCategory = (item: any) => {
categoryId.value = item.id;
};
interface CategoryItem {
id: number
name: string
}
watch(selectedFilters, async (newQuestion) => {
const changeCategory = (item: CategoryItem) => {
categoryId.value = item.id
}
watch(selectedFilters, async (newQuestion: string) => {
if (newQuestion) {
page.value = 1;
reachedEnd.value = false;
loadingElement.value?.scrollIntoView();
page.value = 1
reachedEnd.value = false
loadingElement.value?.scrollIntoView()
let qParams = new FilteredQueryString();
const qParams = new FilteredQueryString()
qParams.append("p", `${page.value}`);
qParams.append("elems", `${elems.value}`);
qParams.append("features", selectedFilters.value.length > 0 ? selectedFilters.value : null);
qParams.append('p', `${page.value}`)
qParams.append('elems', `${elems.value}`)
qParams.append(
'features',
selectedFilters.value.length > 0 ? selectedFilters.value : null,
)
try {
const res = await fetch(
`http://127.0.0.1:4000/api/public/products/category/1?${qParams.toString()}`,
const { data } = await useMyFetch<GenericResponseItems<ProductType[]>>(
`/api/public/products/category/1?${qParams.toString()}`,
{
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`)
},
},
)
products.value = data.items
maxElements.value = data.items_count
}
);
const data = await res.json();
products.value = data.data.items;
maxElements.value = data.data.items_count;
} catch (error) {
console.error("selectedFilters error:", error);
catch (error) {
console.error('selectedFilters error:', error)
}
}
});
})
watch(categoryId, async (newQuestion) => {
if (newQuestion) {
page.value = 1;
reachedEnd.value = false;
loadingElement.value?.scrollIntoView();
watch(categoryId, async (newCategoryId) => {
if (newCategoryId) {
page.value = 1
reachedEnd.value = false
loadingElement.value?.scrollIntoView()
let qParams = new FilteredQueryString();
qParams.append("p", `${page.value}`);
qParams.append("elems", `${elems.value}`);
qParams.append("features", selectedFilters.value.length > 0 ? selectedFilters.value : null);
const qParams = new FilteredQueryString()
qParams.append('p', `${page.value}`)
qParams.append('elems', `${elems.value}`)
qParams.append(
'features',
selectedFilters.value.length > 0 ? selectedFilters.value : null,
)
try {
const res = await fetch(
`http://127.0.0.1:4000/api/public/products/category/${categoryId.value}?${qParams.toString()}`,
const { data } = await useMyFetch<GenericResponseItems<ProductType[]>>(
`api/public/products/category/${newCategoryId}?${qParams.toString()}`,
{
headers: {
"Content-Type": "application/json",
headers: { 'Content-Type': 'application/json' },
onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`)
},
}
);
},
)
const data = await res.json();
products.value = data.data.items;
maxElements.value = data.data.items_count;
} catch (error) {
console.error("getCategory error:", error);
products.value = data.items
maxElements.value = data.items_count
}
catch (error) {
console.error('getCategory error:', error)
}
}
});
})
</script>
<style scoped>

View File

@ -4,12 +4,13 @@
>
<button
:class="[
'h-[40px] cursor-pointer rounded-[10px] px-[22px] transition-all sm:h-[50px] md:h-[65px] md:rounded-[15px] md:px-[42px]',
'h-[40px] cursor-pointer min-w-40 rounded-[10px] px-[22px] transition-all sm:h-[50px] md:h-[65px] md:rounded-[15px] md:px-[42px]',
type === 'fill'
? 'bg-button text-text-dark group-hover:bg-button-hover'
: type === 'border'
? 'border-button text-button group-hover:border-button-hover group-hover:text-button-hover border'
: 'border-button text-button dark:border-block dark:text-block group-hover:border-button-hover group-hover:text-button-hover border',
full && 'w-full',
]"
>
<slot />
@ -48,5 +49,8 @@ defineProps({
arrow: {
type: Boolean,
},
});
full: {
type: Boolean,
},
})
</script>

View File

@ -0,0 +1,81 @@
<template>
<div class="space-y-[15px]">
<p :for="`base-input-${id}`"
class="pl-6"
>
<slot />
</p>
<div class="flex flex-col">
<div class="flex relative">
<input
:id="`base-input-${id}`"
:value="modelValue"
:type="!isPasswordVisible ? type : 'text'"
:placeholder="placeholder"
:disabled="disabled"
class="border border-block placeholder:text-gray dark:placeholder:text-button-disabled rounded-lg px-6 h-[50px] sm:h-[67px] w-full focus:outline-none focus:ring-0 focus:border-2"
@input="
$emit(
'update:modelValue',
($event.target as HTMLInputElement).value,
)
"
@focus="$emit('focus')"
@blur="$emit('blur')"
>
<i
v-if="disabled"
class="uil uil-lock-alt text-[22px] absolute right-6 top-1/2 -translate-y-1/2 text-gray"
/>
<div
v-if="type === 'password'"
class="order-2 ml-1.5 cursor-pointer"
:title="
!isPasswordVisible ? $t('show_password') : $t('hide_password')
"
@click="isPasswordVisible = !isPasswordVisible"
>
<FaceObserver
class="ml-4 text-xl leading-6"
:is-password-visible="isPasswordVisible"
/>
</div>
</div>
<!-- <p class="mt-2 text-xs text-red-600">{{ validationText }}</p> -->
<!-- <p v-if="!validation && validation != null" class="mt-2 text-xs text-red-600">
{{ validationText }}
</p> -->
</div>
</div>
</template>
<script setup lang="ts">
import FaceObserver from './FaceObserver.vue'
defineEmits(['update:modelValue', 'focus', 'blur'])
defineProps<{
modelValue?: string | undefined
modelModifiers?: object
id: number
type?: string
disabled?: boolean
placeholder?: string
validation?: boolean | null
validationText?: string
}>()
const isPasswordVisible = ref(false)
</script>
<style>
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
transition: background-color 5000s ease-in-out 0s;
}
</style>

View File

@ -1,6 +1,7 @@
<template>
<UContainer
class="mx-auto w-full max-w-[380px] px-4 sm:max-w-[768px] sm:px-[17px] md:max-w-[1000px] md:px-6 xl:max-w-[1920px] xl:px-20">
class="mx-auto w-full max-w-[380px] px-4 sm:max-w-[768px] sm:px-[17px] md:max-w-[1000px] md:px-6 xl:max-w-[1920px] xl:px-20"
>
<slot />
</UContainer>
</template>

View File

@ -0,0 +1,37 @@
<template>
<svg
v-if="isPasswordVisible"
xmlns="http://www.w3.org/2000/svg"
height="16px"
viewBox="0 -960 960 960"
width="16px"
fill="#000000"
>
<path
d="m644-428-58-58q9-47-27-88t-93-32l-58-58q17-8 34.5-12t37.5-4q75 0 127.5 52.5T660-500q0 20-4 37.5T644-428Zm128 126-58-56q38-29 67.5-63.5T832-500q-50-101-143.5-160.5T480-720q-29 0-57 4t-55 12l-62-62q41-17 84-25.5t90-8.5q151 0 269 83.5T920-500q-23 59-60.5 109.5T772-302Zm20 246L624-222q-35 11-70.5 16.5T480-200q-151 0-269-83.5T40-500q21-53 53-98.5t73-81.5L56-792l56-56 736 736-56 56ZM222-624q-29 26-53 57t-41 67q50 101 143.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z"
/>
</svg>
<svg
v-else
xmlns="http://www.w3.org/2000/svg"
height="16px"
viewBox="0 -960 960 960"
width="16px"
fill="#000000"
>
<path
d="M480-320q75 0 127.5-52.5T660-500q0-75-52.5-127.5T480-680q-75 0-127.5 52.5T300-500q0 75 52.5 127.5T480-320Zm0-72q-45 0-76.5-31.5T372-500q0-45 31.5-76.5T480-608q45 0 76.5 31.5T588-500q0 45-31.5 76.5T480-392Zm0 192q-146 0-266-81.5T40-500q54-137 174-218.5T480-800q146 0 266 81.5T920-500q-54 137-174 218.5T480-200Zm0-300Zm0 220q113 0 207.5-59.5T832-500q-50-101-144.5-160.5T480-720q-113 0-207.5 59.5T128-500q50 101 144.5 160.5T480-280Z"
/>
</svg>
</template>
<script>
export default {
props: {
isPasswordVisible: {
type: Boolean,
required: true,
},
},
}
</script>

View File

@ -0,0 +1,51 @@
<template>
<div class="hidden md:block">
<svg
width="100%"
height="100%"
viewBox="0 0 870 350"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<clipPath id="customClip">
<path
d="M20 0.5H847.666C858.366 0.5 867.067 9.12193 867.165 19.8213L869.315 254.821C869.415 265.66 860.656 274.5 849.816 274.5H653C641.678 274.5 632.5 283.678 632.5 295V330C632.5 340.77 623.77 349.5 613 349.5H20C9.23045 349.5 0.5 340.77 0.5 330V20C0.5 9.23045 9.23045 0.5 20 0.5Z"
/>
</clipPath>
</defs>
<image
:href="src"
clip-path="url(#customClip)"
preserveAspectRatio="xMidYMid slice"
width="100%"
height="100%"
/>
<foreignObject
x="640"
y="285"
width="calc(100% - 640px - 1px)"
height="calc(100% - 285px)"
>
<slot name="button" />
</foreignObject>
</svg>
</div>
<div class="block md:hidden">
<img
:src="src"
width="100%"
height="100%"
class="object-contain rounded-2xl my-4"
>
<div class="flex justify-center">
<slot name="button" />
</div>
</div>
</template>
<script lang="ts" setup>
defineProps<{ src: string }>()
</script>

View File

@ -1,9 +1,9 @@
import { ofetch } from "ofetch";
import { ofetch } from 'ofetch'
export interface RequestOptions<T> extends RequestInit {
onError?: (error: Error, statusCode: number) => void;
onSuccess?: (data: T, statusCode: number) => void;
onStart?: () => void;
onErrorOccured?: (error: Error, statusCode: number) => void
onSuccess?: (data: T, statusCode: number) => void
onStart?: () => void
}
/**
@ -23,50 +23,52 @@ export interface RequestOptions<T> extends RequestInit {
*/
export const useMyFetch = async <T>(
url: string,
options?: RequestOptions<T>
): Promise<T | undefined> => {
if (options?.onStart) options.onStart();
let response = null;
options?: RequestOptions<T>,
): Promise<T> => {
if (options?.onStart) options.onStart()
let response = null
try {
const event = useRequestEvent();
const event = useRequestEvent()
if (options == null) options = {};
if (options == null) options = {}
options.credentials = "include";
options.credentials = 'include'
if (import.meta.server) {
const api_uri =
event?.node.req.headers["api-uri"] || "http://localhost:4000";
url = api_uri + url;
options.headers = event?.headers;
const api_uri
= event?.node.req.headers['api-uri'] || 'http://localhost:4000'
url = api_uri + url
options.headers = event?.headers
}
response = await ofetch.raw(url, options);
response = await ofetch.raw(url, options)
if (import.meta.server && !event?.handled) {
for (const cookie of response.headers.getSetCookie()) {
event?.headers.set("Cookie", cookie);
event?.node.res.setHeader("set-cookie", cookie);
event?.headers.set('Cookie', cookie)
event?.node.res.setHeader('set-cookie', cookie)
}
}
// handle errors if any
if (!response.ok && typeof options.onError == "function") {
options.onError(new Error(response.statusText), response.status);
if (!response.ok && typeof options.onErrorOccured == 'function') {
options.onErrorOccured(new Error(response.statusText), response.status)
}
// handle success to be able clearly marked that request has finished
if (response.ok && typeof options.onSuccess == "function") {
options.onSuccess(response._data.data, response.status);
if (response.ok && typeof options.onSuccess == 'function') {
options.onSuccess(response._data, response.status)
}
return response._data as T;
} catch (e) {
return response._data as T
}
catch (e) {
// handle errors if any
if (typeof options?.onError == "function") {
options.onError(e as Error, response?.status || 500);
} else {
console.error(e);
if (typeof options?.onErrorOccured == 'function') {
options.onErrorOccured(e as Error, response?.status || 500)
}
else {
console.error(e)
}
return {} as T
}
return undefined;
}
};

View File

@ -1,15 +0,0 @@
// import { ref } from 'vue';
import pocketbase from "pocketbase";
export const usePB = () => {
const nuxtApp = useNuxtApp();
const isServer = !!nuxtApp.ssrContext;
if (isServer) {
const pb = new pocketbase(process.env.POCKETBASE_URL || "http://127.0.0.1:8090");
return pb;
}
const pb = new pocketbase(window.location.origin);
return pb;
};

17
error.vue Normal file
View File

@ -0,0 +1,17 @@
<template>
<div class="p-10 text-center">
<h1 class="text-3xl font-bold">
Error {{ error?.statusCode }}
</h1>
<p class="mt-4 text-gray-600">
{{ error?.statusMessage }}
</p>
<NuxtLink to="/"
class="mt-6 text-blue-500 underline"
>Go back home</NuxtLink>
</div>
</template>
<script setup lang="ts">
defineProps({ error: Object })
</script>

View File

@ -1,5 +1,21 @@
// @ts-check
import withNuxt from "./.nuxt/eslint.config.mjs";
import { createConfigForNuxt } from '@nuxt/eslint-config'
export default withNuxt();
// Your custom configs here
export default createConfigForNuxt({
features: {
stylistic: true,
typescript: true,
},
}).override('nuxt/vue/rules', {
rules: {
'vue/first-attribute-linebreak': 'off',
'vue/no-v-html': 'off',
'vue/html-closing-bracket-newline': 'off',
'vue/html-self-closing': 'off',
'vue/max-attributes-per-line': 'off',
},
}).override('nuxt/stylistic', {
rules: {
'no-useless-escape': 'off',
},
})

View File

@ -1,3 +0,0 @@
{
"welcome": "Welcome to Nuxt 3"
}

View File

@ -1,9 +1,11 @@
<template>
<div
class="bg-bg-light dark:bg-bg-dark text-text-light dark:text-text-dark font-inter flex min-h-screen flex-col overflow-hidden">
class="bg-bg-light dark:bg-bg-dark text-text-light dark:text-text-dark font-inter flex min-h-[calc(100vh-50px)] flex-col overflow-hidden"
>
<HeaderBlock />
<div
class="flex-1 py-[25px] sm:py-[55px] md:py-[75px] space-y-[55px] sm:space-y-[75px] md:space-y-[100px] text-inter">
class="flex py-[25px] sm:py-[55px] md:py-[75px] space-y-[55px] sm:space-y-[75px] md:space-y-[100px] text-inter min-h-[inherit] flex-col justify-between"
>
<slot />
</div>
<!-- <FooterBlock /> -->
@ -11,7 +13,8 @@
</template>
<script setup lang="ts">
useHead({
link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.png" }],
});
// import FooterBlock from "~/components/section/FooterBlock.vue";
// useHead({
// link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.png" }],
// });
</script>

View File

@ -1,59 +1,61 @@
import tailwindcss from "@tailwindcss/vite";
import tailwindcss from '@tailwindcss/vite'
export default defineNuxtConfig({
compatibilityDate: "2024-11-01",
devtools: { enabled: false },
// app: {
// pageTransition: { name: "page", mode: "out-in" },
// },
nitro: {
routeRules: {
"/api/**": {
proxy: {
to: `${process.env.POCKETBASE_URL || "http://127.0.0.1:8090"}/api/**`,
},
},
},
},
modules: ["@pinia/nuxt", "@nuxt/eslint", "@nuxt/ui", "@nuxtjs/i18n", "@pinia/nuxt"],
i18n: {
locales: [
{ code: "pl", name: "Polski", icon: "circle-flags:pl" },
{ code: "en", name: "English", icon: "circle-flags:gb" },
{ code: "cs", name: "Čeština", icon: "circle-flags:cz" }
modules: [
'@pinia/nuxt',
'@nuxt/eslint',
'@nuxt/ui',
'@nuxtjs/i18n',
'@pinia/nuxt',
],
lazy: true,
defaultLocale: "en",
strategy: "prefix",
bundle: {
optimizeTranslationDirective: false,
},
},
css: ["@/assets/fonts.css", "@/assets/main.css"],
devtools: { enabled: false },
css: [
'@/assets/main.css',
'vue3-toastify/dist/index.css',
'@/assets/toastify-custom.css',
],
ui: {},
compatibilityDate: '2024-11-01',
vite: {
plugins: [tailwindcss()],
build: {
sourcemap: false,
},
server: {
allowedHosts: ["arina.ma-al.pl", "marek.ma-al.pl"],
allowedHosts: ['arina.ma-al.pl', 'marek.ma-al.pl'],
watch: {
ignored: ["**/backend/pb_data/**"],
ignored: ['**/backend/pb_data/**'],
},
hmr: {
host: '127.0.0.1',
clientPort: 3000, // useful if proxying
},
},
},
typescript: {
tsConfig: {
compilerOptions: {
typeRoots: ["./types", "./node_modules/@types"],
typeRoots: ['./types', './node_modules/@types'],
},
include: ["./types"],
include: ['./types'],
},
},
i18n: {
locales: [
{ code: 'pl', name: 'Polski', icon: 'circle-flags:pl' },
{ code: 'en', name: 'English', icon: 'circle-flags:gb' },
{ code: 'cs', name: 'Čeština', icon: 'circle-flags:cz' },
],
lazy: true,
defaultLocale: 'en',
strategy: 'prefix',
bundle: {
optimizeTranslationDirective: false,
},
},
ui: {},
icon: {
localApiEndpoint: "/___nuxt_icon",
localApiEndpoint: '/___nuxt_icon',
},
});
})

View File

@ -7,21 +7,23 @@
"dev": "nuxt dev --host 0.0.0.0",
"generate": "nuxt generate",
"preview": "nuxt preview --host 0.0.0.0",
"postinstall": "nuxt prepare"
"postinstall": "nuxt prepare",
"lint": "eslint"
},
"dependencies": {
"@iconscout/unicons": "^4.2.0",
"@nuxt/eslint": "^1.4.1",
"@nuxt/ui": "^3.1.3",
"@nuxtjs/i18n": "^9.5.4",
"@pinia/nuxt": "^0.11.0",
"@tailwindcss/vite": "^4.1.8",
"@vueuse/core": "^13.3.0",
"nuxt": "^3.17.4",
"pocketbase": "^0.26.0",
"tailwindcss": "^4.1.7",
"vue": "^3.5.14",
"vue-router": "^4.5.1"
"@nuxt/eslint": "^1.5.2",
"@nuxt/ui": "^3.2.0",
"@nuxtjs/i18n": "^9.5.6",
"@pinia/nuxt": "^0.11.1",
"@tailwindcss/vite": "^4.1.11",
"@vueuse/core": "^13.5.0",
"gsap": "^3.13.0",
"nuxt": "^3.17.6",
"tailwindcss": "^4.1.11",
"vue": "^3.5.17",
"vue-router": "^4.5.1",
"vue3-toastify": "^0.2.8"
},
"devDependencies": {
"@nuxtjs/tailwindcss": "^6.14.0"

View File

@ -10,17 +10,68 @@
</template>
<script setup>
import { useStore } from "@/stores/store";
const route = useRoute();
const store = useStore();
const menuStore = useMenuStore();
await store.getSections(route.params.id);
import { gsap } from 'gsap'
import ScrollTrigger from 'gsap/ScrollTrigger'
gsap.registerPlugin(ScrollTrigger)
watch(useColorMode(), (color) => {
console.log(color)
})
onMounted(() => {
menuStore.openMenu = false;
});
const anim = gsap.fromTo(
'h1',
{
opacity: 0,
zoom: 0.95,
},
{
opacity: 1,
duration: 1,
zoom: 1,
ease: 'power2.out',
},
)
useHead(menuStore.headMeta);
ScrollTrigger.create({
trigger: 'h1',
start: 'top 80%',
onEnter: () => anim.restart(), // play when scrolling down
onEnterBack: () => anim.restart(), // play again when scrolling up
})
const componentsList = await store.getComponents(route.params.id);
const animh2 = gsap.fromTo(
'h2',
{
// opacity: 0,
// color: 'var(--color-accent-green-light)',
},
{
// opacity: 1,
// duration: 1,
ease: 'power2.out',
},
)
ScrollTrigger.create({
trigger: 'h2',
start: 'top 80%',
onEnter: () => animh2.restart(), // play when scrolling down
onEnterBack: () => animh2.restart(), // play again when scrolling up
})
})
const route = useRoute()
const store = useStore()
const menuStore = useMenuStore()
await store.getSections(route.params.id)
onMounted(() => {
menuStore.openMenu = false
})
useHead(menuStore.headMeta)
const componentsList = await store.getComponents(route.params.id)
</script>

View File

@ -6,26 +6,24 @@
:key="component.name"
:component="component.component"
/>
</KeepAlive>
</template>
<script setup>
import { useStore } from "@/stores/store";
const menuStore = useMenuStore();
const menuStore = useMenuStore()
const route = useRoute();
const route = useRoute()
route.params.id = menuStore.defaultMenu.id_page;
route.params.slug = menuStore.defaultMenu.link_rewrite;
route.params.id = menuStore.defaultMenu.id
route.params.slug = menuStore.defaultMenu.link_rewrite
const store = useStore();
await store.getSections(route.params.id);
const store = useStore()
await store.getSections(route.params.id)
onMounted(() => {
menuStore.openMenu = false;
});
menuStore.openMenu = false
})
useHead(menuStore.headMeta);
const componentsList = await store.getComponents(route.params.id);
useHead(menuStore.headMeta)
const componentsList = await store.getComponents(route.params.id)
</script>

138
plugins/01_i18n.ts Normal file
View File

@ -0,0 +1,138 @@
import type { VueI18n } from 'vue-i18n'
import type { RouteLocation, Router } from 'vue-router'
import type { CookieData, GenericResponse } from '~/types'
// Extend the NuxtApp type
declare module '#app' {
interface NuxtApp {
$session: Session
}
}
declare module 'vue' {
interface ComponentCustomProperties {
$session: Session
}
}
export class Session {
cookieData = ref({} as CookieData)
urlParams = new URLSearchParams()
currentLanguageIso = ref('' as string)
currentCountryIso = ref('' as string)
currentCurrencyIso = ref('' as string)
route = {} as RouteLocation
router = {} as Router
i18n = {} as VueI18n
sessionOngoing: boolean = false
constructor(i18n: VueI18n, router: Router) {
this.route = router.currentRoute.value
this.router = router
this.i18n = i18n
this.setLanguage(
this.route.query?.lang_iso
? (this.route.query?.lang_iso as string)
: unref(i18n.locale),
)
this.setCountry(
this.route.query?.country_iso
? (this.route.query?.country_iso as string)
: '',
)
this.setCurrency(
this.route.query?.currency_iso
? (this.route.query?.currency_iso as string)
: '',
)
}
async loadSession() {
if (this.sessionOngoing) return
this.sessionOngoing = true
this.setQueryParams()
const { data } = await useMyFetch<GenericResponse<CookieData>>(
`/api/public/cookie?${this.urlParams.toString()}`,
{
headers: {
'Content-Type': 'application/json',
},
onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`)
},
},
)
this.cookieData.value = data
this.currentCountryIso.value = this.cookieData.value.country.iso_code
this.currentLanguageIso.value = this.cookieData.value.language.iso_code
this.currentCurrencyIso.value = this.cookieData.value.currency.iso_code
setTimeout(() => (this.sessionOngoing = false), 2000)
}
setLanguage(iso: string) {
this.currentLanguageIso.value = iso
}
setCurrency(iso: string) {
this.currentCurrencyIso.value = iso
}
setCountry(iso: string) {
this.currentCountryIso.value = iso
}
setQueryParams() {
if (this.currentLanguageIso.value.length > 0) {
this.urlParams.set('lang_iso', this.currentLanguageIso.value)
}
else {
this.urlParams.delete('lang_iso')
}
if (this.currentCountryIso.value.length > 0) {
this.urlParams.set('country_iso', this.currentCountryIso.value)
}
else {
this.urlParams.delete('country_iso')
}
if (this.currentCurrencyIso.value.length > 0) {
this.urlParams.set('currency_iso', this.currentCurrencyIso.value)
}
else {
this.urlParams.delete('currency_iso')
}
}
}
export default defineNuxtPlugin(async (nuxtApp) => {
const loaded = [] as Array<string>
const { $i18n: i18n } = nuxtApp as unknown as { $i18n: VueI18n }
const { $router: router } = nuxtApp as unknown as { $router: Router }
i18n.onBeforeLanguageSwitch = async (_, newLocale) => {
if (loaded.includes(newLocale)) return
try {
loaded.push(newLocale)
const { data } = await useMyFetch<GenericResponse<object>>(
'/api/public/front/translation',
)
i18n.setLocaleMessage(newLocale, data)
}
catch (err) {
console.error('❌ Failed to load translation for locale:', newLocale)
throw err
}
}
const session = new Session(i18n, router)
await session.loadSession()
nuxtApp.provide('session', session)
})

10
plugins/02_initLoad.ts Normal file
View File

@ -0,0 +1,10 @@
import { defineNuxtPlugin } from '#app'
export default defineNuxtPlugin(async () => {
const menuStore = useMenuStore()
await menuStore.loadMenu()
await menuStore.getLocales()
const store = useStore()
await store.getMinValue()
await store.getCalculator()
})

View File

@ -1,31 +0,0 @@
import type { VueI18n } from "vue-i18n";
import { usePB } from "~/composables/usePB";
export default defineNuxtPlugin(async (nuxtApp) => {
const loaded = [] as Array<string>;
const i18n = nuxtApp.$i18n as VueI18n;
const pb = usePB();
i18n.onBeforeLanguageSwitch = async (oldLocale, newLocale) => {
if (loaded.includes(newLocale)) return;
try {
const translation = await pb.collection("translation").getList(1, 1, {
expand: "id_lang",
filter: `id_lang.iso='${newLocale}'`,
});
if (translation.totalItems === 1) {
i18n.setLocaleMessage(newLocale, translation.items[0].data);
} else {
i18n.setLocaleMessage(newLocale, {});
}
loaded.push(newLocale);
} catch (err) {
console.error("❌ Failed to load translation for locale:", newLocale);
throw err;
}
};
});

View File

@ -1,9 +0,0 @@
import { defineNuxtPlugin } from "#app";
export default defineNuxtPlugin(async () => {
const menuStore = useMenuStore();
await menuStore.loadMenu();
await menuStore.loadFooter();
await menuStore.getCountryList();
await menuStore.getCurrencies();
});

View File

@ -0,0 +1,10 @@
import * as Vue3Toastify from 'vue3-toastify'
import 'vue3-toastify/dist/index.css'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(Vue3Toastify.default, { autoClose: 2000 })
return {
provide: { toast: Vue3Toastify.toast },
}
})

11618
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.29289 17.4628L0.585786 18.1699L2 19.5841L2.70711 18.877L1.29289 17.4628ZM19.9706 1.19936C19.9706 0.647074 19.5228 0.199359 18.9706 0.199359L9.97056 0.19936C9.41828 0.199359 8.97056 0.647075 8.97056 1.19936C8.97056 1.75164 9.41828 2.19936 9.97056 2.19936L17.9706 2.19936L17.9706 10.1994C17.9706 10.7516 18.4183 11.1994 18.9706 11.1994C19.5228 11.1994 19.9706 10.7516 19.9706 10.1994L19.9706 1.19936ZM2 18.1699L2.70711 18.877L19.6777 1.90647L18.9706 1.19936L18.2635 0.492253L1.29289 17.4628L2 18.1699Z" fill="#1A1A1A"/>
</svg>

Before

Width:  |  Height:  |  Size: 633 B

View File

@ -1,3 +0,0 @@
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.7364 1.49211L2.0293 0.785005L0.615083 2.19922L1.32219 2.90633L2.7364 1.49211ZM18.9999 20.1698C19.5521 20.1698 19.9999 19.7221 19.9999 19.1698L19.9999 10.1698C19.9999 9.6175 19.5521 9.16978 18.9999 9.16978C18.4476 9.16978 17.9999 9.6175 17.9999 10.1698L17.9999 18.1698L9.99986 18.1698C9.44758 18.1698 8.99986 18.6175 8.99986 19.1698C8.99986 19.7221 9.44757 20.1698 9.99986 20.1698L18.9999 20.1698ZM2.0293 2.19922L1.32219 2.90633L18.2928 19.8769L18.9999 19.1698L19.707 18.4627L2.7364 1.49211L2.0293 2.19922Z" fill="#1A1A1A"/>
</svg>

Before

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

3
public/photo.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="76" height="53" viewBox="0 0 76 53" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M76 49.8V2C76 0.9 75.1 0 74 0H2C0.9 0 0 0.9 0 2V50C0 52.5 3.3 52 2 52H74C77 52 75.6 47.4 76 49.8ZM6 48L28 19.3L46.4 43.4L49.9 48H6ZM55 48L50.5 42.2L58 32.4L69.9 48H55ZM72 44L59.6 27.8C58.8 26.7 57.2 26.7 56.4 27.8L48 38.8L29.6 14.8C28.8 13.7 27.2 13.7 26.4 14.8L4 44.1V4H72V44ZM49 10C45.1 10 42 13.1 42 17C42 20.9 45.1 24 49 24C52.9 24 56 20.9 56 17C56 13.1 52.8 10 49 10ZM49 20C47.4 20 46 18.7 46 17C46 15.3 47.3 14 49 14C50.7 14 52 15.3 52 17C52 18.7 50.6 20 49 20Z" fill="#525252"/>
</svg>

After

Width:  |  Height:  |  Size: 598 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

577
stores/checkoutStore.ts Normal file
View File

@ -0,0 +1,577 @@
import { validation } from '../utils/validation'
import { REGEX_PHONE } from '../utils/regex'
import type { GenericResponse, GenericResponseItems, UserCart } from '~/types'
import type {
AddressesList,
CheckoutAddress,
CheckoutOrder,
Payment,
UserAddressOfficial,
} from '~/types/checkout'
import type { CartProduct } from '~/types/product'
export const useCheckoutStore = defineStore('checkoutStore', () => {
const { $toast } = useNuxtApp()
const menuStore = useMenuStore()
const selectedIso = ref(menuStore.selectedCountry)
const vLegal = ref(false)
const vTerms = ref(false)
const vNote = ref('')
const legalValidation = ref(false)
const termsValidation = ref(false)
// get address list
const addressesList = ref<AddressesList[]>()
const activeAddress = ref<AddressesList | null>()
async function getAddressList() {
try {
const { data } = await useMyFetch<GenericResponse<AddressesList[]>>(
`/api/restricted/user/addresses`,
{
headers: {
'Content-Type': 'application/json',
},
onErrorOccured: async (_, status) => {
throw createError({
statusCode: status,
statusMessage: `HTTP error: ${status}`,
})
},
},
)
addressesList.value = data
activeAddress.value = addressesList.value[0]
}
catch (error) {
console.error('restrictedAddress error:', error)
}
}
// get user data
const userName = ref('')
const lastName = ref('')
const address = ref('')
const postCode = ref('')
const city = ref('')
const country = ref('')
const phoneNumber = ref('')
const accountPhoneNumber = ref('')
async function getUserData() {
try {
const { data } = await useMyFetch<GenericResponse<UserAddressOfficial>>(
`/api/restricted/user/address/official`,
{
headers: {
'Content-Type': 'application/json',
},
onErrorOccured: async (_, status) => {
throw createError({
statusCode: status,
statusMessage: `HTTP error: ${status}`,
})
},
},
)
userName.value = data.address.name
lastName.value = data.address.surname
address.value = data.address.street
postCode.value = data.address.postcode
city.value = data.address.city
country.value = data.address.country.country_lang[0].name
}
catch (error) {
console.error('getUserData error:', error)
}
}
// upload new address
const vNewAddressName = ref('')
const vNewAddressSurname = ref('')
const vNewAddressAddress = ref('')
const vNewAddressCode = ref('')
const vNewAddressCity = ref('')
const vNewAddressCountry = ref()
const vUseAccountPhoneNumber = ref(false)
const isOpen = ref<boolean>(false)
async function uploadAddress() {
try {
const res = await useMyFetch<GenericResponse<object>>(
`/api/restricted/user/address`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
address: {
city: vNewAddressCity.value,
country_iso: vNewAddressCountry.value.iso_code,
name: vNewAddressName.value,
postcode: vNewAddressCode.value,
street: vNewAddressAddress.value,
surname: vNewAddressSurname.value,
},
}),
onErrorOccured: async (_, status) => {
throw createError({
statusCode: status,
statusMessage: `HTTP error: ${status}`,
})
},
},
)
if (res.status === 200) {
$toast.success('Address successfully added', {
autoClose: 5000,
dangerouslyHTMLString: true,
})
isOpen.value = false
getAddressList()
}
else {
$toast.error('Failed to add address. Please try again.', {
autoClose: 5000,
dangerouslyHTMLString: true,
})
}
}
catch (error) {
console.error('uploadAddress error:', error)
}
}
const currentPrefix = ref<string | number>(
menuStore.selectedCountry.call_prefix,
)
const changePrefix = (item: string) => {
currentPrefix.value = item
}
const phoneValidation = ref<boolean | null>(null)
// send checkout form
const userStore = useUserStore()
async function sendForm() {
const phoneNum = vUseAccountPhoneNumber.value
? accountPhoneNumber.value
: `${currentPrefix.value}${phoneNumber.value}`.replaceAll(' ', '').trim()
// if (vUseAccountPhoneNumber.value) {
// phoneNum = phoneNumber.value;
// }
phoneValidation.value = validation(phoneNum, 1, 49, REGEX_PHONE)
if (!phoneValidation.value && !vUseAccountPhoneNumber.value) {
return
}
try {
const res = await useMyFetch<GenericResponse<object>>(
`/api/restricted/cart/checkout/delivery`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
address: {
city: activeAddress.value?.address.city,
country_iso: activeAddress.value?.address.country_iso,
name: activeAddress.value?.address.name,
postcode: activeAddress.value?.address.postcode,
street: activeAddress.value?.address.street,
surname: activeAddress.value?.address.surname,
},
phone_number: phoneNum,
email: userStore.fullUserData?.email,
}),
onErrorOccured: async (_, status) => {
throw createError({
statusCode: status,
statusMessage: `HTTP error: ${status}`,
})
},
},
)
console.log(res)
if (res.status === 200) {
$toast.success('Form successfully sent', {
autoClose: 5000,
dangerouslyHTMLString: true,
})
menuStore.navigateToItem(
menuStore.menuItems?.find(item => item.id === 13),
)
}
else {
$toast.error('Failed to send form. Please try again.', {
autoClose: 5000,
dangerouslyHTMLString: true,
})
}
}
catch (error) {
console.error('uploadAddress error:', error)
}
}
// get checkout
async function getCheckout() {
try {
await useMyFetch<GenericResponse<object>>(
`/api/restricted/cart/checkout`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
onErrorOccured: async (_, status) => {
throw createError({
statusCode: status,
statusMessage: `HTTP error: ${status}`,
})
},
},
)
}
catch (error) {
console.error('uploadAddress error:', error)
}
}
// 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
interface DeliveryOptionItem {
country_iso: string
country_name: string
delivery_supplier_id: number
delivery_supplier_name: string
id: number
shippment_price: string
}
const deliveryOption = ref<DeliveryOptionItem[]>([])
const currentDelivery = ref<DeliveryOptionItem | null>(null)
const shippingPrice = ref<number>(0)
async function getDeliveryOptions() {
try {
const { data } = await useMyFetch<GenericResponseItems<DeliveryOptionItem[]>>(
`/api/restricted/cart/checkout/delivery-options`,
{
headers: {
'Content-Type': 'application/json',
},
onErrorOccured: async (_, status) => {
throw createError({
statusCode: status,
statusMessage: `HTTP error: ${status}`,
})
},
},
)
if (data.items && data.items.length > 0) {
deliveryOption.value = data.items
currentDelivery.value = data.items[0]
shippingPrice.value = Number(data.items[0].shippment_price)
fullPrice.value += shippingPrice.value
}
}
catch (error) {
console.error('getDeliveryOptions error:', error)
}
}
const setCurrentDelivery = (item: DeliveryOptionItem) => {
shippingPrice.value = Number(item.shippment_price)
currentDelivery.value = item
fullPrice.value = Number(fullPrice.value) + Number(shippingPrice.value)
}
const defaultAddress = ref<CheckoutAddress | undefined>()
async function getDefAddress() {
try {
const { data } = await useMyFetch<GenericResponse<{ addresses: Address[] }>>(
`/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 => el.is_default === true || el.is_default === 'true',
)
}
catch (error) {
console.error('getUserCart error:', error)
}
}
// get bank data
const paymentMethods = ref([] as Payment[])
const fullAddress = ref<Address>()
const currentPayment = ref<Payment | null>()
async function getBankAccount() {
try {
const { data } = await useMyFetch<GenericResponse<Payment[]>>(
`/api/restricted/suitable-bank-accounts/${menuStore.selectedCurrency.iso_code}/${fullAddress.value?.country_iso}`,
{
headers: {
'Content-Type': 'application/json',
},
onErrorOccured: async (_, status) => {
throw createError({
statusCode: status,
statusMessage: `HTTP error: ${status}`,
})
},
},
)
paymentMethods.value = data
currentPayment.value = data[0]
}
catch (error) {
console.error('getUserCart error:', error)
}
}
// get order (summary)
async function getOrder() {
try {
const { data } = await useMyFetch<GenericResponse<CheckoutOrder>>(
`/api/restricted/cart/checkout/order`,
{
headers: {
'Content-Type': 'application/json',
},
onErrorOccured: async (_, status) => {
throw createError({
statusCode: status,
statusMessage: `HTTP error: ${status}`,
})
},
},
)
fullAddress.value = data.delivery_details.address
}
catch (error) {
console.error('getOrder error:', error)
}
}
async function setNewAddress(indexItem: number) {
currentPayment.value = paymentMethods.value.find(
(item, index) => indexItem === index,
)
}
// send summary form
async function sendSummaryForm() {
legalValidation.value = !vLegal.value
termsValidation.value = !vTerms.value
if (!vTerms.value && !vLegal.value) {
return
}
try {
await useMyFetch<GenericResponse<object>>(
`/api/restricted/cart/checkout/delivery`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
accept_general_conditions: true,
accept_long_purchase: true,
address: fullAddress.value,
delivery_option_id: currentDelivery.value?.id,
note: vNote.value,
}),
onErrorOccured: async (_, status) => {
throw createError({
statusCode: status,
statusMessage: `HTTP error: ${status}`,
})
},
},
)
await putCheckoutBankAccount()
await markOrder()
await getUserCart()
}
catch (error) {
console.error('uploadAddress error:', error)
}
}
// put checkout bank-account
async function putCheckoutBankAccount() {
try {
await useMyFetch<GenericResponse<object>>(
`/api/restricted/cart/checkout/bank-account/${currentPayment.value?.id}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
onErrorOccured: async (_, status) => {
throw createError({
statusCode: status,
statusMessage: `HTTP error: ${status}`,
})
},
},
)
}
catch (error) {
console.error('uploadAddress error:', error)
}
}
const modalMadeOrder = ref(false)
async function markOrder() {
try {
const res = await useMyFetch<GenericResponse<object>>(
`/api/restricted/cart/checkout/order`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
onErrorOccured: async (_, status) => {
throw createError({
statusCode: status,
statusMessage: `HTTP error: ${status}`,
})
},
},
)
if (res.status === 200 || res.status === 201) {
$toast.success('Address successfully added', {
autoClose: 5000,
dangerouslyHTMLString: true,
})
modalMadeOrder.value = true
}
else {
$toast.error('Failed to add address. Please try again.', {
autoClose: 5000,
dangerouslyHTMLString: true,
})
}
// window.location.href = `/golden-panel/my-purchases/${res._data?.data.id}`;
}
catch (error) {
console.error('uploadAddress error:', error)
}
}
return {
addressesList,
activeAddress,
isOpen,
selectedIso,
phoneValidation,
userName,
lastName,
address,
postCode,
city,
country,
phoneNumber,
accountPhoneNumber,
vUseAccountPhoneNumber,
currentPrefix,
vNewAddressName,
vNewAddressSurname,
vNewAddressAddress,
vNewAddressCode,
vNewAddressCity,
vNewAddressCountry,
products,
fullPrice,
fullProductsPrice,
deliveryOption,
currentDelivery,
shippingPrice,
defaultAddress,
paymentMethods,
currentPayment,
fullAddress,
vLegal,
vTerms,
vNote,
legalValidation,
termsValidation,
modalMadeOrder,
changePrefix,
getCheckout,
getAddressList,
getUserData,
uploadAddress,
sendForm,
getUserCart,
getDeliveryOptions,
getDefAddress,
setCurrentDelivery,
getBankAccount,
getOrder,
setNewAddress,
sendSummaryForm,
}
})

View File

@ -1,101 +0,0 @@
import type { CountryList, PartnersList } from "~/types";
export const useMapStore = defineStore("mapStore", () => {
const partnersList = ref<PartnersList[]>([
{
country_iso: "cz",
total: 9,
country_name: "Czech Republic",
},
{
country_iso: "de",
total: 1,
country_name: "Germany",
},
{
country_iso: "ie",
total: 1,
country_name: "Ireland",
},
{
country_iso: "nl",
total: 1,
country_name: "Netherlands",
},
{
country_iso: "pl",
total: 61,
country_name: "Poland",
},
]);
const customersList = ref([
"be",
"cz",
"de",
"dk",
"gb",
"ie",
"it",
"nl",
"no",
"pl",
"sk",
"at",
"lt",
"is",
"se"
]);
// async function getPartnersList() {
// try {
// const res = await fetch(
// `http://127.0.0.1:4000/api/public/partners/count`,
// {
// headers: {
// "Content-Type": "application/json",
// },
// }
// );
// if (!res.ok) {
// throw new Error(`HTTP error: ${res.status}`);
// }
// const data = await res.json();
// partnersList.value = data.data
// } catch (error) {
// console.error("getList error:", error);
// }
// }
// async function getCustomerList() {
// try {
// const res = await fetch(
// `http://127.0.0.1:4000/api/public/customer/countries`,
// {
// headers: {
// "Content-Type": "application/json",
// },
// }
// );
// if (!res.ok) {
// throw new Error(`HTTP error: ${res.status}`);
// }
// const data = await res.json();
// customersList.value = data.data
// } catch (error) {
// console.error("getList error:", error);
// }
// }
return {
partnersList,
customersList,
// getPartnersList,
// getCustomerList,
};
});

View File

@ -1,264 +1,308 @@
import { usePB } from "~/composables/usePB";
import { useStore } from './store'
import type {
CountryList,
Currencies,
FooterListResponse,
MenuListResponse,
PBFooterItem,
PBMenuItem,
UIMenuItem,
} from "~/types";
import { useStore } from "./store";
import { ref, watch } from "vue";
Country,
Currency,
FrontMenu,
GenericResponse,
GenericResponseItems,
Language,
UIFrontMenu,
} from '~/types'
import { useMyFetch } from '#imports'
function buildTreeRecursive(
data: (PBMenuItem | UIMenuItem)[],
parentId: string
): UIMenuItem[] {
data: (FrontMenu | UIFrontMenu)[],
parentId: number,
): UIFrontMenu[] {
const children = data.filter(
(item): item is UIMenuItem =>
item.id_parent === parentId && !item.is_default
);
(item): item is UIFrontMenu =>
item.id_parent === parentId && !item.is_default,
)
return children.map((item) => ({
return children.map(item => ({
...item,
children: buildTreeRecursive(data, item.id),
}));
}))
}
export const useMenuStore = defineStore("menuStore", () => {
const pb = usePB();
const store = useStore();
const { $i18n } = useNuxtApp();
const router = useRouter();
const route = useRoute();
export const useMenuStore = defineStore('menuStore', () => {
const store = useStore()
const { $i18n } = useNuxtApp()
// const session = useSession();
const { $session } = useNuxtApp()
const router = useRouter()
const route = useRoute()
const openMenu = ref(false);
const openDropDown = ref(false);
const openMenu = ref(false)
const openDropDown = ref(false)
const defaultMenu = ref();
const menu = ref<UIMenuItem[]>([]);
const menuItems = ref<MenuListResponse>();
const defaultMenu = ref()
const footerItems = ref<FooterListResponse>();
const countryList = ref<CountryList[]>();
const currencies = ref<Currencies[]>();
const menu = ref([] as UIFrontMenu[])
const menuItems = ref([] as FrontMenu[])
// curr/country
const selectedCountry = ref();
const selectedPhoneCountry = ref();
const selectedCurrency = ref<Currencies>();
const selectedCountry = ref({} as Country)
const selectedPhoneCountry = ref({} as Country)
const selectedCurrency = ref({} as Currency)
const selectedLanguage = ref({} as Language)
const countries = ref([] as Country[])
const currencies = ref([] as Currency[])
const languages = ref([] as Language[])
const getLocales = async () => {
const { data: countriesList } = await useMyFetch<
GenericResponse<Country[]>
>(`/api/public/country/list`)
countries.value = countriesList
selectedCountry.value = countriesList.find(
country => country.iso_code === $session.currentCountryIso.value,
) as Country
selectedPhoneCountry.value = countriesList.find(
country => country.iso_code === $session.currentCountryIso.value,
) as Country
const { data: currenciesList } = await useMyFetch<
GenericResponseItems<Currency[]>
>(`/api/public/currencies`)
currencies.value = currenciesList.items
selectedCurrency.value = currenciesList.items.find(
currency => currency.iso_code === $session.currentCurrencyIso.value,
) as Currency
const { data: languagesList } = await useMyFetch<
GenericResponseItems<Language[]>
>(`/api/public/languages`)
languages.value = languagesList.items
selectedLanguage.value = languagesList.items.find(
language => language.iso_code === $session.currentLanguageIso.value,
) as Language
}
const loadMenu = async () => {
try {
menuItems.value = (await pb
.collection("menu_view")
.getList<PBMenuItem>(1, 50, {
filter: `id_lang="${$i18n.locale.value}"&&active=true`,
sort: "position_id",
})) as MenuListResponse;
const { data } = await useMyFetch<GenericResponse<FrontMenu[]>>(
`/api/public/front/menu`,
{
onErrorOccured: (err, status) => {
console.log(err, status)
},
},
)
const root = menuItems.value.items.find((item) => item.is_root);
defaultMenu.value = menuItems.value.items.find((item) => item.is_default);
menuItems.value = data
const root = data.find(item => item.is_root) as UIFrontMenu
defaultMenu.value = data.find(item => item.is_default)
if (root) {
menu.value = buildTreeRecursive(menuItems.value.items, root.id);
store.currentPageID = menu.value[0]?.id_page || "";
} else {
console.warn("Root menu item not found");
menu.value = [];
menu.value = buildTreeRecursive(data, root.id)
}
} catch (error) {
console.log(error);
else {
console.warn('Root menu item not found')
menu.value = []
}
};
const loadFooter = async () => {
try {
footerItems.value = (await pb
.collection("footer_view")
.getList<PBFooterItem>(1, 50, {
filter: `id_lang="${$i18n.locale.value}"`,
})) as FooterListResponse;
} catch (error) {
console.log(error);
}
};
const getCountryList = async () => {
try {
const res = await fetch(
`http://127.0.0.1:4000/api/public/country/list`,
{
headers: {
"Content-Type": "application/json",
},
}
);
if (!res.ok) {
throw new Error(`HTTP error: ${res.status}`);
}
const data = await res.json();
countryList.value = data.data
if (countryList.value)
selectedPhoneCountry.value = countryList.value[0]
} catch (error) {
console.error("getList error:", error);
catch (error) {
console.log(error)
}
}
const getCurrencies = async () => {
try {
const res = await fetch(
`http://127.0.0.1:4000/api/public/currencies`,
{
headers: {
"Content-Type": "application/json",
},
}
);
if (!res.ok) {
throw new Error(`HTTP error: ${res.status}`);
}
const data = await res.json();
currencies.value = data.data.items
} catch (error) {
console.error("getList error:", error);
}
}
const navigateToItem = (item?: UIMenuItem) => {
const navigateToItem = (item?: UIFrontMenu) => {
if (item) {
router.push({
params: { slug: item.link_rewrite, id: item.id_page },
params: { slug: item.front_menu_lang[0].link_rewrite, id: item.id },
name: `id-slug___${$i18n.locale.value}`,
});
openDropDown.value = false;
})
openDropDown.value = false
}
else {
router.push({
params: {
slug: defaultMenu.value.link_rewrite,
id: defaultMenu.value.id_page,
slug: defaultMenu.value.front_menu_lang[0].link_rewrite,
id: defaultMenu.value.id,
},
name: `id-slug___${$i18n.locale.value}`,
});
})
}
}
};
function navigateToShop() {
navigateToItem(menuItems.value?.items.find(item => item.page_name === 'shop'))
navigateToItem(menuItems.value?.find(item => item.id === 5))
}
// function redirectToPage(link_rewrite: string) {
// const page = menuItems.value?.items.find(
// (item) => item.link_rewrite === link_rewrite
// );
function getShopMenu() {
return menuItems.value?.find(item => item.id === 5)
}
// if (!page?.id_page || !page?.link_rewrite) {
// console.warn(`Page not found or missing data for name: ${link_rewrite}`);
// return;
// }
function getProductMenu() {
return menuItems.value?.find(item => item.id === 14)
}
// router.push({
// params: {
// id: page?.id_page,
// slug: page?.link_rewrite,
// },
// });
// }
function getRegistrationMenu() {
return menuItems.value?.find(item => item.id === 4)
}
const getFirstImage = () => {
const req = useRequestEvent();
const url = useRequestURL();
let img = "";
function getLoginMenu() {
return menuItems.value?.find(item => item.id === 11)
}
function getInvestitionMenu() {
return menuItems.value?.find(item => item.id === 6)
}
function getAboutUsMenu() {
return menuItems.value?.find(item => item.id === 7)
}
function getBusinessMenu() {
return menuItems.value?.find(item => item.id === 8)
}
function getAboutGoldMenu() {
return menuItems.value?.find(item => item.id === 9)
}
function getContactMenu() {
return menuItems.value?.find(item => item.id === 10)
}
const getFirstImage = (size: 'l' | 'm' | 's' = 'm', needbaseurl: boolean) => {
const req = useRequestEvent()
const url = useRequestURL()
// let img = "";
const img: string[] = []
for (const s in store.components) {
store.components[s].section_img.map((item) => {
img = `${req?.headers.get("x-forwarded-proto") || url.protocol}://${req?.headers.get("x-forwarded-host") || req?.headers.get("host")
}/api/files/${store.components[s].image_collection}/${store.components[s].section_id
}/${item}?thumb=400x0`;
});
if (img.length > 0) return img;
if (store.components[s].front_section.img.length === 0) continue
img.push(
`/api/public/file/${store.components[s].front_section.img[0]}_${size}.webp`,
)
if (img.length > 0) break
}
if (img.length > 0) {
if (needbaseurl) {
return `${req?.headers.get('x-forwarded-proto') || url.protocol}://${req?.headers.get('x-forwarded-host')
|| url.host
|| req?.headers.get('host')
}${img[0]}`
}
return img[0]
}
return ''
}
return "";
};
const headMeta = computed(() => {
const item = menuItems.value?.items.find(
(item) => item.id_page === route.params.id
);
return {
title: item?.meta_title,
const item = menuItems.value?.find(
item => item.id.toString() === route.params.id,
)
const meta = {
title: item?.front_menu_lang[0].meta_title,
htmlAttrs: {
lang: $i18n.locale.value,
},
link: [{ rel: "manifest", href: "/api/manifest.json" }],
meta: [
link: [
// { rel: "manifest", href: "/api/manifest.json" }
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.png' },
],
script: [
{
hid: "description",
name: "description",
content: item?.meta_description,
},
{
property: "og:title",
content: item?.meta_title,
},
{
property: "og:description",
content: item?.meta_description,
},
{
property: "og:image",
content: getFirstImage(),
},
{
property: "twitter:title",
content: item?.meta_title,
},
{
property: "twitter:description",
content: item?.meta_description,
},
{
property: "twitter:image",
content: getFirstImage(),
src: 'https://leiadmin.com/leitag.js?lei=894500UT83EISNNA8D04&color=dark',
defer: true,
},
],
};
});
meta: [
{
hid: 'description',
name: 'description',
content: item?.front_menu_lang[0].meta_description,
},
{
property: 'og:title',
content: item?.front_menu_lang[0].meta_title,
},
{
property: 'og:description',
content: item?.front_menu_lang[0].meta_description,
},
{
property: 'og:image',
content: getFirstImage('m', true),
},
{
property: 'twitter:title',
content: item?.front_menu_lang[0].meta_title,
},
{
property: 'twitter:description',
content: item?.front_menu_lang[0].meta_description,
},
{
property: 'twitter:image',
content: getFirstImage('m', true),
},
],
}
watch($i18n.locale, async () => {
await loadMenu();
await loadFooter();
});
const preload = getFirstImage('l', false)
if (preload) {
meta.link.push({ rel: 'preload', as: 'image', href: preload } as never)
}
watch(selectedCurrency, () => {
store.getCalculator()
return meta
})
const formatPrice = (value: number): string => {
return value.toLocaleString(selectedLanguage.value.iso_code, {
minimumFractionDigits: selectedCurrency.value.precision,
maximumFractionDigits: selectedCurrency.value.precision,
currency: selectedCurrency.value.iso_code,
style: 'currency',
currencyDisplay: 'symbol',
currencySign: 'accounting',
})
}
// watches
watch(
() => $session.cookieData,
async () => {
await getLocales()
await loadMenu()
await store.getMinValue()
await store.getCalculator()
},
{ deep: true },
)
return {
menu,
menuItems,
footerItems,
openMenu,
openDropDown,
countryList,
currencies,
languages,
countries,
selectedCountry,
selectedCurrency,
selectedPhoneCountry,
selectedLanguage,
defaultMenu,
headMeta,
navigateToShop,
loadMenu,
loadFooter,
getCountryList,
navigateToItem,
// redirectToPage,
getCurrencies
};
});
getLocales,
getProductMenu,
formatPrice,
getRegistrationMenu,
getLoginMenu,
getInvestitionMenu,
getAboutUsMenu,
getBusinessMenu,
getAboutGoldMenu,
getContactMenu,
getShopMenu,
}
})

View File

@ -1,105 +1,188 @@
export const useProductStore = defineStore("productStore", () => {
const productList = ref();
const modules = ref();
import { useMyFetch } from '#imports'
import type {
GenericResponse,
GenericResponseChildren,
GenericResponseItems,
} from '~/types'
import type { Product } from '~/types/product'
export const useProductStore = defineStore('productStore', () => {
const { $toast } = useNuxtApp()
const productList = ref<Product[]>()
const modules = ref()
const checkoutStore = useCheckoutStore()
async function getList(count: number, categoryId = 1) {
try {
const res = await fetch(
`http://127.0.0.1:4000/api/public/products/category/${categoryId}?p=1&elems=${count}`,
const { data } = await useMyFetch<GenericResponseItems<[]>>(
`/api/public/products/category/${categoryId}?p=1&elems=${count}`,
{
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
}
);
onErrorOccured: async (_, status) => {
// await navigateTo("/error", { replace: true });
throw createError({
statusCode: status,
statusMessage: `HTTP error: ${status}`,
})
},
},
)
if (!res.ok) {
throw new Error(`HTTP error: ${res.status}`);
productList.value = data.items
}
const data = await res.json();
productList.value = data.data.items;
} catch (error) {
console.error("getList error:", error);
catch (error) {
console.error('getList error:', error)
}
}
async function getModules() {
try {
const res = await fetch(
`http://127.0.0.1:4000/api/public/module/e_shop`,
const { data } = await useMyFetch<GenericResponseChildren<[]>>(
`/api/public/module/e_shop`,
{
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
}
);
onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`)
},
},
)
if (!res.ok) {
throw new Error(`HTTP error: ${res.status}`);
}
const data = await res.json();
modules.value = data.children.find(
(item: { id: number; name: string }) =>
item.name === "currency_rates_bar"
);
} catch (error) {
console.error("getList error:", error);
(item: { id: number, name: string }) =>
item.name === 'currency_rates_bar',
)
}
catch (error) {
console.error('getList error:', error)
}
}
async function addToCart(product) {
async function incrementCartItem(id: number) {
try {
const res = await fetch(
`http://127.0.0.1:4000/api/public/user/cart/item/add/${product.id}/1`,
const res = await useMyFetch<GenericResponse<object>>(
`/api/public/user/cart/item/add/${id}/1`,
{
method: "PUT",
method: 'PUT',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
}
);
onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`)
},
},
)
if (!res.ok) {
throw new Error(`HTTP error: ${res.status}`);
if (res.status === 200) {
$toast.success('Item successfully added to your cart.', {
autoClose: 5000,
dangerouslyHTMLString: true,
})
await checkoutStore.getUserCart()
}
const data = await res.json();
console.log(data);
} catch (error) {
console.error("getList error:", error);
else {
$toast.error('Failed to add item to cart. Please try again.', {
autoClose: 5000,
dangerouslyHTMLString: true,
})
}
}
catch (error) {
$toast.error('An unexpected error occurred while updating your cart.', {
autoClose: 5000,
dangerouslyHTMLString: true,
})
console.error('incrementCartItem error:', error)
}
}
const cart = ref();
// async function getCart() {
// try {
// const res = await fetch(`http://127.0.0.1:4000/api/public/user/cart`, {
// headers: {
// "Content-Type": "application/json",
// },
// });
async function decrementCartItem(id: number) {
try {
const res = await useMyFetch<GenericResponse<object>>(
`/api/public/user/cart/item/subtract/${id}/1`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`)
},
},
)
// if (!res.ok) {
// throw new Error(`HTTP error: ${res.status}`);
// }
if (res.status === 200) {
$toast.success('Item successfully removed from your cart.', {
autoClose: 5000,
dangerouslyHTMLString: true,
})
await checkoutStore.getUserCart()
}
else {
$toast.error('Failed to removed item from cart. Please try again.', {
autoClose: 5000,
dangerouslyHTMLString: true,
})
}
}
catch (error) {
$toast.error('An unexpected error occurred while updating your cart.', {
autoClose: 5000,
dangerouslyHTMLString: true,
})
console.error('decrementCartItem error:', error)
}
}
// const data = await res.json();
// console.log(data);
async function deleteCartItem(id: number) {
try {
const res = await useMyFetch<GenericResponse<object>>(
`/api/public/user/cart/item/${id}`,
{
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`)
},
},
)
// cart.value = data;
// } catch (error) {
// console.error("getList error:", error);
// }
// }
if (res.status === 200) {
$toast.success('Item successfully removed from your cart.', {
autoClose: 5000,
dangerouslyHTMLString: true,
})
await checkoutStore.getUserCart()
}
else {
$toast.error('Failed to removed item from cart. Please try again.', {
autoClose: 5000,
dangerouslyHTMLString: true,
})
}
}
catch (error) {
$toast.error('An unexpected error occurred while updating your cart.', {
autoClose: 5000,
dangerouslyHTMLString: true,
})
console.error('deleteCartItem error:', error)
}
}
return {
productList,
modules,
getList,
getModules,
addToCart,
// getCart,
};
});
incrementCartItem,
decrementCartItem,
deleteCartItem,
}
})

View File

@ -1,151 +1,110 @@
import { usePB } from "~/composables/usePB";
import type { componentsListType, PBPageItem } from "~/types";
// import { useI18n } from "vue-i18n";
import { useMyFetch } from '#imports'
import type {
componentsListType,
GenericResponse,
PlanPrediction,
} from '~/types'
import type { FrontPageSection } from '~/types/frontSection'
export const useStore = defineStore("store", () => {
const currentPageID = ref("");
const pb = usePB();
const { $i18n } = useNuxtApp();
export const useStore = defineStore('store', () => {
const currentPageID = ref('')
// calculator
const monthlySavings = ref(137);
const storagePeriod = ref(10);
const totalInvestment = ref()
const monthlySavings = ref(137)
const storagePeriod = ref(10)
const totalInvestment: Ref<number> = ref(0)
const minValue = ref()
// login
const email = ref()
const password = ref()
const components = ref({} as FrontPageSection[])
const components = ref({} as PBPageItem[]);
const getSections = async (id: string) => {
pb.cancelRequest("menu_view");
components.value = (
await pb.collection<PBPageItem>("page_view").getList(1, 50, {
filter: `id="${id}"&&(section_lang_id_lang="${$i18n.locale.value
}"||section_is_no_lang=${true})`,
sort: "page_section_id_position",
})
).items as PBPageItem[];
};
const { data } = await useMyFetch<GenericResponse<FrontPageSection[]>>(
`/api/public/front/sections/${id}`,
)
components.value = data
}
async function getComponents(): Promise<componentsListType[]> {
try {
const children = components.value;
const children = components.value
if (!children || !Array.isArray(children)) {
console.warn("No components available in store.");
return [];
console.warn('No components available in store.')
return []
}
const componentsList = [] as componentsListType[];
const componentsList = [] as componentsListType[]
for (const child of children) {
const componentName = child.component_name;
const pageName = child.page_name;
if (!componentName) continue;
const componentName = child.front_section.component_name
if (!componentName) continue
try {
const componentInstance = (
await import(`@/components/section/${componentName}.vue`)
).default;
const nonReactiveComponent = markRaw(componentInstance);
).default
const nonReactiveComponent = markRaw(componentInstance)
componentsList.push({
name: componentName,
component: child,
component: child.front_section,
componentInstance: nonReactiveComponent,
data: child.section_lang_data,
});
} catch (error) {
console.error(`Failed to load component ${componentName}`, error);
})
}
catch (error) {
console.error(`Failed to load component ${componentName}`, error)
}
}
return componentsList;
} catch (error) {
console.error("Failed to process components list", error);
return componentsList
}
return [];
catch (error) {
console.error('Failed to process components list', error)
}
return []
}
async function getCalculator() {
try {
const res = await fetch(
`http://127.0.0.1:4000/api/public/plan-prediction/easy/calculate?monthly_deposit=${monthlySavings.value}&years=${storagePeriod.value}`,
const { data } = await useMyFetch<GenericResponse<PlanPrediction>>(
`/api/public/plan-prediction/easy/calculate?monthly_deposit=${monthlySavings.value}&years=${storagePeriod.value}`,
{
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`)
},
},
)
totalInvestment.value = data.total_investement_value
}
);
if (!res.ok) {
throw new Error(`HTTP error: ${res.status}`);
}
const data = await res.json();
totalInvestment.value = data.data.total_investement_value
} catch (error) {
console.error("getList error:", error);
catch (error) {
console.error('getList error:', error)
}
}
async function getMinValue() {
try {
const res = await fetch(
'http://127.0.0.1:4000/api/public/plan-prediction/free/minimum',
const { data } = await useMyFetch<GenericResponse<number>>(
'/api/public/plan-prediction/free/minimum',
{
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
}
);
if (!res.ok) {
throw new Error(`HTTP error: ${res.status}`);
}
const data = await res.json();
minValue.value = data.data
} catch (error) {
console.error("getList error:", error);
}
}
async function logIn() {
try {
const res = await fetch(
'http://127.0.0.1:4000/api/public/user/session/start',
{
method: 'POST',
body: JSON.stringify({
mail: email.value,
password: password.value
}),
headers: {
"Content-Type": "application/json",
onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`)
},
},
)
minValue.value = data
}
);
if (!res.ok) {
throw new Error(`HTTP error: ${res.status}`);
}
const data = await res.json();
minValue.value = data.data
} catch (error) {
console.error("getList error:", error);
catch (error) {
console.error('getList error:', error)
}
}
getCalculator()
getMinValue()
return {
currentPageID,
components,
@ -153,11 +112,9 @@ export const useStore = defineStore("store", () => {
monthlySavings,
storagePeriod,
minValue,
email,
password,
logIn,
getCalculator,
getComponents,
getSections,
};
});
getMinValue,
}
})

156
stores/userStore.ts Normal file
View File

@ -0,0 +1,156 @@
import type { GenericResponse } from '~/types'
import type { Customer } from '~/types/user'
export const useUserStore = defineStore('userStore', () => {
const store = useStore()
const menuStore = useMenuStore()
const checkoutStore = useCheckoutStore()
const { $toast } = useNuxtApp()
const fullUserData = ref<Customer | null>(null)
const isLogged = ref<boolean>(true)
const user = ref<string | null>(null)
async function checkIsLogged() {
try {
const { data } = await useMyFetch<GenericResponse<Customer>>(
`/api/public/user`,
{
headers: {
'Content-Type': 'application/json',
},
onErrorOccured: async (_, status) => {
throw createError({
statusCode: status,
statusMessage: `HTTP error: ${status}`,
})
},
},
)
if ('loggedin' in data && data.loggedin === true) {
isLogged.value = true
user.value = `${data.first_name} ${data.last_name}` as string
fullUserData.value = data
checkoutStore.accountPhoneNumber = fullUserData.value.phone_number
}
else {
isLogged.value = false
user.value = null
fullUserData.value = null
}
}
catch (error) {
console.error('checkIsLogged error:', error)
}
}
// login
const email = ref()
const password = ref()
const vLogin = ref<boolean>(true)
const vCodeVerify = ref<boolean>(false)
const vCode = ref<number | null>(null)
async function logIn() {
try {
const data = await useMyFetch<GenericResponse<object>>(
`/api/public/user/session/start`,
{
method: 'POST',
body: JSON.stringify({
mail: email.value,
password: password.value,
}),
headers: {
'Content-Type': 'application/json',
},
onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`)
},
},
)
if (data.status === 200 || data.status === 201) {
console.log(vCodeVerify.value)
$toast.success('Code successfully sent to your email', {
autoClose: 5000,
dangerouslyHTMLString: true,
})
vLogin.value = false
vCodeVerify.value = true
}
else {
$toast.error('Failed to sent code to your email. Please try again.', {
autoClose: 5000,
dangerouslyHTMLString: true,
})
}
store.minValue = data
}
catch (error) {
console.error('getList error:', error)
}
}
const sendFormCode = async (redirect?: boolean) => {
try {
await useMyFetch<GenericResponse<object>>(
`/api/public/user/session/confirm`,
{
method: 'POST',
body: JSON.stringify({
code: vCode.value,
mail: email.value,
}),
headers: {
'Content-Type': 'application/json',
},
onErrorOccured: (_, status) => {
throw new Error(`HTTP error: ${status}`)
},
},
)
await checkIsLogged()
if (isLogged.value) {
if (redirect) {
console.log(isLogged.value)
menuStore.navigateToItem()
}
else {
// window.location.href = atob(redirect);
}
}
else {
useNuxtApp().$toast.error(`Error occurred: Failed to confirm code`, {
autoClose: 5000,
dangerouslyHTMLString: true,
})
}
}
catch (e) {
console.error(e)
useNuxtApp().$toast.error(`Invalid code provided`, {
autoClose: 5000,
dangerouslyHTMLString: true,
})
}
}
return {
isLogged,
user,
fullUserData,
vCodeVerify,
vCode,
email,
password,
logIn,
checkIsLogged,
sendFormCode,
}
})

View File

@ -1,115 +0,0 @@
version: "3"
vars:
REGISTRY: registry.ma-al.com
Version: "0.0.6"
BuildDate: $(date +"%Y-%m-%d %H:%M")
Company: Maal sp. z o.o.
CompanyUrl: "https://www.ma-al.com"
CompileStr: go build -ldflags "-s -w -X 'pocketbase/custom/version.Version={{.Version}}' -X 'pocketbase/custom/version.BuildDate={{.BuildDate}}' -X 'pocketbase/custom/version.Company={{.Company}}' -X 'pocketbase/custom/version.CompanyUrl={{.CompanyUrl}}'" -o ../.pocketbase/pocketbase .
tasks:
default:
cmds:
- task --list
silent: true
compile_musl:
aliases: [cm]
desc: "compiles pocketbase for musl"
env:
CGO_ENABLED: "0"
GOOS: "linux"
GOARCH: "amd64"
CC: "x86_64-linux-musl-gcc"
cmds:
- |
mkdir -p ./.output
cd ./backend
{{.CompileStr}}
compile_gnu:
aliases: [cg]
desc: "compiles pocketbase for gnu"
env:
CGO_ENABLED: "0"
GOOS: "linux"
GOARCH: "amd64"
cmds:
- |
mkdir -p ./.output
cd ./backend
{{.CompileStr}}
build_run_gnu:
aliases: [br]
desc: "compiles pocketbase for gnu"
env:
CGO_ENABLED: "0"
GOOS: "linux"
GOARCH: "amd64"
cmds:
- |
mkdir -p ./.output
cd ./backend
go build -ldflags "-s -w" -o ../.pocketbase/pocketbase .
cd ..
./.pocketbase/pocketbase serve --dir=./backend/pb_data
watch_backend:
aliases: [wb]
desc: "watch backend and compile"
cmds:
- |
cd ./backend
pwd
air -build.args_bin='serve --dir=./pb_data' -build.exclude_dir=pb_data,backups -build.include_ext=go
watch_front:
aliases: [wf]
desc: "build and watch frontend in dev mode"
cmds:
- |
pnpm run dev
preview_front:
aliases: [pf]
desc: "build and preview frontend"
cmds:
- |
pnpm run build && pnpm run preview
rebuild_front:
aliases: [rf]
desc: "remove all and install all packages"
cmds:
- |
rm -rf ./node_modules ./pnpm-lock.yaml ./.nuxt ./.output
pnpm install
# build_docker_image:
# aliases: [bdi]
# desc: "build docker image"
# cmds:
# - |
# pnpm run build
# task compile_gnu
# cat <<EOF > temp.Dockerfile
# FROM node:slim
# COPY ./.output /nuxt
# COPY ./.pocketbase/pocketbase /bin/
# RUN mkdir /data
# # ENTRYPOINT ["ash"]
# CMD ["pocketbase", "serve", "--dir=/data", "--proxy=http://localhost:3000", "--subcommand=node /nuxt/server/index.mjs", "--http=0.0.0.0:8090"]
# EOF
# docker build -t {{.REGISTRY}}/abrasive/abrasive:{{.Version}} -t {{.REGISTRY}}/abrasive/abrasive:latest -f temp.Dockerfile .
# rm temp.Dockerfile
# push_docker_image:
# aliases: [pdi]
# desc: "push docker image to registry server"
# cmds:
# - |
# docker push {{.REGISTRY}}/abrasive/abrasive:{{.Version}}
# docker push {{.REGISTRY}}/abrasive/abrasive:latest

70
types/checkout.ts Normal file
View File

@ -0,0 +1,70 @@
export interface AddressesList {
address: {
city: string
country_iso: string
name: string
postcode: string
street: string
surname: string
}
address_id: number
alias: string
customer_id: number
is_default: boolean
is_official: boolean
}
export interface UserAddressOfficial {
address: {
name: string
surname: string
street: string
postcode: string
city: string
country: {
country_lang: [
{
name: string
},
]
}
}
}
export interface Payment {
bank_name: string
city: string
country_account_number: string
country_iso: string
country_name: string
currency_iso: string
iban: string
id: number
postcode: string
street_and_number: string
swift: string
}
export interface CheckoutOrder {
cart_id: number
currency_iso: string
customer_id: number
delivery_details: {
address: CheckoutAddress
contact_email: string
contact_phone_number: string
delivery_option_id: number
}
payment_bank_account_id: number
}
export interface CheckoutAddress {
city: string
country_iso: {
str: string
}
name: string
postcode: string
street: string
surname: string
}

26
types/footer.ts Normal file
View File

@ -0,0 +1,26 @@
export interface Footer {
data: {
contact: Contact
docs: FooterDoc[]
}
id: number
name: string
}
export interface Contact {
header: string
ownerAddress: string
ownerMail: string
}
export interface FooterDoc {
footer_section: string
translation: string
data: FooterPdf[]
}
export interface FooterPdf {
name: string
translation: string
guest_avaliable: boolean
}

25
types/frontMenu.ts Normal file
View File

@ -0,0 +1,25 @@
export interface FrontMenu {
id: number
id_parent: number
active: boolean
position_id: number
id_front_page: number
is_default: boolean
is_root: boolean
url: string
front_menu_lang: FrontMenuLang[]
}
export interface FrontMenuLang {
name: string
id_lang: number
id_menu: number
link_title: string
meta_title: string
meta_description: string
link_rewrite: string
}
export interface UIFrontMenu extends FrontMenu {
children?: UIFrontMenu[]
}

22
types/frontSection.ts Normal file
View File

@ -0,0 +1,22 @@
export interface FrontPageSection {
id_front_page: number
id_position: number
id_section: number
front_section: FrontSection
}
export interface FrontSection {
id: number
name: string
img: string[]
component_name: string
is_no_lang: boolean
page_name: string
front_section_lang: FrontSectionLang[]
}
export interface FrontSectionLang {
data: unknown
id_front_section: number
id_lang: number
}

View File

@ -1,159 +1,130 @@
export interface ListResponse {
page: number;
perPage: number;
totalItems: number;
totalPages: number;
}
export interface PBMenuItem {
page: number;
page_name: string;
perPage: number;
totalItems: number;
totalPages: number;
is_root: boolean;
is_default: boolean;
id_page: string;
id: string;
id_parent: string;
url: string;
name: string;
link_rewrite: string;
active: boolean;
collectionId: string;
collectionName: string;
created: string;
link_title: string;
meta_description: string;
meta_title: string;
position_id: number;
updated: string;
}
export interface PBFooterItem {
id: string;
id_lang: string;
address: string;
phone: string;
email: string;
contact_info: Array<{
field: keyof PBFooterItem;
title: string;
}>;
data: Array<{
title: string;
items: Array<string>
}>;
company_info: Array<{
data: string;
title: string;
}>;
}
export interface PBPageItem {
collectionId: string;
collectionName: string;
component_name: string;
id: string;
image_collection: string;
page_created: string;
page_id: string;
page_name: string;
page_section_id_position: number;
page_updated: string;
section_id: string;
section_img: string[];
section_lang_created: string;
section_lang_data: SectionLangData;
section_lang_id_lang: string;
section_name: string;
}
import type { DefineComponent } from "vue";
export interface SectionLangData {
title: string;
description: string;
button: string;
button_call: string;
}
export interface UIMenuItem extends PBMenuItem {
children?: UIMenuItem[];
}
export interface MenuListResponse extends ListResponse {
items: PBMenuItem[];
}
export interface FooterListResponse extends ListResponse {
items: PBFooterItem[];
}
import type { DefineComponent } from 'vue'
import type { FrontSection } from '~/types/frontSection'
export type componentsListType = {
name: string;
component: PBPageItem;
componentInstance: DefineComponent;
data: SectionLangData;
};
// menuStore
export type CountryList = {
call_prefix: string;
currency_iso_code: string;
iso_code: string;
name: string;
}
export type Countries = {
call_prefix: string;
currency_iso_code: string;
iso_code: string;
name: string;
name: string
component: FrontSection
componentInstance: DefineComponent
}
export type PartnersList = {
country_iso: string;
country_name: string;
total: number;
}
export type Currencies = {
iso_code: string;
name: string;
UpdatedAt: string;
iso_code_num: number;
precision: number;
sign: string;
active: boolean;
suffix: boolean;
country_iso: string
country_name: string
total: number
}
export type FeatureValue = {
parent: number,
products_with_value: number,
value: string,
value_id: number,
parent: number
products_with_value: number
value: string
value_id: number
}
export type Feature = {
feature: string,
feature_id: number,
feature_values: FeatureValue[],
products_with_feature: number,
feature: string
feature_id: number
feature_values: FeatureValue[]
products_with_feature: number
}
export type ProductType = {
applied_tax_rate: number,
cover_picture_uuid: string,
description: string,
formatted_price: string,
id: number,
in_stock: number,
is_sale_active: boolean,
link_rewrite: string,
name: string,
price: number,
tax_name: string,
applied_tax_rate: number
cover_picture_uuid: string
description: string
formatted_price: string
id: number
in_stock: number
is_sale_active: boolean
link_rewrite: string
name: string
price: number
tax_name: string
cart_item_id?: number
product_id?: number
}
export interface Country {
iso_code: string
currency_iso_code: string
call_prefix: string
need_postcode: boolean
postcode_format: string
is_default: boolean
active: boolean
name: string
}
export interface Currency {
iso_code: string
name: string
UpdatedAt: string
iso_code_num: number
precision: number
sign: string
active: boolean
suffix: boolean
}
export interface Language {
id: number
name: string
iso_code: string
lang_code: string
date_format: string
date_format_short: string
rtl: boolean
is_default: boolean
active: boolean
}
export interface CookieData {
country: Country
currency: Currency
language: Language
}
export interface CartItem {
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 GenericResponse<Data> {
data: Data
message?: string
status: number
}
export interface GenericResponseItems<Data> {
data: { items: Data, items_count: number }
message?: string
status: number
}
export interface GenericResponseChildren<Data> {
data: { children: Data, items_count: number }
message?: string
status: number
}
export type {
InvestmentPiece,
PlanPrediction,
PeriodToFirstPiece,
} from './planPrediction'
export type {
Product,
ProductItem,
CategoryProductItem,
FeatureProductItem,
RecommendationProductItem,
CartProduct,
UserCart,
} from './product'
export type { FrontMenu, FrontMenuLang, UIFrontMenu } from './frontMenu'
export type { Contact, Footer, FooterDoc, FooterPdf } from './footer'

Some files were not shown because too many files have changed in this diff Show More