Compare commits
38 Commits
837eea76a5
...
main
Author | SHA1 | Date | |
---|---|---|---|
8a4d835c33 | |||
3b02ccbe80 | |||
9063846282 | |||
e935e9f911 | |||
5d59059474 | |||
90e1d70f64 | |||
fbb6c071af | |||
687e92429f | |||
29f8582e47 | |||
ea72074559 | |||
fd4b122936 | |||
012058b998 | |||
96dbc38c3a | |||
8bab93274b | |||
10b9610918 | |||
edf3036e6a | |||
9d7fd3d52a | |||
4fc12ff9bf | |||
9407253e69 | |||
98a4125804 | |||
5de09aa13b | |||
7cc292296b | |||
b867030c8d | |||
a000f966eb | |||
26e7467f7f | |||
7d0a449a1e | |||
77a490a94d | |||
35575eda6f | |||
c7479b1aa6 | |||
92511dbfb5 | |||
261fdd16ce | |||
902e2db011 | |||
3d91d7f1bb | |||
c7d71ddb21 | |||
a7c4ff51ca | |||
c9348dc092 | |||
2ffd64da98 | |||
9246f80857 |
13
.husky/hooks/pre-commit
Executable 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
|
||||||
|
|
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"plugins": ["prettier-plugin-tailwindcss"]
|
|
||||||
}
|
|
7
app.vue
@ -9,4 +9,9 @@
|
|||||||
</UApp>
|
</UApp>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts"></script>
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
await userStore.checkIsLogged()
|
||||||
|
</script>
|
||||||
|
@ -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;
|
|
||||||
}
|
|
@ -1,11 +1,11 @@
|
|||||||
@import "@nuxt/ui";
|
@import '@nuxt/ui';
|
||||||
@import "tailwindcss";
|
@import 'tailwindcss';
|
||||||
@import '@iconscout/unicons/css/line.css';
|
@import '@iconscout/unicons/css/line.css';
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
/* fonts */
|
/* fonts */
|
||||||
--font-inter: "Inter", sans-serif;
|
--font-inter: 'Inter', sans-serif;
|
||||||
--font-bounded: "Bounded", sans-serif;
|
--font-bounded: 'Bounded', sans-serif;
|
||||||
|
|
||||||
/* colors */
|
/* colors */
|
||||||
--color-bg-light: #fffefb;
|
--color-bg-light: #fffefb;
|
||||||
@ -16,6 +16,12 @@
|
|||||||
--color-text-light: #1a1a1a;
|
--color-text-light: #1a1a1a;
|
||||||
--color-text-dark: #fffefb;
|
--color-text-dark: #fffefb;
|
||||||
|
|
||||||
|
--color-block: #e8e7e0;
|
||||||
|
|
||||||
|
--color-accent-green-light: #004f3d;
|
||||||
|
--color-accent-green-dark: #008567;
|
||||||
|
|
||||||
|
--color-gray: #6b6b6b;
|
||||||
/* button */
|
/* button */
|
||||||
--color-button: #9a7f62;
|
--color-button: #9a7f62;
|
||||||
--color-button-hover: #b7946d;
|
--color-button-hover: #b7946d;
|
||||||
@ -27,14 +33,41 @@
|
|||||||
@apply font-bounded text-[24px] leading-[140%] font-bold uppercase md:text-[48px] xl:text-[64px];
|
@apply font-bounded text-[24px] leading-[140%] font-bold uppercase md:text-[48px] xl:text-[64px];
|
||||||
}
|
}
|
||||||
|
|
||||||
.h4-uppercase {
|
.h1-big {
|
||||||
|
@apply font-bounded text-[32px] leading-[140%] font-bold uppercase sm:text-[48px] xl:text-[64px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.h2-bold-bounded {
|
||||||
|
@apply font-bounded text-[24px] leading-[150%] md:leading-[120%] font-bold sm:text-[36px] md:text-[40px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.h4-uppercase-bold-inter {
|
||||||
@apply font-inter text-base leading-[150%] font-bold uppercase sm:text-[20px] md:text-[24px];
|
@apply font-inter text-base leading-[150%] font-bold uppercase sm:text-[20px] md:text-[24px];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.text-inter {
|
.text-bold-24 {
|
||||||
font-family: var(--font-inter);
|
@apply font-inter text-[17px] sm:text-[21px] md:text-2xl leading-[150%] font-bold;
|
||||||
line-height: 150%;
|
}
|
||||||
|
|
||||||
|
.text-inter {
|
||||||
|
@apply font-inter text-sm sm:text-lg leading-[150%];
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-25-55-75 {
|
||||||
|
@apply space-y-[25px] sm:space-y-[55px] md:space-y-[75px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-25-55 {
|
||||||
|
@apply space-y-[25px] sm:space-y-[55px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-25-75 {
|
||||||
|
@apply space-y-[25px] sm:space-y-[75px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-55-75 {
|
||||||
|
@apply space-y-[55px] sm:space-y-[75px];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
|
51
assets/toastify-custom.css
Normal 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;
|
||||||
|
}
|
@ -1,82 +0,0 @@
|
|||||||
export class TurnStilleCaptcha {
|
|
||||||
sitekey = "{{.SiteKey}}";
|
|
||||||
/**
|
|
||||||
* Initializes the TurnStilleCaptcha instance.
|
|
||||||
* Creates a container for the captcha and appends it to the target element.
|
|
||||||
* If the Cloudflare Turnstile script is already loaded, it runs the captcha.
|
|
||||||
* Otherwise, it loads the script and initializes the captcha with the given properties.
|
|
||||||
* @param {HTMLElement} target - The element to attach the captcha container to.
|
|
||||||
* @param {Object} [props={}] - Optional properties for captcha initialization, such as theme.
|
|
||||||
*/
|
|
||||||
|
|
||||||
constructor(target, props = {}) {
|
|
||||||
// create holder
|
|
||||||
this.holder = document.createElement("div");
|
|
||||||
this.holder.id = "turnstile-container";
|
|
||||||
this.theme = props.theme || "auto";
|
|
||||||
target.appendChild(this.holder);
|
|
||||||
|
|
||||||
|
|
||||||
// execute code
|
|
||||||
if (window.turnstile) {
|
|
||||||
this.runCaptcha();
|
|
||||||
} else {
|
|
||||||
this.loadCloudflareScript();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
runCaptcha() {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (globalThis.turnstileInstance) {
|
|
||||||
window.turnstile.remove(globalThis.turnstileInstance);
|
|
||||||
}
|
|
||||||
globalThis.turnstileInstance = window.turnstile.render(this.holder, {
|
|
||||||
sitekey: this.sitekey,
|
|
||||||
theme: this.theme,
|
|
||||||
callback: (token) => {
|
|
||||||
if (token) {
|
|
||||||
const event = new CustomEvent("token", {
|
|
||||||
detail: token,
|
|
||||||
bubbles: true,
|
|
||||||
});
|
|
||||||
this.holder.dispatchEvent(event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: (error) => {
|
|
||||||
const event = new CustomEvent("failure", {
|
|
||||||
detail: error,
|
|
||||||
bubbles: true,
|
|
||||||
});
|
|
||||||
this.holder.dispatchEvent(event);
|
|
||||||
window.turnstile.reset(globalThis.turnstileInstance);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadCloudflareScript() {
|
|
||||||
const script = document.createElement("script");
|
|
||||||
script.id = "turnstile-script";
|
|
||||||
script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js";
|
|
||||||
// script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
|
|
||||||
script.async = true;
|
|
||||||
script.defer = true;
|
|
||||||
|
|
||||||
script.onload = () => {
|
|
||||||
const event = new CustomEvent("loaded", {
|
|
||||||
detail: "Turnstile script loaded",
|
|
||||||
bubbles: true,
|
|
||||||
});
|
|
||||||
this.holder.dispatchEvent(event);
|
|
||||||
|
|
||||||
this.runCaptcha();
|
|
||||||
};
|
|
||||||
script.onerror = () => {
|
|
||||||
const event = new CustomEvent("failure", {
|
|
||||||
detail: "Failed to load Turnstile script",
|
|
||||||
bubbles: true,
|
|
||||||
});
|
|
||||||
this.holder.dispatchEvent(event);
|
|
||||||
};
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
package cloudflare
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"html/template"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
var VerifyUrl = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
|
|
||||||
|
|
||||||
type TurnstileResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
ErrorCodes []string `json:"error-codes,omitempty"`
|
|
||||||
ChallengeTS string `json:"challenge_ts,omitempty"`
|
|
||||||
Hostname string `json:"hostname,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TurnStilleCaptcha struct {
|
|
||||||
SiteKey string
|
|
||||||
SecretKey string
|
|
||||||
}
|
|
||||||
|
|
||||||
func ServeTurnstilleCaptchaJS(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] {
|
|
||||||
return se.Router.GET("/api/email/script.js", func(e *core.RequestEvent) error {
|
|
||||||
// www.abrasive.ma-al.pl
|
|
||||||
// siteKey: "0x4AAAAAABdgeAdu4Pxxovj3"
|
|
||||||
// secretKey: "0x4AAAAAABdgeHJDjMwmeX5aXaXGh6HWZbw"
|
|
||||||
|
|
||||||
settings, err := GetSettings(app)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := JS.ReadFile("TurnStilleCaptcha.js")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
templ, err := template.New("test").Parse(string(file))
|
|
||||||
|
|
||||||
buf := bytes.Buffer{}
|
|
||||||
templ.Execute(&buf, map[string]interface{}{
|
|
||||||
"SiteKey": settings.SiteKey,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Response.Header().Set("Content-Type", "application/javascript")
|
|
||||||
return e.String(http.StatusOK, buf.String())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetSettings(app *pocketbase.PocketBase) (*TurnStilleCaptcha, error) {
|
|
||||||
record, err := app.FindFirstRecordByFilter("settings", "key='turnstile'", nil)
|
|
||||||
|
|
||||||
settings := TurnStilleCaptcha{}
|
|
||||||
json.Unmarshal([]byte(record.GetString("value")), &settings)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &settings, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func VerifyTurnstile(app *pocketbase.PocketBase, token, ip string) error {
|
|
||||||
conf, err := GetSettings(app)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
data := map[string]string{"secret": conf.SecretKey, "response": token, "remoteip": ip}
|
|
||||||
jsonData, _ := json.Marshal(data)
|
|
||||||
resp, err := http.Post(VerifyUrl, "application/json", bytes.NewBuffer(jsonData))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
var turnstileResp TurnstileResponse
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
json.Unmarshal(body, &turnstileResp)
|
|
||||||
|
|
||||||
if !turnstileResp.Success {
|
|
||||||
return errors.New(turnstileResp.ChallengeTS + ": " + strings.Join(turnstileResp.ErrorCodes, " "))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
package cloudflare
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed TurnStilleCaptcha.js
|
|
||||||
var JS embed.FS
|
|
@ -1,61 +0,0 @@
|
|||||||
package custom
|
|
||||||
|
|
||||||
import (
|
|
||||||
"pocketbase/custom/cloudflare"
|
|
||||||
"pocketbase/custom/gtm"
|
|
||||||
"pocketbase/custom/mail"
|
|
||||||
"pocketbase/custom/manifest"
|
|
||||||
"pocketbase/custom/proxy"
|
|
||||||
"pocketbase/custom/seo"
|
|
||||||
"pocketbase/custom/supervise"
|
|
||||||
"pocketbase/custom/version"
|
|
||||||
webpconverter "pocketbase/custom/webpConverter"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
)
|
|
||||||
|
|
||||||
func LoadCustomCode(app *pocketbase.PocketBase, se *core.ServeEvent) error {
|
|
||||||
|
|
||||||
// include supervised subprocess if command provided
|
|
||||||
supervise.ServeSubprocessSupervisor(app, se)
|
|
||||||
|
|
||||||
// include serving js code from cloudflare
|
|
||||||
cloudflare.ServeTurnstilleCaptchaJS(app, se)
|
|
||||||
|
|
||||||
// include sending emails service
|
|
||||||
mail.ServeMailSender(app, se)
|
|
||||||
|
|
||||||
// include robots.txt endpoint
|
|
||||||
seo.ServeRobotsTxt(app, se)
|
|
||||||
|
|
||||||
// include feeds endpoint
|
|
||||||
seo.ServeFeeds(app, se)
|
|
||||||
seo.ServeFeedsIndex(app, se)
|
|
||||||
|
|
||||||
// include proxy to serve nuxt app
|
|
||||||
proxy.ServeProxyPassingToNuxt(app, se)
|
|
||||||
|
|
||||||
// create endpoint to serve version information
|
|
||||||
version.ServeVersionInfo(app, se)
|
|
||||||
|
|
||||||
// include endpoint to server GTM script
|
|
||||||
gtm.ServeTagMangerJS(app, se)
|
|
||||||
|
|
||||||
// include endpoint serving manifest
|
|
||||||
manifest.ServeManifst(app, se)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExtendApp(app *pocketbase.PocketBase) *pocketbase.PocketBase {
|
|
||||||
app.RootCmd.PersistentFlags().String("proxy", "", "inner proxy target")
|
|
||||||
app.RootCmd.PersistentFlags().String("subcommand", "", "provide command with params like cli that will be executed and supervised by main process")
|
|
||||||
|
|
||||||
// include webp converter
|
|
||||||
app.OnRecordCreate().Bind(webpconverter.CreateEventHandler(app))
|
|
||||||
app.OnRecordUpdate().Bind(webpconverter.CreateEventHandler(app))
|
|
||||||
app.OnFileDownloadRequest().Bind(webpconverter.ThumbEventHandler(app))
|
|
||||||
|
|
||||||
return app
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
package gtm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ServeTagMangerJS(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] {
|
|
||||||
return se.Router.GET("/api/gtm/script.js", func(e *core.RequestEvent) error {
|
|
||||||
|
|
||||||
settings, err := GetSettings(app)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := JS.ReadFile("gtm.js")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
templ, err := template.New("test").Parse(string(file))
|
|
||||||
|
|
||||||
buf := bytes.Buffer{}
|
|
||||||
templ.Execute(&buf, map[string]interface{}{
|
|
||||||
"GtagID": settings.GtagID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Response.Header().Set("Content-Type", "application/javascript")
|
|
||||||
return e.String(http.StatusOK, buf.String())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type GTagSettings struct {
|
|
||||||
GtagID string `json:"gtagID"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetSettings(app *pocketbase.PocketBase) (*GTagSettings, error) {
|
|
||||||
record, err := app.FindFirstRecordByFilter("settings", "key='gtagID'", nil)
|
|
||||||
|
|
||||||
settings := GTagSettings{}
|
|
||||||
json.Unmarshal([]byte(record.GetString("value")), &settings)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &settings, nil
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
export class GTM {
|
|
||||||
gtagID = "{{.GtagID}}";
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.insertScript(window, document, 'script', 'dataLayer', this.gtagID);
|
|
||||||
this.insertNoScript();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
insertScript(w, d, s, l, i) {
|
|
||||||
w[l] = w[l] || []; w[l].push({
|
|
||||||
'gtm.start':
|
|
||||||
new Date().getTime(), event: 'gtm.js'
|
|
||||||
}); var f = d.getElementsByTagName(s)[0],
|
|
||||||
j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =
|
|
||||||
'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);
|
|
||||||
}
|
|
||||||
|
|
||||||
insertNoScript() {
|
|
||||||
const noscript = document.createElement("noscript");
|
|
||||||
const iframe = document.createElement("iframe");
|
|
||||||
iframe.src = "https://www.googletagmanager.com/ns.html?id=" + this.gtagID;
|
|
||||||
iframe.height = 0;
|
|
||||||
iframe.width = 0;
|
|
||||||
iframe.style = "display:none;visibility:hidden";
|
|
||||||
noscript.appendChild(iframe);
|
|
||||||
document.body.appendChild(noscript);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
package gtm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed gtm.js
|
|
||||||
var JS embed.FS
|
|
@ -1,175 +0,0 @@
|
|||||||
package mail
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/mail"
|
|
||||||
"pocketbase/custom/cloudflare"
|
|
||||||
"strings"
|
|
||||||
"text/template"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/mailer"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EmailData struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Phone string `json:"phone"`
|
|
||||||
LangIso string `json:"lang_iso"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MailsSettings struct {
|
|
||||||
ReceiverMail string `json:"receiver_mail"`
|
|
||||||
ReceiverName string `json:"receiver_name"`
|
|
||||||
Subject string `json:"subject"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ServeMailSender(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] {
|
|
||||||
return se.Router.POST("/api/email/send", func(e *core.RequestEvent) error {
|
|
||||||
// name := e.Request.PathValue("name")
|
|
||||||
|
|
||||||
data := EmailData{}
|
|
||||||
if err := e.BindBody(&data); err != nil {
|
|
||||||
e.Response.Header().Set("Content-Type", "application/json")
|
|
||||||
app.Logger().Error(err.Error(), "type", "mail")
|
|
||||||
return e.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
if pass, err := validateMX(data.Email); !pass && err != nil {
|
|
||||||
e.Response.Header().Set("Content-Type", "application/json")
|
|
||||||
app.Logger().Error("Invalid email address.", "type", "mail", "error", err.Error())
|
|
||||||
return e.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid email address."})
|
|
||||||
}
|
|
||||||
|
|
||||||
// fmt.Printf("e.Request.Body: %v IP: %s\n", data, e.RealIP())
|
|
||||||
|
|
||||||
err := cloudflare.VerifyTurnstile(app, data.Token, e.RealIP())
|
|
||||||
if err != nil {
|
|
||||||
e.Response.Header().Set("Content-Type", "application/json")
|
|
||||||
app.Logger().Error("Captcha verification failed.", "type", "mail", "error", err.Error())
|
|
||||||
return e.JSON(http.StatusBadRequest, map[string]string{"error": "Captcha verification failed."})
|
|
||||||
}
|
|
||||||
|
|
||||||
eamil_body, err := app.FindFirstRecordByFilter("email_template", fmt.Sprintf("name='contact_form'&&id_lang='%s'", data.LangIso), nil)
|
|
||||||
if err != nil {
|
|
||||||
e.Response.Header().Set("Content-Type", "application/json")
|
|
||||||
app.Logger().Error("Template not available", "type", "mail", "error", err.Error())
|
|
||||||
return e.JSON(http.StatusBadRequest, map[string]string{"error": "Template not available"})
|
|
||||||
}
|
|
||||||
templ, err := template.New("test").Parse(eamil_body.GetString("template"))
|
|
||||||
if err != nil {
|
|
||||||
app.Logger().Error("Template parsing error.", "type", "mail", "error", err.Error())
|
|
||||||
e.Response.Header().Set("Content-Type", "application/json")
|
|
||||||
return e.JSON(http.StatusBadRequest, map[string]string{"error": "Template parsing error."})
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := bytes.Buffer{}
|
|
||||||
templ.Execute(&buf, map[string]interface{}{
|
|
||||||
"Data": time.Now().Local().Format("2006-01-02 15:04:05"),
|
|
||||||
"Name": data.Name,
|
|
||||||
"Email": data.Email,
|
|
||||||
"Message": strings.ReplaceAll(data.Message, "\n", "<br>"),
|
|
||||||
"Phone": data.Phone,
|
|
||||||
})
|
|
||||||
|
|
||||||
mailsSettings, err := getSettings(app)
|
|
||||||
if err != nil {
|
|
||||||
app.Logger().Error("Mails settings corrupted", "type", "mail", "error", err.Error())
|
|
||||||
e.Response.Header().Set("Content-Type", "application/json")
|
|
||||||
return e.JSON(http.StatusBadRequest, map[string]string{"error": "Mails settings corrupted"})
|
|
||||||
}
|
|
||||||
|
|
||||||
cc := []mail.Address{{Address: data.Email, Name: data.Name}}
|
|
||||||
to := []mail.Address{{Address: mailsSettings.ReceiverMail, Name: mailsSettings.ReceiverName}}
|
|
||||||
|
|
||||||
message := &mailer.Message{
|
|
||||||
From: mail.Address{
|
|
||||||
Address: e.App.Settings().Meta.SenderAddress,
|
|
||||||
Name: e.App.Settings().Meta.SenderName,
|
|
||||||
},
|
|
||||||
Cc: cc,
|
|
||||||
To: to,
|
|
||||||
Subject: mailsSettings.Subject,
|
|
||||||
HTML: buf.String(),
|
|
||||||
}
|
|
||||||
|
|
||||||
err = e.App.NewMailClient().Send(message)
|
|
||||||
if err != nil {
|
|
||||||
app.Logger().Error("Mails sending error", "type", "mail", "error", err.Error())
|
|
||||||
e.Response.Header().Set("Content-Type", "application/json")
|
|
||||||
return e.JSON(http.StatusBadRequest, map[string]string{"error": "Mails sending error"})
|
|
||||||
}
|
|
||||||
|
|
||||||
receivers := formReceiversList(to, cc, []mail.Address{})
|
|
||||||
app.Logger().Info("Mail Sent", "type", "mail", "receivers", receivers)
|
|
||||||
return e.JSON(http.StatusOK, map[string]string{"status": "success", "message": "Email sent successfully. to " + receivers})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func formReceiversList(to []mail.Address, cc []mail.Address, bcc []mail.Address) string {
|
|
||||||
|
|
||||||
res := ""
|
|
||||||
|
|
||||||
for _, a := range to {
|
|
||||||
res = fmt.Sprintf("%s %s<%s>", res, a.Name, a.Address)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, a := range cc {
|
|
||||||
res = fmt.Sprintf("%s %s<%s>", res, a.Name, a.Address)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, a := range bcc {
|
|
||||||
res = fmt.Sprintf("%s %s<%s>", res, a.Name, a.Address)
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
func getSettings(app *pocketbase.PocketBase) (*MailsSettings, error) {
|
|
||||||
record, err := app.FindFirstRecordByFilter("settings", "key='contact_page'", nil)
|
|
||||||
|
|
||||||
settings := MailsSettings{}
|
|
||||||
json.Unmarshal([]byte(record.GetString("value")), &settings)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &settings, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractDomain(email string) string {
|
|
||||||
parts := strings.Split(email, "@")
|
|
||||||
if len(parts) != 2 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(parts[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateMX checks if the domain has valid MX records or A records as a fallback
|
|
||||||
func validateMX(email string) (bool, error) {
|
|
||||||
// Check MX records
|
|
||||||
|
|
||||||
domain := extractDomain(email)
|
|
||||||
mxRecords, err := net.LookupMX(domain)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("'MX' records for %s not found", domain)
|
|
||||||
} else if len(mxRecords) > 0 {
|
|
||||||
// At least one MX record exists
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: Check for A records (some domains accept mail via A records)
|
|
||||||
aRecords, err := net.LookupHost(domain)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("'A' record for %s not found", domain)
|
|
||||||
}
|
|
||||||
return len(aRecords) > 0, nil
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
package manifest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Manifest struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
ShortName string `json:"short_name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Icons []Icon `json:"icons"`
|
|
||||||
StartURL string `json:"start_url"`
|
|
||||||
Display string `json:"display"`
|
|
||||||
BackgroundColor string `json:"background_color"`
|
|
||||||
ThemeColor string `json:"theme_color"`
|
|
||||||
Lang string `json:"lang"`
|
|
||||||
Author string `json:"author"`
|
|
||||||
OgHost string `json:"ogHost"`
|
|
||||||
Orientation string `json:"orientation"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Icon struct {
|
|
||||||
Src string `json:"src"`
|
|
||||||
Sizes string `json:"sizes"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ServeManifst(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] {
|
|
||||||
return se.Router.GET("/api/manifest.json", func(e *core.RequestEvent) error {
|
|
||||||
manifest, err := GetSettings(app)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
e.Response.Header().Add("content-type", "application/json")
|
|
||||||
return e.String(http.StatusOK, manifest)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetSettings(app *pocketbase.PocketBase) (string, error) {
|
|
||||||
record, err := app.FindFirstRecordByFilter("settings", "key='manifest'", nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return record.GetString("value"), nil
|
|
||||||
}
|
|
@ -1,121 +0,0 @@
|
|||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httputil"
|
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Logger *slog.Logger
|
|
||||||
|
|
||||||
func ServeProxyPassingToNuxt(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] {
|
|
||||||
Logger = app.Logger()
|
|
||||||
|
|
||||||
proxyUrl, _ := app.RootCmd.Flags().GetString("proxy")
|
|
||||||
|
|
||||||
if len(proxyUrl) > 0 {
|
|
||||||
target, _ := url.Parse(proxyUrl) // Node.js app
|
|
||||||
|
|
||||||
proxy := httputil.NewSingleHostReverseProxy(target)
|
|
||||||
|
|
||||||
originalDirector := proxy.Director
|
|
||||||
proxy.Director = func(req *http.Request) {
|
|
||||||
originalDirector(req)
|
|
||||||
req.Host = target.Host
|
|
||||||
}
|
|
||||||
proxy.Transport = &loggingTransport{http.DefaultTransport}
|
|
||||||
|
|
||||||
proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, err error) {
|
|
||||||
if errors.Is(err, context.Canceled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
app.Logger().Error(fmt.Sprintf("Proxy error: %v for %s %s", err, req.Method, req.URL.Path), "type", "proxy")
|
|
||||||
http.Error(rw, "Proxy error", http.StatusBadGateway)
|
|
||||||
}
|
|
||||||
|
|
||||||
return se.Router.Any("/", func(e *core.RequestEvent) error {
|
|
||||||
// Ping the backend with a HEAD request (or TCP dial)
|
|
||||||
backendUp := isBackendAlive(target)
|
|
||||||
if !backendUp {
|
|
||||||
app.Logger().Error(fmt.Sprintf("Backend %s is unreachable, sending 502", target), "type", "proxy", "path", e.Request.URL.Path, "remoteIP", e.Request.RemoteAddr)
|
|
||||||
e.Response.WriteHeader(http.StatusBadGateway)
|
|
||||||
e.Response.Write([]byte("502 Backend is unavailable"))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forward to Node.js
|
|
||||||
proxy.ServeHTTP(e.Response, e.Request)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isBackendAlive(target *url.URL) bool {
|
|
||||||
conn, err := net.DialTimeout("tcp", target.Host, 500*time.Millisecond)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
conn.Close()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
type loggingTransport struct {
|
|
||||||
rt http.RoundTripper
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *loggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
||||||
start := time.Now()
|
|
||||||
|
|
||||||
// Do the actual request
|
|
||||||
resp, err := t.rt.RoundTrip(req)
|
|
||||||
duration := time.Since(start)
|
|
||||||
|
|
||||||
// Prepare fields
|
|
||||||
remoteAddr := req.RemoteAddr
|
|
||||||
if ip := req.Header.Get("X-Real-IP"); len(ip) > 0 {
|
|
||||||
remoteAddr = ip
|
|
||||||
}
|
|
||||||
method := req.Method
|
|
||||||
uri := req.URL.RequestURI()
|
|
||||||
// proto := req.Proto
|
|
||||||
status := 0
|
|
||||||
size := 0
|
|
||||||
|
|
||||||
if resp != nil {
|
|
||||||
status = resp.StatusCode
|
|
||||||
if resp.ContentLength > 0 {
|
|
||||||
size = int(resp.ContentLength)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
referer := req.Referer()
|
|
||||||
ua := req.UserAgent()
|
|
||||||
// timestamp := time.Now().Format("02/Jan/2006:15:04:05 -0700")
|
|
||||||
|
|
||||||
Logger.Info(
|
|
||||||
fmt.Sprintf("%s %s", method, uri),
|
|
||||||
"type", "proxy",
|
|
||||||
// "method", method,
|
|
||||||
// "url", uri,
|
|
||||||
"referer", referer,
|
|
||||||
"remoteIP", remoteAddr,
|
|
||||||
"userAgent", ua,
|
|
||||||
"execTime", fmt.Sprintf("%.4fms", float64(duration.Milliseconds())),
|
|
||||||
"status", status,
|
|
||||||
"size", size,
|
|
||||||
)
|
|
||||||
|
|
||||||
return resp, err
|
|
||||||
}
|
|
@ -1,194 +0,0 @@
|
|||||||
package seo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"path"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MenuRecord struct {
|
|
||||||
Active bool `json:"active"`
|
|
||||||
CollectionID string `json:"collectionId"`
|
|
||||||
CollectionName string `json:"collectionName"`
|
|
||||||
Created string `json:"created"`
|
|
||||||
ID string `json:"id"`
|
|
||||||
IDLang string `json:"id_lang"`
|
|
||||||
IDPage string `json:"id_page"`
|
|
||||||
IDParent string `json:"id_parent"`
|
|
||||||
IsDefault bool `json:"is_default"`
|
|
||||||
IsRoot bool `json:"is_root"`
|
|
||||||
LinkRewrite string `json:"link_rewrite"`
|
|
||||||
LinkTitle string `json:"link_title"`
|
|
||||||
MetaDescription string `json:"meta_description"`
|
|
||||||
MetaTitle string `json:"meta_title"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
PageName string `json:"page_name"`
|
|
||||||
PositionID int `json:"position_id"`
|
|
||||||
Updated string `json:"updated"`
|
|
||||||
Url string `json:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UrlSet struct {
|
|
||||||
XMLName xml.Name `xml:"urlset"`
|
|
||||||
Xmlns string `xml:"xmlns,attr"`
|
|
||||||
Urls []Url `xml:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Url struct {
|
|
||||||
Loc string `xml:"loc"`
|
|
||||||
LastMod string `xml:"lastmod,omitempty"`
|
|
||||||
ChangeFreq string `xml:"changefreq,omitempty"`
|
|
||||||
Priority string `xml:"priority,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ServeFeeds(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] {
|
|
||||||
return se.Router.GET("/feeds/{lang}/sitemap.xml", func(e *core.RequestEvent) error {
|
|
||||||
|
|
||||||
lang := e.Request.PathValue("lang")
|
|
||||||
|
|
||||||
urls, err := getLocations(app, lang)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
xx := UrlSet{
|
|
||||||
Xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9",
|
|
||||||
Urls: urls,
|
|
||||||
}
|
|
||||||
|
|
||||||
bytes, err := xml.MarshalIndent(xx, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Response.Header().Add("content-type", "text/xml")
|
|
||||||
return e.String(http.StatusOK, xml.Header+string(bytes))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLocations(app *pocketbase.PocketBase, lang string) ([]Url, error) {
|
|
||||||
records, err := app.FindRecordsByFilter("menu_view", fmt.Sprintf("id_lang='%s'&&active=true", lang), "position_id", 200, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
baseUrl, err := getBaseUrl(app)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
locations := []Url{}
|
|
||||||
lastMod := time.Now().Add(time.Hour * 24 * 7 * -1).Format("2006-01-02")
|
|
||||||
|
|
||||||
for _, r := range records {
|
|
||||||
rec := MenuRecord{}
|
|
||||||
x, _ := r.MarshalJSON()
|
|
||||||
json.Unmarshal(x, &rec)
|
|
||||||
if rec.IsRoot {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(rec.Url) > 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if rec.IsDefault {
|
|
||||||
|
|
||||||
locations = append(locations, Url{
|
|
||||||
Loc: baseUrl + path.Join(lang),
|
|
||||||
LastMod: lastMod,
|
|
||||||
ChangeFreq: "weekly",
|
|
||||||
Priority: "1.0",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
locations = append(locations, Url{
|
|
||||||
Loc: baseUrl + path.Join(lang, rec.IDPage, rec.LinkRewrite),
|
|
||||||
LastMod: lastMod,
|
|
||||||
ChangeFreq: "weekly",
|
|
||||||
Priority: "1.0",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return locations, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type BaseUrl struct {
|
|
||||||
BaseURL string `json:"baseUrl"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBaseUrl(app *pocketbase.PocketBase) (string, error) {
|
|
||||||
record, err := app.FindFirstRecordByFilter("settings", "key='baseUrl'", nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
settings := BaseUrl{}
|
|
||||||
json.Unmarshal([]byte(record.GetString("value")), &settings)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings.BaseURL, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type SitemapIndex struct {
|
|
||||||
XMLName xml.Name `xml:"sitemapindex"`
|
|
||||||
Xmlns string `xml:"xmlns,attr"`
|
|
||||||
Sitemaps []Sitemap `xml:"sitemap"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sitemap represents each <sitemap> entry
|
|
||||||
type Sitemap struct {
|
|
||||||
Loc string `xml:"loc"`
|
|
||||||
LastMod string `xml:"lastmod"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ServeFeedsIndex(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] {
|
|
||||||
return se.Router.GET("/feeds/index.xml", func(e *core.RequestEvent) error {
|
|
||||||
index, err := makeSiteMapIndex(app)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
bytes, err := xml.MarshalIndent(index, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Response.Header().Add("content-type", "text/xml")
|
|
||||||
return e.String(http.StatusOK, xml.Header+string(bytes))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeSiteMapIndex(app *pocketbase.PocketBase) (*SitemapIndex, error) {
|
|
||||||
index := &SitemapIndex{
|
|
||||||
Xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9",
|
|
||||||
}
|
|
||||||
|
|
||||||
bseUrl, err := getBaseUrl(app)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
langs, err := app.FindRecordsByFilter("lang", "active=true", "", 200, 0)
|
|
||||||
if err != nil {
|
|
||||||
return index, err
|
|
||||||
}
|
|
||||||
|
|
||||||
lastMod := time.Now().Add(time.Hour * 24 * 7 * -1).Format("2006-01-02")
|
|
||||||
|
|
||||||
for _, l := range langs {
|
|
||||||
index.Sitemaps = append(index.Sitemaps, Sitemap{
|
|
||||||
Loc: bseUrl + path.Join("feeds/", l.Id, "sitemap.xml"),
|
|
||||||
LastMod: lastMod,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return index, nil
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
package seo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Robots struct {
|
|
||||||
Robots []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func ServeRobotsTxt(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] {
|
|
||||||
return se.Router.GET("/robots.txt", func(e *core.RequestEvent) error {
|
|
||||||
|
|
||||||
text, err := getRobots(app)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return e.String(http.StatusOK, text)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRobots(app *pocketbase.PocketBase) (string, error) {
|
|
||||||
record, err := app.FindFirstRecordByFilter("settings", "key='robots_txt'", nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
settings := Robots{}
|
|
||||||
json.Unmarshal([]byte(record.GetString("value")), &settings)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
baseUrl, err := getBaseUrl(app)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
settings.Robots = append(settings.Robots, fmt.Sprintf("\n\nSitemap: %s%s", baseUrl, "feeds/index.xml"))
|
|
||||||
|
|
||||||
return strings.Join(settings.Robots, "\n"), nil
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
package supervise
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ServeSubprocessSupervisor(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] {
|
|
||||||
|
|
||||||
command, _ := app.RootCmd.PersistentFlags().GetString("subcommand")
|
|
||||||
|
|
||||||
if len(command) > 0 {
|
|
||||||
cmdsub := strings.Split(command, " ")
|
|
||||||
if len(cmdsub) > 0 {
|
|
||||||
startNodeProcessSupervised(cmdsub[0], strings.Join(cmdsub[1:], " "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func startNodeProcessSupervised(command string, args ...string) {
|
|
||||||
const maxRetries = 3
|
|
||||||
const retryDelay = 30 * time.Second
|
|
||||||
|
|
||||||
var (
|
|
||||||
cmdMu sync.Mutex
|
|
||||||
cmd *exec.Cmd
|
|
||||||
)
|
|
||||||
|
|
||||||
stopChan := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(stopChan, os.Interrupt, syscall.SIGTERM)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
retries := 0
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-stopChan:
|
|
||||||
log.Println("Received shutdown signal. Terminating subprocess...")
|
|
||||||
cmdMu.Lock()
|
|
||||||
if cmd != nil && cmd.Process != nil {
|
|
||||||
_ = cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
}
|
|
||||||
cmdMu.Unlock()
|
|
||||||
return
|
|
||||||
|
|
||||||
default:
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
cmdMu.Lock()
|
|
||||||
cmd = exec.CommandContext(ctx, command, args...)
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
cmdMu.Unlock()
|
|
||||||
|
|
||||||
log.Printf("Starting Process: %s %v\n", command, args)
|
|
||||||
err := cmd.Run()
|
|
||||||
cancel() // cancel the context when done
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Process exited with error: %v\n", err)
|
|
||||||
retries++
|
|
||||||
if retries >= maxRetries {
|
|
||||||
log.Printf("Process failed %d times. Shutting down application...", retries)
|
|
||||||
// _ = app.ResetBootstrapState()
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
log.Printf("Retrying in %s (%d/%d)...", retryDelay, retries, maxRetries)
|
|
||||||
time.Sleep(retryDelay)
|
|
||||||
} else {
|
|
||||||
log.Printf("Process exited normally. Resetting retry count.")
|
|
||||||
retries = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
package version
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Version string
|
|
||||||
var BuildDate string
|
|
||||||
var Company string
|
|
||||||
var CompanyUrl string
|
|
||||||
|
|
||||||
func ServeVersionInfo(app *pocketbase.PocketBase, se *core.ServeEvent) *router.Route[*core.RequestEvent] {
|
|
||||||
return se.Router.GET("/api/version/send", func(e *core.RequestEvent) error {
|
|
||||||
|
|
||||||
return e.JSON(http.StatusOK, map[string]string{
|
|
||||||
"version": Version,
|
|
||||||
"build_date": BuildDate,
|
|
||||||
"company": Company,
|
|
||||||
"company_url": CompanyUrl,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
package webpconverter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
type memFileReader struct {
|
|
||||||
data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *memFileReader) Open() (io.ReadSeekCloser, error) {
|
|
||||||
return readSeekCloser{bytes.NewReader(m.data)}, nil
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
package webpconverter
|
|
||||||
|
|
||||||
import "bytes"
|
|
||||||
|
|
||||||
type readSeekCloser struct {
|
|
||||||
*bytes.Reader
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r readSeekCloser) Close() error { return nil }
|
|
@ -1,144 +0,0 @@
|
|||||||
package webpconverter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"image"
|
|
||||||
"image/png"
|
|
||||||
"io"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/chai2010/webp"
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
|
||||||
"github.com/pocketbase/pocketbase/tools/hook"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CreateEventHandler(app *pocketbase.PocketBase) *hook.Handler[*core.RecordEvent] {
|
|
||||||
return &hook.Handler[*core.RecordEvent]{
|
|
||||||
Func: func(e *core.RecordEvent) error {
|
|
||||||
fieldsData := e.Record.FieldsData()
|
|
||||||
for _, v := range fieldsData {
|
|
||||||
if files, ok := v.([]interface{}); ok {
|
|
||||||
for _, f := range files {
|
|
||||||
if file, ok := f.(*filesystem.File); ok {
|
|
||||||
if shouldBeReplacedWithWebp(file.Name) {
|
|
||||||
convertToWebp(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if f, ok := v.(interface{}); ok {
|
|
||||||
if file, ok := f.(*filesystem.File); ok {
|
|
||||||
if shouldBeReplacedWithWebp(file.Name) {
|
|
||||||
convertToWebp(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return e.Next()
|
|
||||||
},
|
|
||||||
Priority: 100,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ThumbEventHandler(app *pocketbase.PocketBase) *hook.Handler[*core.FileDownloadRequestEvent] {
|
|
||||||
return &hook.Handler[*core.FileDownloadRequestEvent]{
|
|
||||||
Func: func(fdre *core.FileDownloadRequestEvent) error {
|
|
||||||
if filepath.Ext(fdre.ServedName) == ".webp" {
|
|
||||||
|
|
||||||
filename := fdre.Request.PathValue("filename")
|
|
||||||
baseFilesPath := fdre.Record.BaseFilesPath()
|
|
||||||
|
|
||||||
fs, err := app.App.NewFilesystem()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer fs.Close()
|
|
||||||
|
|
||||||
blob, err := fs.GetReader(baseFilesPath + "/thumbs_" + filename + "/" + fdre.ServedName)
|
|
||||||
if err != nil {
|
|
||||||
blob, err = fs.GetReader(baseFilesPath + "/" + fdre.ServedName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer blob.Close()
|
|
||||||
|
|
||||||
img, err := png.Decode(blob)
|
|
||||||
if err != nil {
|
|
||||||
return fdre.Next()
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := bytes.Buffer{}
|
|
||||||
err = webp.Encode(&buf, img, &webp.Options{Quality: 80})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = fs.Upload(buf.Bytes(), baseFilesPath+"/thumbs_"+filename+"/"+fdre.ServedName)
|
|
||||||
if err != nil {
|
|
||||||
_, err := fdre.Response.Write(buf.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fdre.Next()
|
|
||||||
}
|
|
||||||
return fdre.Next()
|
|
||||||
},
|
|
||||||
Priority: 99,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertToWebp(file *filesystem.File) error {
|
|
||||||
file.Name = replaceExtWithWebp(file.Name)
|
|
||||||
file.OriginalName = replaceExtWithWebp(file.OriginalName)
|
|
||||||
ff, err := file.Reader.Open()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer ff.Close()
|
|
||||||
fff, err := io.ReadAll(ff)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
img, _, err := image.Decode(bytes.NewReader(fff))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := bytes.Buffer{}
|
|
||||||
err = webp.Encode(&buf, img, &webp.Options{Quality: 80})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
file.Reader = &memFileReader{data: buf.Bytes()}
|
|
||||||
file.Size = int64(buf.Len())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func replaceExtWithWebp(path string) string {
|
|
||||||
// List of image extensions to convert
|
|
||||||
exts := []string{".png", ".jpg", ".jpeg", ".bmp", ".tiff"}
|
|
||||||
|
|
||||||
for _, ext := range exts {
|
|
||||||
if strings.HasSuffix(strings.ToLower(path), ext) {
|
|
||||||
return strings.TrimSuffix(path, ext) + ".webp"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldBeReplacedWithWebp(path string) bool {
|
|
||||||
// List of image extensions to convert
|
|
||||||
exts := []string{".png", ".jpg", ".jpeg", ".bmp", ".tiff"}
|
|
||||||
|
|
||||||
for _, ext := range exts {
|
|
||||||
if strings.HasSuffix(strings.ToLower(path), ext) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
module pocketbase
|
|
||||||
|
|
||||||
go 1.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
|
|
||||||
)
|
|
127
backend/go.sum
@ -1,127 +0,0 @@
|
|||||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
|
||||||
github.com/chai2010/webp v1.4.0 h1:6DA2pkkRUPnbOHvvsmGI3He1hBKf/bkRlniAiSGuEko=
|
|
||||||
github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
|
||||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
|
||||||
github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
|
|
||||||
github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
|
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
|
||||||
github.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv/Tk=
|
|
||||||
github.com/ganigeorgiev/fexpr v0.5.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
|
|
||||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
|
|
||||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
|
|
||||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
|
||||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/pocketbase/dbx v1.11.0 h1:LpZezioMfT3K4tLrqA55wWFw1EtH1pM4tzSVa7kgszU=
|
|
||||||
github.com/pocketbase/dbx v1.11.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
|
|
||||||
github.com/pocketbase/pocketbase v0.28.2 h1:b6cfUfr5d4whvUFGFhI8oHRzx/eB76GCUQGftqgv9lM=
|
|
||||||
github.com/pocketbase/pocketbase v0.28.2/go.mod h1:ElwIYS1b5xS9w0U7AK7tsm6FuC0lzw57H8p/118Cu7g=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
|
||||||
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
|
|
||||||
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
|
||||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
|
||||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
|
||||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
|
||||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
|
||||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
|
||||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
|
||||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
|
||||||
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
|
|
||||||
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
|
|
||||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
|
||||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
|
||||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
|
||||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
|
||||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
|
||||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
|
||||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
|
||||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|
||||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
|
||||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
|
||||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
|
||||||
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
|
||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
|
|
||||||
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
|
||||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
|
||||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
|
||||||
modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
|
|
||||||
modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
|
||||||
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
|
|
||||||
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
|
||||||
modernc.org/libc v1.65.8 h1:7PXRJai0TXZ8uNA3srsmYzmTyrLoHImV5QxHeni108Q=
|
|
||||||
modernc.org/libc v1.65.8/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|
||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
|
||||||
modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs=
|
|
||||||
modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g=
|
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
|
||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
|
@ -1,27 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"pocketbase/custom"
|
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase"
|
|
||||||
"github.com/pocketbase/pocketbase/core"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := pocketbase.New()
|
|
||||||
|
|
||||||
app = custom.ExtendApp(app)
|
|
||||||
|
|
||||||
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
|
||||||
|
|
||||||
custom.LoadCustomCode(app, se)
|
|
||||||
|
|
||||||
return se.Next()
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := app.Start(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
111
components/CartPopup.vue
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<template>
|
||||||
|
<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 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>
|
||||||
|
</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>
|
145
components/CountryCurrencySelector.vue
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
<template>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{{ $session.cookieData.value.country.iso_code }}/{{
|
||||||
|
$session.cookieData.value.currency.iso_code
|
||||||
|
}}
|
||||||
|
</UButton>
|
||||||
|
|
||||||
|
<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="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="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
|
||||||
|
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="
|
||||||
|
() => {
|
||||||
|
$session.setCountry(item.iso_code);
|
||||||
|
$session.loadSession();
|
||||||
|
dropCountry = false;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ item?.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-start gap-[6px]">
|
||||||
|
<p>{{ $t("currency") }}</p>
|
||||||
|
<div
|
||||||
|
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="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
|
||||||
|
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]"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
$session.setCurrency(item.iso_code);
|
||||||
|
$session.loadSession();
|
||||||
|
dropCurrency = false;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ item?.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onClickOutside } from '@vueuse/core'
|
||||||
|
|
||||||
|
const { $session } = useNuxtApp()
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
const menuStore = useMenuStore()
|
||||||
|
|
||||||
|
const dropdownRef = ref(null)
|
||||||
|
|
||||||
|
const dropCountry = ref(false)
|
||||||
|
const dropCurrency = ref(false)
|
||||||
|
|
||||||
|
function openDrop(type?: string) {
|
||||||
|
if (type === 'country') {
|
||||||
|
dropCurrency.value = false
|
||||||
|
dropCountry.value = !dropCountry.value
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dropCountry.value = false
|
||||||
|
dropCurrency.value = !dropCurrency.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickOutside(dropdownRef, () => {
|
||||||
|
isOpen.value = false
|
||||||
|
dropCurrency.value = false
|
||||||
|
dropCountry.value = false
|
||||||
|
})
|
||||||
|
</script>
|
@ -1,67 +1,394 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="border-border border-b">
|
<div>
|
||||||
<UContainer
|
<!-- xl -->
|
||||||
class="mx-auto w-full max-w-[360px] px-[16px] sm:max-w-[768px] sm:px-[17px] md:max-w-[1000px] md:px-[24px] xl:max-w-[1920px] xl:px-[80px]"
|
<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 gap-5 whitespace-nowrap w-full"
|
||||||
>
|
>
|
||||||
<div class="hidden h-[120px] w-full items-center justify-between xl:flex">
|
|
||||||
<ul class="flex items-center gap-20 whitespace-nowrap">
|
|
||||||
<li
|
<li
|
||||||
v-for="(item, index) in menuStore.menu"
|
v-for="(item, index) in menuStore.menu"
|
||||||
@click="menuStore.navigateToItem(item);"
|
:key="item.id"
|
||||||
:key="index"
|
:class="[
|
||||||
class="hover:text-text-light/80 dark:hover:text-text-dark/70 cursor-pointer text-lg transition-all"
|
'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 />
|
0{{ index + 1 }} <br>
|
||||||
{{ item.name }}
|
{{ item.front_menu_lang[0].name }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<img class="dark:hidden" src="/logo.png" alt="Frame" />
|
<ClientOnly v-if="!colorMode?.forced">
|
||||||
<img class="hidden dark:block" src="/logo-dark.png" alt="Frame" />
|
<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]">
|
<div class="flex items-center gap-[30px]">
|
||||||
<i class="uil uil-user text-[35px] cursor-pointer"></i>
|
<div>
|
||||||
<i class="uil uil-shopping-cart text-[35px] cursor-pointer"></i>
|
<i v-if="!userStore.isLogged"
|
||||||
|
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>
|
</div>
|
||||||
<ThemeSwitcher />
|
<ThemeSwitcher />
|
||||||
<LangSwitcher />
|
|
||||||
<button
|
<button
|
||||||
class="hover:bg-button-hover bg-button cursor-pointer rounded-xl px-6 py-3 font-medium text-white transition-all"
|
: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"
|
||||||
>
|
>
|
||||||
E-shop
|
{{ $t("eshop") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- md -->
|
||||||
<div
|
<div
|
||||||
class="hidden h-[116px] w-full items-center justify-between md:flex xl:hidden"
|
class="hidden w-full md:flex md:flex-col xl:hidden items-center justify-center"
|
||||||
>
|
>
|
||||||
<img src="/logo.png" alt="Frame" />
|
<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()"
|
||||||
|
>
|
||||||
|
</ClientOnly>
|
||||||
<div class="flex items-center gap-6">
|
<div class="flex items-center gap-6">
|
||||||
<i class="uil uil-user text-[35px] cursor-pointer"></i>
|
<div class="flex items-center gap-[30px]">
|
||||||
<i class="uil uil-shopping-cart text-[35px] 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 />
|
<ThemeSwitcher />
|
||||||
<i class="uil uil-apps text-[35px] cursor-pointer"></i>
|
<i
|
||||||
|
variant="subtle"
|
||||||
|
block
|
||||||
|
class="uil uil-apps text-[33px] cursor-pointer"
|
||||||
|
@click="open = !open"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</UiContainer>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<UCollapsible
|
||||||
class="hidden h-[84px] w-full items-center justify-between sm:flex md:hidden"
|
v-model:open="open"
|
||||||
|
:ui="{ content: 'w-full' }"
|
||||||
|
class="w-full"
|
||||||
>
|
>
|
||||||
<img src="/logo.png" alt="Frame" />
|
<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"
|
||||||
|
: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;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="leading-[70%] text-inter">
|
||||||
|
<span class="mr-4">0{{ index + 1 }}</span>
|
||||||
|
{{ item.front_menu_lang[0].name }}
|
||||||
|
</div>
|
||||||
|
<!-- <i class="uil uil-arrow-up-right text-[35px]"></i> -->
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCollapsible>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- sm -->
|
||||||
|
<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()"
|
||||||
|
>
|
||||||
|
</ClientOnly>
|
||||||
<div class="flex items-center gap-6">
|
<div class="flex items-center gap-6">
|
||||||
<i class="uil uil-user text-[35px] cursor-pointer"></i>
|
<div class="flex items-center gap-[30px]">
|
||||||
<i class="uil uil-shopping-cart text-[35px] cursor-pointer"></i>
|
<div>
|
||||||
<i class="uil uil-apps text-[35px] cursor-pointer"></i>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex h-[84px] w-full items-center justify-between sm:hidden">
|
<CartPopup />
|
||||||
<img src="/logo.png" alt="Frame" />
|
</div>
|
||||||
|
<i
|
||||||
|
variant="subtle"
|
||||||
|
block
|
||||||
|
class="uil uil-apps text-[30px] cursor-pointer"
|
||||||
|
@click="open = !open"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</div>
|
||||||
|
<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"
|
||||||
|
: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;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="leading-[70%] text-inter">
|
||||||
|
<span class="mr-4">0{{ index + 1 }}</span>
|
||||||
|
{{ item.front_menu_lang[0].name }}
|
||||||
|
</div>
|
||||||
|
<!-- <i class="uil uil-arrow-up-right text-[35px]"></i> -->
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<p class="leading-[70%] text-inter">
|
||||||
|
{{ $t("change_language") }}
|
||||||
|
</p>
|
||||||
|
<LangSwitcher />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<p class="leading-[70%] text-inter">
|
||||||
|
{{ $t("change_currency_and_country") }}
|
||||||
|
</p>
|
||||||
|
<CountryCurrencySelector />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<p class="leading-[70%] text-inter">
|
||||||
|
{{ $t("change_theme") }}
|
||||||
|
</p>
|
||||||
|
<ThemeSwitcher />
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCollapsible>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- smallest one -->
|
||||||
|
<div class="w-full items-center justify-between flex flex-col sm: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()"
|
||||||
|
>
|
||||||
|
</ClientOnly>
|
||||||
<div class="flex items-center gap-6">
|
<div class="flex items-center gap-6">
|
||||||
<i class="uil uil-user text-[35px] cursor-pointer"></i>
|
<div>
|
||||||
<i class="uil uil-apps text-[35px] cursor-pointer"></i>
|
<i
|
||||||
|
class="uil uil-user text-[30px] cursor-pointer"
|
||||||
|
@click="
|
||||||
|
!userStore.isLogged
|
||||||
|
&& menuStore.navigateToItem(
|
||||||
|
menuStore.menuItems?.find((item) => item.id === 11),
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<CartPopup />
|
||||||
|
<i
|
||||||
|
variant="subtle"
|
||||||
|
block
|
||||||
|
class="uil uil-apps text-[30px] cursor-pointer"
|
||||||
|
@click="open = !open"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</div>
|
||||||
|
<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"
|
||||||
|
: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;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="leading-[70%] text-inter">
|
||||||
|
<span class="mr-4">0{{ index + 1 }}</span>
|
||||||
|
{{ item.front_menu_lang[0].name }}
|
||||||
|
</div>
|
||||||
|
<!-- <i class="uil uil-arrow-up-right text-[35px]"></i> -->
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<p class="leading-[70%] text-lg text-inter">
|
||||||
|
{{ $t("change_language") }}
|
||||||
|
</p>
|
||||||
|
<LangSwitcher />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<p class="leading-[70%] text-lg text-inter">
|
||||||
|
{{ $t("change_currency_and_country") }}
|
||||||
|
</p>
|
||||||
|
<CountryCurrencySelector />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<p class="leading-[70%] text-lg text-inter">
|
||||||
|
{{ $t("change_theme") }}
|
||||||
|
</p>
|
||||||
|
<ThemeSwitcher />
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCollapsible>
|
||||||
</div>
|
</div>
|
||||||
</UContainer>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
|
||||||
import LangSwitcher from './langSwitcher.vue';
|
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import CartPopup from './CartPopup.vue'
|
||||||
|
import CountryCurrencySelector from './CountryCurrencySelector.vue'
|
||||||
|
import LangSwitcher from './LangSwitcher.vue'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const open = ref(false)
|
||||||
|
const colorMode = useColorMode()
|
||||||
const menuStore = useMenuStore()
|
const menuStore = useMenuStore()
|
||||||
|
const checkoutStore = useCheckoutStore()
|
||||||
|
|
||||||
|
await checkoutStore.getUserCart()
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const isDark = computed({
|
||||||
|
get() {
|
||||||
|
return colorMode.value === 'dark'
|
||||||
|
},
|
||||||
|
set(_isDark) {
|
||||||
|
colorMode.preference = _isDark ? 'dark' : 'light'
|
||||||
|
},
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
53
components/ImageBlock.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<!-- <svg
|
||||||
|
class=""
|
||||||
|
viewBox="0 0 870 350"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="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"
|
||||||
|
fill="url(#pattern0_381_6471)"
|
||||||
|
/>
|
||||||
|
<defs>
|
||||||
|
<pattern
|
||||||
|
id="pattern0_381_6471"
|
||||||
|
patternContentUnits="objectBoundingBox"
|
||||||
|
width="1"
|
||||||
|
height="1"
|
||||||
|
>
|
||||||
|
<use
|
||||||
|
xlink:href="#image0_381_6471"
|
||||||
|
transform="matrix(0.000732064 0 0 0.0018197 0 -0.198766)"
|
||||||
|
/>
|
||||||
|
</pattern>
|
||||||
|
<image
|
||||||
|
id="image0_381_6471"
|
||||||
|
width="1366"
|
||||||
|
height="768"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
xlink:href="/block.webp"
|
||||||
|
/>
|
||||||
|
</defs>
|
||||||
|
</svg> -->
|
||||||
|
<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'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</style>
|
50
components/LangSwitcher.vue
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<USelect
|
||||||
|
v-model="selectedLocaleCode"
|
||||||
|
:items="locales"
|
||||||
|
value-key="code"
|
||||||
|
:searchable="false"
|
||||||
|
:ui="{
|
||||||
|
base: 'bg-inherit ring-0 cursor-pointer w-auto focus:ring-0 outline-none focus-visible:ring-0',
|
||||||
|
trailing: 'hidden',
|
||||||
|
viewport: 'ring-0',
|
||||||
|
content: 'bg-bg-light dark:bg-bg-dark w-auto ring-0 border border-button',
|
||||||
|
leading:
|
||||||
|
'left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 pr-6',
|
||||||
|
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($event);
|
||||||
|
$session.setLanguage($event);
|
||||||
|
$session.loadSession();
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template #leading="{ modelValue }">
|
||||||
|
<div class="flex items-center gap-2 text-xl font-medium uppercase">
|
||||||
|
{{ locales.find((item: any) => item.code === modelValue)?.code }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #item="{ item }">
|
||||||
|
<div class="flex items-center gap-2 cursor-pointer w-full">
|
||||||
|
<UIcon
|
||||||
|
:name="item.icon as string"
|
||||||
|
class="size-5 rounded-full border border-button/40"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
class="text-xl font-medium uppercase text-text-light dark:text-text-dark opacity-100"
|
||||||
|
>
|
||||||
|
{{ item.code }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</USelect>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { locale, locales, setLocale } = useI18n()
|
||||||
|
const { $session } = useNuxtApp()
|
||||||
|
|
||||||
|
const selectedLocaleCode = ref(locale.value)
|
||||||
|
</script>
|
322
components/MapBlock.vue
Normal file
@ -1,23 +1,29 @@
|
|||||||
<script setup>
|
|
||||||
const colorMode = useColorMode();
|
|
||||||
|
|
||||||
const isDark = computed({
|
|
||||||
get() {
|
|
||||||
return colorMode.value === "dark";
|
|
||||||
},
|
|
||||||
set(_isDark) {
|
|
||||||
colorMode.preference = _isDark ? "dark" : "light";
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ClientOnly v-if="!colorMode?.forced">
|
<ClientOnly v-if="!colorMode?.forced">
|
||||||
<div class="flex h-8 w-8 cursor-pointer items-center justify-center">
|
<div class="flex h-8 w-8 cursor-pointer items-center justify-center">
|
||||||
<i @click="isDark = !isDark" :class="['uil text-[35px] cursor-pointer', isDark ? 'uil-moon' : 'uil-sun']"></i>
|
<i
|
||||||
|
:class="[
|
||||||
|
'uil text-[32px] cursor-pointer',
|
||||||
|
isDark ? 'uil-moon' : 'uil-sun',
|
||||||
|
]"
|
||||||
|
@click="isDark = !isDark"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<div class="size-20" />
|
<div class="size-20" />
|
||||||
</template>
|
</template>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
|
const isDark = computed({
|
||||||
|
get() {
|
||||||
|
return colorMode.value === 'dark'
|
||||||
|
},
|
||||||
|
set(_isDark) {
|
||||||
|
colorMode.preference = _isDark ? 'dark' : 'light'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
<template>
|
|
||||||
<USelectMenu
|
|
||||||
v-model="selectedLocale"
|
|
||||||
:items="locales"
|
|
||||||
label-key="name"
|
|
||||||
:search-input="{ icon: 'i-lucide-search' }"
|
|
||||||
placeholder="Select country"
|
|
||||||
class="w-48 bg-block-light dark:bg-block-dark border-border-light dark:border-border-dark"
|
|
||||||
@update:model-value="setLocaleManually($event)"
|
|
||||||
>
|
|
||||||
<template #leading="{ modelValue, ui }">
|
|
||||||
<span v-if="modelValue" class="size-5 text-center">
|
|
||||||
<UIcon
|
|
||||||
:name="(modelValue?.icon as string) || 'i-lucide-earth'"
|
|
||||||
:class="[ui.leadingIcon(), 'border border-border-light dark:border-border-dark rounded-full']"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template #item-leading="{ item }">
|
|
||||||
<span class="size-5 text-center">
|
|
||||||
<!-- {{ item.name }} -->
|
|
||||||
<UIcon :name="item.icon as string" class="size-5 text-accent" />
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</USelectMenu>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { LocaleObject } from "@nuxtjs/i18n";
|
|
||||||
const { setLocale, locales, locale } = useI18n();
|
|
||||||
|
|
||||||
const selectedLocale = ref(
|
|
||||||
locales.value.find((item) => item.code === locale.value)
|
|
||||||
);
|
|
||||||
|
|
||||||
function setLocaleManually(newLocale: LocaleObject) {
|
|
||||||
locale.value = newLocale.code;
|
|
||||||
locales.value.find((item) => item.code === locale.value);
|
|
||||||
setLocale(newLocale.code);
|
|
||||||
}
|
|
||||||
</script>
|
|
168
components/section/AboutGoldImpact.vue
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
<template>
|
||||||
|
<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.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.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"
|
||||||
|
>
|
||||||
|
{{ 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"
|
||||||
|
>
|
||||||
|
{{ item.block_title }}
|
||||||
|
</h4>
|
||||||
|
<p>{{ item.block_text }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-25-55-75">
|
||||||
|
<h2 class="h2-bold-bounded">
|
||||||
|
{{ 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
|
||||||
|
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>
|
||||||
|
</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/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/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.front_section_lang[0].data.section_3.title }}
|
||||||
|
</h2>
|
||||||
|
<div class="flex flex-col space-25-55">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<h2 class="h2-bold-bounded">
|
||||||
|
{{ component.front_section_lang[0].data.section_4.title }}
|
||||||
|
</h2>
|
||||||
|
<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/public/file/${component.img[2]}_l.webp')`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<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: {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
img: string[]
|
||||||
|
component_name: string
|
||||||
|
is_no_lang: boolean
|
||||||
|
page_name: string
|
||||||
|
front_section_lang: {
|
||||||
|
data: {
|
||||||
|
section_1: {
|
||||||
|
title: string
|
||||||
|
text_blocks: [
|
||||||
|
{
|
||||||
|
block_title: string
|
||||||
|
block_text: string
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
section_2: {
|
||||||
|
title: string
|
||||||
|
text_blocks: [
|
||||||
|
{
|
||||||
|
block_title: string
|
||||||
|
block_text: string
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
section_3: {
|
||||||
|
title: string
|
||||||
|
text_blocks: []
|
||||||
|
}
|
||||||
|
section_4: {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
description_second: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
id_front_section: number
|
||||||
|
id_lang: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
</script>
|
150
components/section/AboutGoldMain.vue
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<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>
|
||||||
|
<h1 class="h1-big text-accent-green-light mb-6 md:mb-10">
|
||||||
|
{{ component.front_section_lang[0].data.title }}
|
||||||
|
</h1>
|
||||||
|
<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>
|
||||||
|
</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/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',
|
||||||
|
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/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/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.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.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/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.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>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
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: string[]
|
||||||
|
}
|
||||||
|
section_3: {
|
||||||
|
title: string
|
||||||
|
text_blocks: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
id_front_section: number
|
||||||
|
id_lang: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
</script>
|
79
components/section/AboutUsDifferent.vue
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<UiContainer class="space-25-55-75">
|
||||||
|
<div class="grid xl:grid-cols-2">
|
||||||
|
<div class="space-25-75 xl:col-start-2">
|
||||||
|
<h2 class="h2-bold-bounded">
|
||||||
|
{{ component.front_section_lang[0].data.title }}
|
||||||
|
</h2>
|
||||||
|
<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.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',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<h4 class="h4-uppercase-bold-inter">
|
||||||
|
<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>
|
||||||
|
</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/public/file/${component.img[0]}_l.webp')`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'top',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { UiButtonArrow } from '#components'
|
||||||
|
|
||||||
|
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>
|
157
components/section/AboutUsHistory.vue
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
<template>
|
||||||
|
<UiContainer class="space-55-75">
|
||||||
|
<div class="space-25-55-75">
|
||||||
|
<h2 class="h2-bold-bounded">
|
||||||
|
{{ 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/public/file/${component.img[0]}_l.webp')`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'bottom',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col justify-between space-25-55">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<h4 class="h4-uppercase-bold-inter">
|
||||||
|
{{ component.front_section_lang[0].data.section_1.sub_title }}
|
||||||
|
</h4>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<p
|
||||||
|
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.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.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, 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.front_section_lang[0].data.section_3.title }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col xl:flex-row gap-12">
|
||||||
|
<div class="flex flex-col sm:flex-row gap-3 min-w-[60%]">
|
||||||
|
<div
|
||||||
|
class="rounded-2xl h-[230px] sm:h-[300px] w-full xl:h-[770px]"
|
||||||
|
:style="{
|
||||||
|
backgroundImage: `url('/api/public/file/${component.img[1]}_l.webp')`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="rounded-2xl xl:hidden w-full h-[230px] sm:h-[300px] xl:w-full xl:h-[328px]"
|
||||||
|
:style="{
|
||||||
|
backgroundImage: `url('/api/public/file/${component.img[2]}_l.webp')`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col justify-between space-25-55 xl:w-[70%]">
|
||||||
|
<div
|
||||||
|
class="hidden xl:block rounded-2xl max-full min-h-[330px]"
|
||||||
|
:style="{
|
||||||
|
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.front_section_lang[0].data.section_4.title }}
|
||||||
|
</h4>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<p
|
||||||
|
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.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.front_section_lang[0].data.section_5.title }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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>
|
84
components/section/AboutUsMain.vue
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<UiContainer class="space-y-[55px] sm:space-y-[75px] xl:space-y-[100px]">
|
||||||
|
<div class="w-full flex flex-col md:flex-row justify-between gap-7">
|
||||||
|
<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.front_section_lang[0].data.main_title }}
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
{{ component.front_section_lang[0].data.main_description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<h4 class="h4-uppercase-bold-inter">
|
||||||
|
{{ 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"
|
||||||
|
:style="{
|
||||||
|
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.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/public/file/${component.img[1]}_l.webp')`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}"></div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col w-full gap-6">
|
||||||
|
<h4 class="h4-uppercase-bold-inter">
|
||||||
|
{{ component.front_section_lang[0].data.story_subtitle }}
|
||||||
|
</h4>
|
||||||
|
<div class="flex flex-col justify-between xl:max-w-[70%]">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
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>
|
147
components/section/BusinessMain.vue
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<UiContainer class="space-y-[55px] md:space-y-[75px] xl:space-y-[100px]">
|
||||||
|
<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.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.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/public/file/${component.img[0]}_l.webp')`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<div class="md:w-full xl:w-[70%] space-y-14">
|
||||||
|
<p>{{ component.front_section_lang[0].data.main_description }}</p>
|
||||||
|
<h4 class="h4-uppercase-bold-inter">
|
||||||
|
{{ 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.front_section_lang[0].data.section_1_title }}
|
||||||
|
</h2>
|
||||||
|
<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.front_section_lang[0].data
|
||||||
|
.section_2_title"
|
||||||
|
:key="index"
|
||||||
|
:class="[
|
||||||
|
item.highlight
|
||||||
|
? 'text-accent-green-light dark:text-accent-green-dark'
|
||||||
|
: '',
|
||||||
|
'inline',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ item.text }}
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
index !== component.front_section_lang[0].data.title.length - 1
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<p v-html="component.front_section_lang[0].data.section_2_description" />
|
||||||
|
</div>
|
||||||
|
<div class="space-25-75">
|
||||||
|
<h2 class="h2-bold-bounded">
|
||||||
|
{{ 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.front_section_lang[0].data.section_3_description }}
|
||||||
|
</p>
|
||||||
|
<div class="">
|
||||||
|
<p
|
||||||
|
v-for="(item, index) in component.front_section_lang[0].data
|
||||||
|
.section_3_items"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
{{ index + 1 }}.
|
||||||
|
{{ item }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="h-[315px] xl:h-full rounded-2xl"
|
||||||
|
:style="{
|
||||||
|
backgroundImage: `url('/api/public/file/${component.img[1]}_l.webp')`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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.front_section_lang[0].data.section_4_title }}
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
v-html="component.front_section_lang[0].data.section_4_description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
id_front_section: number
|
||||||
|
id_lang: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
</script>
|
256
components/section/BusinessReasons.vue
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
<template>
|
||||||
|
<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.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.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.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 }}
|
||||||
|
</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/public/file/${component.img[0]}_l.webp')`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'top',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-25-55">
|
||||||
|
<div>
|
||||||
|
<div class="space-25-75">
|
||||||
|
<h2 class="h2-bold-bounded">
|
||||||
|
{{ 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.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.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.front_section_lang[0].data.cta_description_intro }}
|
||||||
|
</p>
|
||||||
|
<h4 class="h4-uppercase-bold-inter">
|
||||||
|
{{
|
||||||
|
component.front_section_lang[0].data
|
||||||
|
.main_call_to_action_statement
|
||||||
|
}}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<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.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="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"
|
||||||
|
>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="w-full flex justify-center sm:justify-start">
|
||||||
|
<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") }}
|
||||||
|
</h4>
|
||||||
|
<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>+420 608 428 782</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray">
|
||||||
|
{{ $t("email") }}
|
||||||
|
</p>
|
||||||
|
<p>web@yourgold.cz</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray">
|
||||||
|
{{ $t("office_address") }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Floriána Nováka 3 <br>
|
||||||
|
796 01 Prostějov <br>
|
||||||
|
Czech Republic <br>
|
||||||
|
CZ 08435456
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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/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.front_section_lang[0].data.closing_inspirational_block
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<h4 class="h4-uppercase-bold-inter">
|
||||||
|
<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.front_section_lang[0].data.reasons_section_title
|
||||||
|
.length
|
||||||
|
- 1
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</h4>
|
||||||
|
<p>{{ component.front_section_lang[0].data.final_tagline }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
id_front_section: number
|
||||||
|
id_lang: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
</script>
|
82
components/section/CategoryTree.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{{ 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"
|
||||||
|
/></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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' : ''"
|
||||||
|
@click="$emit('change-category', child)"
|
||||||
|
>
|
||||||
|
<span>{{ child.langs[0].Name }}</span>
|
||||||
|
<span>12</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<!-- Optional: Placeholder or message when main category is not available -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
// Define the props
|
||||||
|
const props = defineProps({
|
||||||
|
data: Array,
|
||||||
|
active: Number,
|
||||||
|
})
|
||||||
|
|
||||||
|
defineEmits(['change-category'])
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
isOpen.value = !isOpen.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computed properties
|
||||||
|
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>
|
464
components/section/CheckoutMain.vue
Normal 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>
|
252
components/section/CheckoutSummary.vue
Normal 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?.address.name }} {{
|
||||||
|
checkoutStore.defaultAddress?.address.surname }}</span>
|
||||||
|
<span>{{ checkoutStore.defaultAddress?.address.street }}</span>
|
||||||
|
<span>{{ checkoutStore.defaultAddress?.address.postcode }} {{
|
||||||
|
checkoutStore.defaultAddress?.address.city }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div 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>
|
91
components/section/ContactMain.vue
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<UiContainer class="space-y-[45px]">
|
||||||
|
<div class="xl:w-[70%] space-y-[25px]">
|
||||||
|
<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="flex gap-[30px]">
|
||||||
|
<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-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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-[30px] xl:px-[50px] sm:px-10">
|
||||||
|
<h4 class="h4-uppercase-bold-inter">
|
||||||
|
{{ $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]"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray">
|
||||||
|
{{ $t("phone") }}
|
||||||
|
</p>
|
||||||
|
<p>+420 608 428 782</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray">
|
||||||
|
{{ $t("email") }}
|
||||||
|
</p>
|
||||||
|
<p>web@yourgold.cz</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-gray">
|
||||||
|
{{ $t("office_address") }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Floriána Nováka 3 <br>
|
||||||
|
796 01 Prostějov <br>
|
||||||
|
Czech Republic <br>
|
||||||
|
CZ 08435456
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
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>
|
75
components/section/CurrencyRatesBar.vue
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex gap-24">
|
||||||
|
<div class="price-container">
|
||||||
|
<div class="slider"
|
||||||
|
v-html="duplicatedContent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
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()
|
||||||
|
productStore.getModules()
|
||||||
|
|
||||||
|
// Computed property to duplicate the content
|
||||||
|
const duplicatedContent = computed(() => {
|
||||||
|
const originalContent = res || ''
|
||||||
|
return originalContent + originalContent + originalContent + originalContent
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.price-container {
|
||||||
|
/* display: flex; */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
display: flex;
|
||||||
|
gap: 64px;
|
||||||
|
animation: slidein 40s linear infinite;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logos {
|
||||||
|
display: flex;
|
||||||
|
gap: 64px;
|
||||||
|
width: 100%;
|
||||||
|
/* display: inline-block; */
|
||||||
|
margin: 0px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fas {
|
||||||
|
height: 50%;
|
||||||
|
animation: fade-in 0.5s cubic-bezier(0.455, 0.03, 0.515, 0.955) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slidein {
|
||||||
|
from {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translate3d(-100%, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 900px) {
|
||||||
|
.slider {
|
||||||
|
animation: slidein 80s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
85
components/section/FooterBlock.vue
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<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>
|
||||||
|
<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()"
|
||||||
|
>
|
||||||
|
</ClientOnly>
|
||||||
|
</UiContainer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { GenericResponse, Footer } from '~/types'
|
||||||
|
|
||||||
|
const menuStore = useMenuStore()
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
|
const { data } = await useMyFetch<GenericResponse<Footer>>(
|
||||||
|
'/api/public/front/footer',
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
const contact = ref(data.data.contact)
|
||||||
|
const docs = ref(data.data.docs)
|
||||||
|
|
||||||
|
const isDark = computed(() => colorMode.value === 'dark')
|
||||||
|
</script>
|
112
components/section/InvestmentFaq.vue
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<UiContainer>
|
||||||
|
<div class="space-25-55">
|
||||||
|
<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.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"
|
||||||
|
>
|
||||||
|
<h4 class="h4-uppercase-bold-inter">
|
||||||
|
<span v-if="index + 1 < 10">0</span>{{ index + 1 }}
|
||||||
|
</h4>
|
||||||
|
<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',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="max-w-[1200px] flex flex-col gap-6">
|
||||||
|
<h4
|
||||||
|
:class="[
|
||||||
|
'h4-uppercase-bold-inter transition-colors duration-300',
|
||||||
|
active === index
|
||||||
|
&& 'text-accent-green-light dark:text-accent-green-dark',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<transition name="fade-slide">
|
||||||
|
<p v-if="active === index">
|
||||||
|
{{ item.content }}
|
||||||
|
</p>
|
||||||
|
</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
|
||||||
|
? '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"
|
||||||
|
/>
|
||||||
|
</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.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>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
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
|
||||||
|
},
|
||||||
|
]
|
||||||
|
sub_title: [
|
||||||
|
{
|
||||||
|
text: string
|
||||||
|
highlight: boolean
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
id_front_section: number
|
||||||
|
id_lang: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const active = ref<number>(0)
|
||||||
|
</script>
|
99
components/section/InvestmentMain.vue
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<template>
|
||||||
|
<UiContainer class="space-y-[30px] xl:space-y-[55px]">
|
||||||
|
<div class="flex flex-col xl:flex-row items-stretch w-full h-full gap-5">
|
||||||
|
<div
|
||||||
|
class="flex flex-col justify-between items-center space-25-55 xl:gap-4 w-auto h-auto"
|
||||||
|
>
|
||||||
|
<h1 class="h1">
|
||||||
|
<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.front_section_lang[0].data.title.length - 1
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<h4 class="h4-uppercase-bold-inter">
|
||||||
|
<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>
|
||||||
|
<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/public/file/${component.img[0]}_l.webp')`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col xl:flex-row items-stretch w-full h-full gap-5">
|
||||||
|
<div
|
||||||
|
class="w-full xl:max-w-[570px] h-[225px] block rounded-2xl"
|
||||||
|
:style="{
|
||||||
|
backgroundImage: `url('/api/public/file/${component.img[1]}_l.webp')`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="flex flex-col justify-between items-center space-25-55 xl:gap-4 w-auto h-auto"
|
||||||
|
>
|
||||||
|
<p>{{ component.front_section_lang[0].data.description_second }}</p>
|
||||||
|
<h4 class="h4-uppercase-bold-inter">
|
||||||
|
{{ component.front_section_lang[0].data.sub_title_second }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
id_front_section: number
|
||||||
|
id_lang: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
</script>
|
106
components/section/InvestmentSave.vue
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<template>
|
||||||
|
<UiContainer class="space-y-[50px] sm:space-y-[30px] xl:space-y-[100px]">
|
||||||
|
<div class="space-25-55">
|
||||||
|
<div class="space-25-75">
|
||||||
|
<h4 class="h4-uppercase-bold-inter">
|
||||||
|
{{ 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"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex flex-col space-y-[55px] xl:space-y-0 xl:justify-between"
|
||||||
|
>
|
||||||
|
<p>{{ component.front_section_lang[0].data.main_description }}</p>
|
||||||
|
<p>{{ component.front_section_lang[0].data.section_title }}</p>
|
||||||
|
<ul>
|
||||||
|
<li
|
||||||
|
v-for="(item, index) in component.front_section_lang[0].data
|
||||||
|
.section_items"
|
||||||
|
:key="index"
|
||||||
|
class=""
|
||||||
|
>
|
||||||
|
<span>{{ index + 1 }}. </span>{{ item }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="w-full xl:max-w-[690px] h-[200px] sm:h-[390px] block rounded-2xl"
|
||||||
|
:style="{
|
||||||
|
backgroundImage: `url('/api/public/file/${component.img[0]}_l.webp')`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center w-[60%] 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/public/file/${component.img[2]}_l.webp`"
|
||||||
|
alt=""
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="hidden dark:block"
|
||||||
|
:src="`/api/public/file/${component.img[3]}_l.webp`"
|
||||||
|
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.front_section_lang[0].data.info_description }}</p>
|
||||||
|
<h4 class="h4-uppercase-bold-inter">
|
||||||
|
{{ component.front_section_lang[0].data.info_title }}
|
||||||
|
</h4>
|
||||||
|
<div
|
||||||
|
class=""
|
||||||
|
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/public/file/${component.img[1]}_l.webp')`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
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>
|
52
components/section/InvestmentWhy.vue
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<UiContainer>
|
||||||
|
<div class="space-25-55-75 max-w-[1000px] mx-auto">
|
||||||
|
<h1 class="h2-bold-bounded">
|
||||||
|
<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.front_section_lang[0].data.title.length - 1
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<h4 class="h4-uppercase-bold-inter">
|
||||||
|
{{ component.front_section_lang[0].data.description }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
id_front_section: number
|
||||||
|
id_lang: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
</script>
|
232
components/section/InvestmentZone.vue
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
<template>
|
||||||
|
<UiContainer class="space-25-75">
|
||||||
|
<h2 class="h2-bold-bounded">
|
||||||
|
<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.front_section_lang[0].data.main_title.length - 1
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="space-25-55-75">
|
||||||
|
<p>{{ component.front_section_lang[0].data.main_description }}</p>
|
||||||
|
<h4 class="h4-uppercase-bold-inter">
|
||||||
|
{{ component.front_section_lang[0].data.section_title }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- products -->
|
||||||
|
<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',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<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="`/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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiButtonArrow
|
||||||
|
:arrow="true"
|
||||||
|
class="mx-auto"
|
||||||
|
type="fill"
|
||||||
|
@click="menuStore.navigateToShop"
|
||||||
|
>
|
||||||
|
{{ $t("eshop") }}
|
||||||
|
</UiButtonArrow>
|
||||||
|
</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 space-y-[55px] sm:justify-between">
|
||||||
|
<div class="space-25-55">
|
||||||
|
<p>{{ component.front_section_lang[0].data.section_description }}</p>
|
||||||
|
<h4 class="h4-uppercase-bold-inter">
|
||||||
|
{{ component.front_section_lang[0].data.info_title }}
|
||||||
|
</h4>
|
||||||
|
<p>{{ component.front_section_lang[0].data.info_description }}</p>
|
||||||
|
</div>
|
||||||
|
<h4 class="h4-uppercase-bold-inter">
|
||||||
|
{{ 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"
|
||||||
|
>
|
||||||
|
<h2 class="h2-bold-bounded text-center mb-10 sm:mb-20">
|
||||||
|
{{ 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>
|
||||||
|
</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()"
|
||||||
|
>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
{{ 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()"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{{ menuStore.selectedCurrency?.sign }} {{ store.totalInvestment }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<UiButtonArrow :arrow="true"
|
||||||
|
type="fill"
|
||||||
|
class="mx-auto sm:m-0"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
component.front_section_lang[0].data.button
|
||||||
|
}}
|
||||||
|
</UiButtonArrow>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
id_front_section: number
|
||||||
|
id_lang: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const menuStore = useMenuStore()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(itemCount, async () => {
|
||||||
|
await productStore.getList(itemCount.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await updateItemCount()
|
||||||
|
window.addEventListener('resize', updateItemCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', updateItemCount)
|
||||||
|
})
|
||||||
|
</script>
|
115
components/section/LoginMain.vue
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<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("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-[15px]">
|
||||||
|
<p class="pl-6">
|
||||||
|
{{ $t("email") }}
|
||||||
|
</p>
|
||||||
|
<input 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="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>
|
||||||
|
<div class="py-[25px] sm:py-12 border-b border-gray flex justify-center w-full">
|
||||||
|
<UiButtonArrow 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>
|
||||||
|
</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 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 type="fill" :arrow="true" @click="userStore.sendFormCode(true)">
|
||||||
|
{{ $t("confirm") }}
|
||||||
|
</UiButtonArrow>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
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>
|
86
components/section/MainBlocks.vue
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<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>
|
||||||
|
<p>{{ item.description }}</p>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
<p>{{ item.description }}</p>
|
||||||
|
<h4 class="h4-uppercase-bold-inter">
|
||||||
|
{{ item.sub_title }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<UiImgWrapper :src="`/api/public/file/${component.img[index]}_l.webp`">
|
||||||
|
<template #button>
|
||||||
|
<UiButtonArrow :arrow="true">
|
||||||
|
{{ item.title }}
|
||||||
|
</UiButtonArrow>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<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" />
|
||||||
|
<p>Partners</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-3 h-3 rounded-[2.8px] bg-block" />
|
||||||
|
<p>Customers</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
type 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
|
||||||
|
sub_title: string
|
||||||
|
}[]
|
||||||
|
id_front_section: number
|
||||||
|
id_lang: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
component: Component
|
||||||
|
}>()
|
||||||
|
</script>
|
55
components/section/MainHero.vue
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<UiContainer>
|
||||||
|
<div class="space-y-[30px] sm:space-y-[53px]">
|
||||||
|
<h1 class="h1">
|
||||||
|
{{ 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/public/file/${component.img[0]}_l.webp')`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
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.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.front_section_lang[0].data.button
|
||||||
|
}}
|
||||||
|
</UiButtonArrow>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
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
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
</script>
|
104
components/section/MainProducts.vue
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<UiContainer class="space-y-[40px] sm:space-y-[55px] md:space-y-[75px]">
|
||||||
|
<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="`/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>
|
||||||
|
</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
|
||||||
|
type="fill"
|
||||||
|
:arrow="true"
|
||||||
|
@click="menuStore.navigateToShop"
|
||||||
|
>
|
||||||
|
{{ $t("eshop") }}
|
||||||
|
</UiButtonArrow>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(itemCount, async () => {
|
||||||
|
await productStore.getList(itemCount.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await updateItemCount()
|
||||||
|
window.addEventListener('resize', updateItemCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', updateItemCount)
|
||||||
|
})
|
||||||
|
</script>
|
65
components/section/Product.vue
Normal file
@ -0,0 +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] 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">
|
||||||
|
{{ product.name }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-[9px] sm:text-[12px] text-sm text-bg-dark">
|
||||||
|
{{ 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">
|
||||||
|
{{ product.formatted_price }}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
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">
|
||||||
|
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()
|
||||||
|
|
||||||
|
function handleImageError(event: Event) {
|
||||||
|
const img = event.target as HTMLImageElement
|
||||||
|
img.src = '/photo.svg'
|
||||||
|
}
|
||||||
|
</script>
|
263
components/section/ProductBlock.vue
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<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"
|
||||||
|
@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="flex w-full gap-1">
|
||||||
|
<div class="w-[50%] min-w-150px">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div
|
||||||
|
v-for="image in product.picture_uuids"
|
||||||
|
:key="image"
|
||||||
|
class="flex justify-center h-20 cursor-pointer"
|
||||||
|
@click="curImage = image"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="`/api/public/file/${image}_m.webp`"
|
||||||
|
:alt="product.name"
|
||||||
|
srcset=""
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="cursor-pointer"
|
||||||
|
@click="showPopup = true"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="`/api/public/file/${curImage}_m.webp`"
|
||||||
|
class="w-72"
|
||||||
|
alt=""
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-[50%]">
|
||||||
|
<h1 class="font-bold text-2xl">
|
||||||
|
{{ product.name }}
|
||||||
|
</h1>
|
||||||
|
<div>
|
||||||
|
<span class="mx-4">Product reference:</span><span>{{ product.reference }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="my-4"
|
||||||
|
v-html="product.description"
|
||||||
|
/>
|
||||||
|
<div class="my-12">
|
||||||
|
<div class="text-2xl font-bold">
|
||||||
|
<span class="mx-4">Price:</span><span>{{ formatedPrice }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<span class="mx-4">Tax information:</span><span>{{ product.tax_name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="font-bold">
|
||||||
|
<span class="mx-4">Buy out price:</span><span>{{ formatedBuyOutPrice }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
:disabled="!product.sale_active"
|
||||||
|
class="btn disabled:bg-gray-500 bg-button text-white px-4 py-4 rounded-2xl cursor-pointer"
|
||||||
|
@click="productStore.incrementCartItem(product.id)"
|
||||||
|
>
|
||||||
|
Add to cart
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="w-1/2 flex cursor-pointer">
|
||||||
|
<div v-for="rel in product.recommendations"
|
||||||
|
:key="rel.id"
|
||||||
|
>
|
||||||
|
<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: rel?.id, name: rel?.link_rewrite },
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="w-1/3">
|
||||||
|
{{ rel.name }}
|
||||||
|
<img
|
||||||
|
:src="`/api/public/file/${rel.cover_picture_uuid}_m.webp`"
|
||||||
|
class="h-24"
|
||||||
|
alt=""
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-1/2">
|
||||||
|
<div
|
||||||
|
v-for="feature in product.features"
|
||||||
|
:key="feature.feature"
|
||||||
|
class="flex gap-4 justify-around border-b"
|
||||||
|
>
|
||||||
|
<span class="mx-4">{{ feature.feature }}:</span>
|
||||||
|
<span class="mx-4">
|
||||||
|
{{ feature.value }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { GenericResponse, ProductItem } from '~/types'
|
||||||
|
|
||||||
|
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 formatedPrice = computed(() => {
|
||||||
|
return formatPrice(product.value.price)
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatedBuyOutPrice = 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>
|
253
components/section/RegistrationMain.vue
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
<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/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("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"
|
||||||
|
@click="menuStore.navigateToItem()"
|
||||||
|
>
|
||||||
|
{{ $t("back_to_home") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-[25px] sm:space-y-[30px]">
|
||||||
|
<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 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"
|
||||||
|
>
|
||||||
|
</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 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 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"
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
/></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-[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.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
|
||||||
|
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.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',
|
||||||
|
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]"
|
||||||
|
>
|
||||||
|
<p class="truncate whitespace-nowrap">
|
||||||
|
{{
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
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>
|
||||||
|
</template>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
</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("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"
|
||||||
|
@click="
|
||||||
|
menuStore.navigateToItem(
|
||||||
|
menuStore.menuItems?.find((item) => item.id === 11),
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ $t("login") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
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 dropCountry = ref()
|
||||||
|
|
||||||
|
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>
|
62
components/section/ResetPasswordMain.vue
Normal file
@ -0,0 +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="{
|
||||||
|
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>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pt-[25px] sm:pt-12 flex justify-center w-full">
|
||||||
|
<UiButtonArrow type="fill"
|
||||||
|
:arrow="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: {
|
||||||
|
reset_password_description: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
480
components/section/ShopMain.vue
Normal file
@ -0,0 +1,480 @@
|
|||||||
|
<template>
|
||||||
|
<!-- <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
|
||||||
|
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>
|
||||||
|
|
||||||
|
<Transition>
|
||||||
|
<div v-if="openCategories" class="min-w-[250px] px-5 sm:p-0 xl:hidden">
|
||||||
|
<h1 class="font-bounded leading-[140%] font-bold text-[24px] mb-[25px]">
|
||||||
|
{{ $t("category") }}
|
||||||
|
</h1>
|
||||||
|
<div class="flex flex-col gap-[25px]">
|
||||||
|
<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" :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',
|
||||||
|
]">
|
||||||
|
<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" /></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}`" 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<!-- categories -->
|
||||||
|
<div class="min-w-[250px] hidden xl:block">
|
||||||
|
<h1 class="font-bounded leading-[140%] font-bold text-[40px] mb-[55px]">
|
||||||
|
{{ $t("category") }}
|
||||||
|
</h1>
|
||||||
|
<div class="flex flex-col gap-12">
|
||||||
|
<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" :active="categoryId" @change-category="changeCategory($event)" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="mb-10 text-2xl font-extrabold text-black dark:text-white">
|
||||||
|
{{ $t("filtered_by") }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- <div v-if="filters.length < 1" class="mb-8 text-white animate-pulse">
|
||||||
|
<div v-for="i in 5"
|
||||||
|
class="mt-10 flex justify-between font-bold text-black cursor-pointer 2xl:pr-24 dark:text-gray">
|
||||||
|
<div class="w-32 h-4 bg-gray-200 rounded"></div>
|
||||||
|
<div class="w-4 h-4 bg-gray-200 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<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" /></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" />
|
||||||
|
<label :for="`${filter.value_id}`" class="cursor-pointer">{{ filter.value }}</label> -->
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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 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.front_section_lang[0].data.title }}
|
||||||
|
</h4>
|
||||||
|
<p>{{ component.front_section_lang[0].data.description }}</p>
|
||||||
|
</div>
|
||||||
|
<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" :key="index" /> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- products -->
|
||||||
|
<div v-else ref="loadingElement" class="flex flex-wrap justify-center gap-5 sm:gap-10">
|
||||||
|
<Product v-for="product in products" :key="product.id" :product="product" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="reachedEnd" class="w-full flex justify-center">
|
||||||
|
<p>
|
||||||
|
{{ $t("FrontTranslations", "You reached end of the list.") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import Product from './Product.vue'
|
||||||
|
import CategoryTree from './CategoryTree.vue'
|
||||||
|
import type {
|
||||||
|
Feature,
|
||||||
|
GenericResponse,
|
||||||
|
GenericResponseChildren,
|
||||||
|
GenericResponseItems,
|
||||||
|
ProductType,
|
||||||
|
} from '~/types'
|
||||||
|
|
||||||
|
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<string[]>([])
|
||||||
|
const categoryId = ref<number>(1)
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const reachedEnd = ref(false)
|
||||||
|
|
||||||
|
const loadingElement = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const page = ref(1)
|
||||||
|
const elems = ref(12)
|
||||||
|
const maxElements = ref(0)
|
||||||
|
|
||||||
|
const products = ref([] as ProductType[])
|
||||||
|
async function getProducts() {
|
||||||
|
try {
|
||||||
|
const { data } = await useMyFetch<GenericResponseItems<ProductType[]>>(
|
||||||
|
`/api/public/products/category/${categoryId.value}?p=${page.value}&elems=${elems.value}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
onErrorOccured: (_, status) => {
|
||||||
|
throw new Error(`HTTP error: ${status}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
products.value = data.items
|
||||||
|
maxElements.value = data.items_count + 1
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('getProducts error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters = ref([] as Feature[])
|
||||||
|
async function getCategory() {
|
||||||
|
try {
|
||||||
|
const { data } = await useMyFetch<GenericResponse<object>>(
|
||||||
|
`/api/public/products/category/1/classification`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
onErrorOccured: (_, status) => {
|
||||||
|
throw new Error(`HTTP error: ${status}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoriesList = ref()
|
||||||
|
async function getCategoryTree() {
|
||||||
|
try {
|
||||||
|
const { data } = await useMyFetch<GenericResponseChildren<ProductType[]>>(
|
||||||
|
`/api/public/categories/tree`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
onErrorOccured: (_, status) => {
|
||||||
|
throw new Error(`HTTP error: ${status}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
categoriesList.value = data.children
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('getCategory error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getProducts()
|
||||||
|
getCategory()
|
||||||
|
getCategoryTree()
|
||||||
|
|
||||||
|
const closeElement = () => {
|
||||||
|
isInfo.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('scroll', scrollEvent)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('scroll', scrollEvent)
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filters.value.forEach((item) => {
|
||||||
|
visibleFeatures[item.feature] = false
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
super.append(name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMoreProducts() {
|
||||||
|
const qParams = new FilteredQueryString()
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await useMyFetch<GenericResponseItems<ProductType[]>>(
|
||||||
|
`/api/public/products/category/${categoryId.value}?${qParams.toString()}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'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[]))
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
reachedEnd.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (products.value.length >= maxElements.value) {
|
||||||
|
reachedEnd.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('getCategory error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CategoryItem {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeCategory = (item: CategoryItem) => {
|
||||||
|
categoryId.value = item.id
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(selectedFilters, async (newQuestion: string) => {
|
||||||
|
if (newQuestion) {
|
||||||
|
page.value = 1
|
||||||
|
reachedEnd.value = false
|
||||||
|
loadingElement.value?.scrollIntoView()
|
||||||
|
|
||||||
|
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 { data } = await useMyFetch<GenericResponseItems<ProductType[]>>(
|
||||||
|
`/api/public/products/category/1?${qParams.toString()}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
onErrorOccured: (_, status) => {
|
||||||
|
throw new Error(`HTTP error: ${status}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
products.value = data.items
|
||||||
|
maxElements.value = data.items_count
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('selectedFilters error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(categoryId, async (newCategoryId) => {
|
||||||
|
if (newCategoryId) {
|
||||||
|
page.value = 1
|
||||||
|
reachedEnd.value = false
|
||||||
|
loadingElement.value?.scrollIntoView()
|
||||||
|
|
||||||
|
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 { data } = await useMyFetch<GenericResponseItems<ProductType[]>>(
|
||||||
|
`api/public/products/category/${newCategoryId}?${qParams.toString()}`,
|
||||||
|
{
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
onErrorOccured: (_, status) => {
|
||||||
|
throw new Error(`HTTP error: ${status}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
products.value = data.items
|
||||||
|
maxElements.value = data.items_count
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('getCategory error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-enter-active,
|
||||||
|
.v-leave-active {
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-enter-from,
|
||||||
|
.v-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,49 +0,0 @@
|
|||||||
<template>
|
|
||||||
<UContainer
|
|
||||||
class="mx-auto w-full max-w-[360px] px-[16px] sm:max-w-[768px] sm:px-[17px] md:max-w-[1000px] md:px-[24px] xl:max-w-[1920px] xl:px-[80px]"
|
|
||||||
>
|
|
||||||
<div class="">
|
|
||||||
<div class="mt-[75px] space-y-[30px] sm:space-y-[53px]">
|
|
||||||
<h1 class="h1">
|
|
||||||
{{ component.section_lang_data.title }}
|
|
||||||
</h1>
|
|
||||||
<div
|
|
||||||
class="hidden h-[509px] w-full rounded-[20px] bg-cover bg-center transition-transform duration-300 group-hover:scale-105 xl:block"
|
|
||||||
:style="{
|
|
||||||
backgroundImage: `url('/header-hero-desc.png')`,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="h-[465px] w-full rounded-[20px] bg-cover bg-center transition-transform duration-300 group-hover:scale-105 sm:h-[509px] xl:hidden"
|
|
||||||
:style="{
|
|
||||||
backgroundImage: `url('/header-hero.png')`,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
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 sm:min-w-[45%]">
|
|
||||||
{{ component.section_lang_data.title_second }}
|
|
||||||
</h3>
|
|
||||||
<div class="flex w-full items-start justify-center sm:justify-end">
|
|
||||||
<ButtonArrow type="fill">{{ component.section_lang_data.button }}</ButtonArrow>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</UContainer>
|
|
||||||
</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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
</script>
|
|
@ -1,28 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="group flex cursor-pointer items-center justify-start gap-2">
|
<div
|
||||||
|
class="group flex cursor-pointer items-center justify-start gap-2 whitespace-nowrap"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
:class="[
|
:class="[
|
||||||
'text-inter h-[40px] cursor-pointer rounded-[10px] px-[22px] text-lg 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'
|
type === 'fill'
|
||||||
? 'bg-button text-text-dark group-hover:bg-button-hover'
|
? 'bg-button text-text-dark group-hover:bg-button-hover'
|
||||||
: 'border-button text-button group-hover:border-button-hover group-hover:text-button-hover border',
|
: 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 />
|
<slot />
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
|
v-if="arrow"
|
||||||
:class="[
|
: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]',
|
'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]',
|
||||||
type === 'fill'
|
type === 'fill'
|
||||||
? 'bg-button text-text-dark group-hover:bg-button-hover'
|
? 'bg-button text-text-dark group-hover:bg-button-hover'
|
||||||
: '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',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<!-- <UIcon
|
|
||||||
class="w-10 h-10 block"
|
|
||||||
name="i-heroicons-arrow-up-right"
|
|
||||||
/> -->
|
|
||||||
<!-- <img src="/icons/ArrowCorner.svg" alt="" class="text-black" /> -->
|
|
||||||
<svg
|
<svg
|
||||||
class=""
|
class=""
|
||||||
width="26"
|
width="26"
|
||||||
@ -45,5 +46,11 @@ defineProps({
|
|||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
});
|
arrow: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
full: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
})
|
||||||
</script>
|
</script>
|
81
components/ui/CheckoutInput.vue
Normal 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>
|
9
components/ui/Container.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<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"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</UContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup></script>
|
37
components/ui/FaceObserver.vue
Normal 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>
|
51
components/ui/ImgWrapper.vue
Normal 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>
|
74
composables/useMyFetch.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { ofetch } from 'ofetch'
|
||||||
|
|
||||||
|
export interface RequestOptions<T> extends RequestInit {
|
||||||
|
onErrorOccured?: (error: Error, statusCode: number) => void
|
||||||
|
onSuccess?: (data: T, statusCode: number) => void
|
||||||
|
onStart?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function useMyFetch
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Makes a request to a given url, handles cookies and errors.
|
||||||
|
*
|
||||||
|
* @param {string} url - The url to make a request to.
|
||||||
|
* @param {RequestOptions} [options] - The options to use for the request.
|
||||||
|
*
|
||||||
|
* @returns {Promise<T | undefined>} - A promise resolving to the response data
|
||||||
|
* or undefined if an error occurred.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { data } = useMyFetch<{ name: string }>('/api/user')
|
||||||
|
*/
|
||||||
|
export const useMyFetch = async <T>(
|
||||||
|
url: string,
|
||||||
|
options?: RequestOptions<T>,
|
||||||
|
): Promise<T> => {
|
||||||
|
if (options?.onStart) options.onStart()
|
||||||
|
let response = null
|
||||||
|
try {
|
||||||
|
const event = useRequestEvent()
|
||||||
|
|
||||||
|
if (options == null) options = {}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle errors if any
|
||||||
|
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, response.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response._data as T
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// handle errors if any
|
||||||
|
if (typeof options?.onErrorOccured == 'function') {
|
||||||
|
options.onErrorOccured(e as Error, response?.status || 500)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
return {} as T
|
||||||
|
}
|
||||||
|
}
|
@ -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
@ -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>
|
@ -1,5 +1,21 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
import withNuxt from "./.nuxt/eslint.config.mjs";
|
import { createConfigForNuxt } from '@nuxt/eslint-config'
|
||||||
|
|
||||||
export default withNuxt();
|
export default createConfigForNuxt({
|
||||||
// Your custom configs here
|
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',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"welcome": "Welcome to Nuxt 3"
|
|
||||||
}
|
|
@ -1,17 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<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 />
|
<HeaderBlock />
|
||||||
<div class="flex-1">
|
<div
|
||||||
|
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 />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<!-- <footer-block /> -->
|
<!-- <FooterBlock /> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
useHead({
|
// import FooterBlock from "~/components/section/FooterBlock.vue";
|
||||||
link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.png" }],
|
// useHead({
|
||||||
});
|
// link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.png" }],
|
||||||
|
// });
|
||||||
</script>
|
</script>
|
@ -1,59 +1,61 @@
|
|||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
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"],
|
modules: [
|
||||||
|
'@pinia/nuxt',
|
||||||
i18n: {
|
'@nuxt/eslint',
|
||||||
locales: [
|
'@nuxt/ui',
|
||||||
{ code: "pl", name: "Polski", icon: "emojione:flag-for-poland" },
|
'@nuxtjs/i18n',
|
||||||
{ code: "en", name: "English", icon: "emojione:flag-for-united-kingdom" },
|
'@pinia/nuxt',
|
||||||
{ code: "cs", name: "Čeština", icon: "emojione:flag-for-chechia" },
|
|
||||||
],
|
],
|
||||||
lazy: true,
|
devtools: { enabled: false },
|
||||||
defaultLocale: "en",
|
css: [
|
||||||
strategy: "prefix",
|
'@/assets/main.css',
|
||||||
bundle: {
|
'vue3-toastify/dist/index.css',
|
||||||
optimizeTranslationDirective: false,
|
'@/assets/toastify-custom.css',
|
||||||
},
|
],
|
||||||
},
|
ui: {},
|
||||||
css: ["@/assets/fonts.css", "@/assets/main.css"],
|
compatibilityDate: '2024-11-01',
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()],
|
plugins: [tailwindcss()],
|
||||||
build: {
|
build: {
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: ["arina.ma-al.pl", "marek.ma-al.pl"],
|
allowedHosts: ['arina.ma-al.pl', 'marek.ma-al.pl'],
|
||||||
watch: {
|
watch: {
|
||||||
ignored: ["**/backend/pb_data/**"],
|
ignored: ['**/backend/pb_data/**'],
|
||||||
|
},
|
||||||
|
hmr: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
clientPort: 3000, // useful if proxying
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
typescript: {
|
typescript: {
|
||||||
tsConfig: {
|
tsConfig: {
|
||||||
compilerOptions: {
|
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: {
|
icon: {
|
||||||
localApiEndpoint: "/___nuxt_icon",
|
localApiEndpoint: '/___nuxt_icon',
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
25
package.json
@ -7,20 +7,23 @@
|
|||||||
"dev": "nuxt dev --host 0.0.0.0",
|
"dev": "nuxt dev --host 0.0.0.0",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview --host 0.0.0.0",
|
"preview": "nuxt preview --host 0.0.0.0",
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare",
|
||||||
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconscout/unicons": "^4.2.0",
|
"@iconscout/unicons": "^4.2.0",
|
||||||
"@nuxt/eslint": "^1.4.1",
|
"@nuxt/eslint": "^1.5.2",
|
||||||
"@nuxt/ui": "^3.1.3",
|
"@nuxt/ui": "^3.2.0",
|
||||||
"@nuxtjs/i18n": "^9.5.4",
|
"@nuxtjs/i18n": "^9.5.6",
|
||||||
"@pinia/nuxt": "^0.11.0",
|
"@pinia/nuxt": "^0.11.1",
|
||||||
"@tailwindcss/vite": "^4.1.8",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"nuxt": "^3.17.4",
|
"@vueuse/core": "^13.5.0",
|
||||||
"pocketbase": "^0.26.0",
|
"gsap": "^3.13.0",
|
||||||
"tailwindcss": "^4.1.7",
|
"nuxt": "^3.17.6",
|
||||||
"vue": "^3.5.14",
|
"tailwindcss": "^4.1.11",
|
||||||
"vue-router": "^4.5.1"
|
"vue": "^3.5.17",
|
||||||
|
"vue-router": "^4.5.1",
|
||||||
|
"vue3-toastify": "^0.2.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxtjs/tailwindcss": "^6.14.0"
|
"@nuxtjs/tailwindcss": "^6.14.0"
|
||||||
|
@ -1,24 +1,77 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
|
||||||
<KeepAlive>
|
<KeepAlive>
|
||||||
<component :is="component.componentInstance" v-for="component in componentsList" :key="component.name" :component="component.component" />
|
<component
|
||||||
|
:is="component.componentInstance"
|
||||||
|
v-for="component in componentsList"
|
||||||
|
:key="component.name"
|
||||||
|
:component="component.component"
|
||||||
|
/>
|
||||||
</KeepAlive>
|
</KeepAlive>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useStore } from "@/stores/store";
|
import { gsap } from 'gsap'
|
||||||
const route = useRoute();
|
import ScrollTrigger from 'gsap/ScrollTrigger'
|
||||||
const store = useStore();
|
|
||||||
const menuStore = useMenuStore();
|
|
||||||
await store.getSections(route.params.id);
|
|
||||||
|
|
||||||
onMounted(() => {
|
gsap.registerPlugin(ScrollTrigger)
|
||||||
menuStore.openMenu = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
useHead(menuStore.headMeta);
|
watch(useColorMode(), (color) => {
|
||||||
|
console.log(color)
|
||||||
|
})
|
||||||
|
|
||||||
const componentsList = await store.getComponents(route.params.id);
|
onMounted(() => {
|
||||||
|
const anim = gsap.fromTo(
|
||||||
|
'h1',
|
||||||
|
{
|
||||||
|
opacity: 0,
|
||||||
|
zoom: 0.95,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
opacity: 1,
|
||||||
|
duration: 1,
|
||||||
|
zoom: 1,
|
||||||
|
ease: 'power2.out',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ScrollTrigger.create({
|
||||||
|
trigger: 'h1',
|
||||||
|
start: 'top 80%',
|
||||||
|
onEnter: () => anim.restart(), // play when scrolling down
|
||||||
|
onEnterBack: () => anim.restart(), // play again when scrolling up
|
||||||
|
})
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
@ -1,5 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
|
||||||
<KeepAlive>
|
<KeepAlive>
|
||||||
<component
|
<component
|
||||||
:is="component.componentInstance"
|
:is="component.componentInstance"
|
||||||
@ -8,25 +7,23 @@
|
|||||||
:component="component.component"
|
:component="component.component"
|
||||||
/>
|
/>
|
||||||
</KeepAlive>
|
</KeepAlive>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
<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
|
route.params.id = menuStore.defaultMenu.id
|
||||||
route.params.slug = menuStore.defaultMenu.link_rewrite
|
route.params.slug = menuStore.defaultMenu.link_rewrite
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore()
|
||||||
await store.getSections(route.params.id);
|
await store.getSections(route.params.id)
|
||||||
|
|
||||||
onMounted(()=> {
|
onMounted(() => {
|
||||||
menuStore.openMenu = false
|
menuStore.openMenu = false
|
||||||
})
|
})
|
||||||
|
|
||||||
useHead(menuStore.headMeta);
|
useHead(menuStore.headMeta)
|
||||||
const componentsList = await store.getComponents(route.params.id);
|
const componentsList = await store.getComponents(route.params.id)
|
||||||
</script>
|
</script>
|
138
plugins/01_i18n.ts
Normal 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
@ -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()
|
||||||
|
})
|
@ -1,32 +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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
});
|
|
@ -1,7 +0,0 @@
|
|||||||
import { defineNuxtPlugin } from "#app";
|
|
||||||
|
|
||||||
export default defineNuxtPlugin(async () => {
|
|
||||||
const menuStore = useMenuStore();
|
|
||||||
await menuStore.loadMenu();
|
|
||||||
await menuStore.loadFooter();
|
|
||||||
});
|
|
10
plugins/vue3-toastify.client.ts
Normal 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 },
|
||||||
|
}
|
||||||
|
})
|
11891
pnpm-lock.yaml
generated
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 829 KiB |
Before Width: | Height: | Size: 496 KiB |
BIN
public/lei_certificate_aurrie.pdf
Normal file
Before Width: | Height: | Size: 1008 B |
14
public/logo-dark.svg
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<svg width="95" height="48" viewBox="0 0 95 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2.3 35L4.57999 39.6917L6.70002 35H9L5.60002 42.1549V48H3.46V42.1155L0 35H2.3Z" fill="#FFFEFB"/>
|
||||||
|
<path d="M24 41.5001C24 43.2642 23.2178 45.0099 21.8954 46.2542C20.6848 47.4057 19.1389 48 17.3696 48C13.7937 48 11 45.1584 11 41.463C11 39.8657 11.6705 38.2131 12.8439 36.95C13.9799 35.7243 15.6748 35 17.4441 35C21.0387 35 24 37.9344 24 41.5001ZM13.1605 41.5001C13.1605 42.9672 13.6447 44.1 14.6318 44.9542C15.4327 45.6599 16.4756 46.05 17.4814 46.05C19.9399 46.05 21.8209 44.0813 21.8209 41.5371C21.8209 38.9928 19.9212 36.95 17.5 36.95C15.0788 36.95 13.1605 38.9184 13.1605 41.5001Z" fill="#FFFEFB"/>
|
||||||
|
<path d="M28.0385 35V43.4813C28.0385 44.3623 28.0962 44.7645 28.2693 45.0899C28.5769 45.7214 29.1923 46.0661 29.9615 46.0661C30.8077 46.0661 31.4423 45.6639 31.7308 44.9364C31.9231 44.4579 31.9423 44.2474 31.9423 43.0029V35H34V43.2516C34 44.5154 33.9038 45.1855 33.6154 45.7788C32.9807 47.1576 31.6153 48 30.0193 48C28.4231 48 27 47.1001 26.3654 45.6832C26.077 45.0706 26 44.4961 26 43.2134V35H28.0385Z" fill="#FFFEFB"/>
|
||||||
|
<path d="M38.6855 35C39.8134 35 40.6011 35.1747 41.2278 35.5627C42.3197 36.2224 42.9465 37.4062 42.9465 38.803C42.9465 40.6269 41.9261 42.0625 40.2072 42.7224L43 46.8552L41.4426 48L37.9335 42.7998V47.8836H36V35H38.6855ZM38.6855 40.918C39.33 40.918 39.5269 40.8985 39.7954 40.7821C40.4757 40.491 40.8875 39.7341 40.8875 38.7835C40.8875 37.6 40.1714 36.9015 38.954 36.9015H37.9514V40.8985H38.6855V40.918Z" fill="#FFFEFB"/>
|
||||||
|
<path d="M61 40.6995V46.4749C59.6786 47.4852 58.2016 48 56.608 48C52.915 48 50 45.1215 50 41.4618C50 37.8021 52.8764 35 56.5495 35C57.7352 35 58.9981 35.3241 59.9507 35.8578L59.5425 37.8593C58.7069 37.3446 57.3851 36.9824 56.3358 36.9824C54.0618 36.9824 52.2736 39.06 52.2736 41.6142C52.2736 44.1683 54.1394 46.151 56.5495 46.151C57.5211 46.151 58.4928 45.903 58.9596 45.5218V42.5485H55.772L56.1802 40.6995H61Z" fill="#FFFEFB"/>
|
||||||
|
<path d="M76 41.5001C76 43.2642 75.2175 45.0099 73.8952 46.2542C72.6849 47.4057 71.1391 48 69.3694 48C65.7939 48 63 45.1584 63 41.463C63 39.8657 63.6707 38.2131 64.844 36.95C65.9799 35.7243 67.6749 35 69.4445 35C73.0387 35 76 37.9344 76 41.5001ZM65.1605 41.5001C65.1605 42.9672 65.6448 44.1 66.632 44.9542C67.4327 45.6599 68.476 46.05 69.4815 46.05C71.9398 46.05 73.8208 44.0813 73.8208 41.5371C73.8208 38.9928 71.9211 36.95 69.5002 36.95C67.0789 36.95 65.1605 38.9184 65.1605 41.5001Z" fill="#FFFEFB"/>
|
||||||
|
<path d="M80.1695 35V46.084H84V48H78V35.0196H80.1695V35Z" fill="#FFFEFB"/>
|
||||||
|
<path d="M88.7465 35C89.8224 35 90.6202 35.1568 91.4181 35.549C93.5711 36.5882 95 39 95 41.6076C95 44.2157 93.478 46.7056 91.1586 47.5684C90.3235 47.8823 89.5998 48 88.5979 48H86V35.0195H88.7465V35ZM88.4121 46.0391C89.08 46.0391 89.5257 45.9803 89.9896 45.8235C91.7524 45.255 92.9214 43.5294 92.9214 41.5488C92.9214 39.4119 91.5852 37.6077 89.6738 37.098C89.2286 36.9804 88.9136 36.9412 88.2636 36.9412H87.9855L87.9669 46.0391H88.4121Z" fill="#FFFEFB"/>
|
||||||
|
<path d="M60 0H53V25H60V0Z" fill="#008567"/>
|
||||||
|
<path d="M50 2H43V25H50V2Z" fill="#D3E0DE"/>
|
||||||
|
<path d="M40 5H33V25H40V5Z" fill="#E8E7E0"/>
|
||||||
|
<path d="M76 29H19V30H76V29Z" fill="#FFFEFB"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
10
public/logo-footer-dark.svg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<svg width="596" height="94" viewBox="0 0 596 94" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12.2667 1.5L24.4266 27.124L35.7334 1.5H48L29.8668 40.5766V72.5H18.4533V40.3615L0 1.5H12.2667Z" fill="#FFFEFB"/>
|
||||||
|
<path d="M142 37.0003C142 47.0421 137.547 56.9797 130.02 64.0624C123.129 70.6173 114.329 74 104.258 74C83.9024 74 68 57.8248 68 36.7896C68 27.6972 71.8164 18.2898 78.4958 11.0999C84.9627 4.12274 94.6103 0 104.682 0C125.143 0 142 16.7038 142 37.0003ZM80.298 37.0003C80.298 45.3519 83.0546 51.8002 88.6732 56.6625C93.232 60.6797 99.1691 62.9001 104.894 62.9001C118.888 62.9001 129.596 51.6937 129.596 37.211C129.596 22.7284 118.782 11.0999 105 11.0999C91.2178 11.0999 80.298 22.3047 80.298 37.0003Z" fill="#FFFEFB"/>
|
||||||
|
<path d="M173.212 1V47.9736C173.212 52.853 173.529 55.0802 174.481 56.8824C176.173 60.38 179.558 62.289 183.788 62.289C188.442 62.289 191.933 60.0618 193.519 56.0324C194.577 53.3825 194.683 52.2166 194.683 45.3237V1H206V46.7009C206 53.7007 205.471 57.4119 203.885 60.6981C200.394 68.3342 192.884 73 184.106 73C175.327 73 167.5 68.0161 164.01 60.1686C162.423 56.7756 162 53.5938 162 46.4896V1H173.212Z" fill="#FFFEFB"/>
|
||||||
|
<path d="M242.113 1.5C248.88 1.5 253.607 2.45412 257.367 4.57337C263.918 8.17616 267.679 14.6413 267.679 22.2703C267.679 32.2317 261.556 40.0719 251.243 43.6763L268 66.2474L258.656 72.5L237.601 44.0987V71.8641H226V1.5H242.113ZM242.113 33.8216C245.98 33.8216 247.162 33.7148 248.773 33.0789C252.854 31.4891 255.325 27.3555 255.325 22.1635C255.325 15.6997 251.028 11.8852 243.724 11.8852H237.708V33.7148H242.113V33.8216Z" fill="#FFFEFB"/>
|
||||||
|
<path d="M378 32.5663V64.5535C370.792 70.149 362.736 73 354.043 73C333.9 73 318 57.0573 318 36.7884C318 16.5196 333.69 1 353.724 1C360.192 1 367.081 2.79497 372.277 5.7509L370.05 16.8363C365.492 13.9854 358.282 11.9795 352.559 11.9795C340.155 11.9795 330.401 23.4863 330.401 37.6324C330.401 51.7785 340.578 62.7592 353.724 62.7592C359.024 62.7592 364.324 61.3858 366.87 59.2747V42.8071H349.483L351.71 32.5663H378Z" fill="#9A7F62"/>
|
||||||
|
<path d="M472 37.0003C472 47.0421 467.546 56.9797 460.019 64.0624C453.129 70.6173 444.33 74 434.257 74C413.904 74 398 57.8248 398 36.7896C398 27.6972 401.818 18.2898 408.497 11.0999C414.963 4.12274 424.611 0 434.684 0C455.144 0 472 16.7038 472 37.0003ZM410.298 37.0003C410.298 45.3519 413.055 51.8002 418.674 56.6625C423.232 60.6797 429.171 62.9001 434.895 62.9001C448.888 62.9001 459.595 51.6937 459.595 37.211C459.595 22.7284 448.782 11.0999 435.001 11.0999C421.218 11.0999 410.298 22.3047 410.298 37.0003Z" fill="#9A7F62"/>
|
||||||
|
<path d="M503.571 1.5V62.0358H524V72.5H492V1.60709H503.571V1.5Z" fill="#9A7F62"/>
|
||||||
|
<path d="M559.868 1.5C566.085 1.5 570.695 2.35658 575.304 4.49821C587.744 10.1738 596 23.3463 596 37.5879C596 51.8318 587.206 65.4308 573.805 70.1428C568.98 71.8574 564.799 72.5 559.01 72.5H544V1.60673H559.868V1.5ZM557.937 61.7907C561.796 61.7907 564.371 61.4693 567.051 60.6132C577.236 57.5078 583.99 48.0838 583.99 37.2666C583.99 25.5956 576.27 15.7423 565.227 12.9583C562.654 12.3158 560.834 12.1017 557.078 12.1017H555.472L555.364 61.7907H557.937Z" fill="#9A7F62"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
10
public/logo-footer.svg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<svg width="596" height="94" viewBox="0 0 596 94" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12.2667 1.5L24.4266 27.124L35.7334 1.5H48L29.8668 40.5766V72.5H18.4533V40.3615L0 1.5H12.2667Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M142 37.0003C142 47.0421 137.547 56.9797 130.02 64.0624C123.129 70.6173 114.329 74 104.258 74C83.9024 74 68 57.8248 68 36.7896C68 27.6972 71.8164 18.2898 78.4958 11.0999C84.9627 4.12274 94.6103 0 104.682 0C125.143 0 142 16.7038 142 37.0003ZM80.298 37.0003C80.298 45.3519 83.0546 51.8002 88.6732 56.6625C93.232 60.6797 99.1691 62.9001 104.894 62.9001C118.888 62.9001 129.596 51.6937 129.596 37.211C129.596 22.7284 118.782 11.0999 105 11.0999C91.2178 11.0999 80.298 22.3047 80.298 37.0003Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M173.212 1V47.9736C173.212 52.853 173.529 55.0802 174.481 56.8824C176.173 60.38 179.558 62.289 183.788 62.289C188.442 62.289 191.933 60.0618 193.519 56.0324C194.577 53.3825 194.683 52.2166 194.683 45.3237V1H206V46.7009C206 53.7007 205.471 57.4119 203.885 60.6981C200.394 68.3342 192.884 73 184.106 73C175.327 73 167.5 68.0161 164.01 60.1686C162.423 56.7756 162 53.5938 162 46.4896V1H173.212Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M242.113 1.5C248.88 1.5 253.607 2.45412 257.367 4.57337C263.918 8.17616 267.679 14.6413 267.679 22.2703C267.679 32.2317 261.556 40.0719 251.243 43.6763L268 66.2474L258.656 72.5L237.601 44.0987V71.8641H226V1.5H242.113ZM242.113 33.8216C245.98 33.8216 247.162 33.7148 248.773 33.0789C252.854 31.4891 255.325 27.3555 255.325 22.1635C255.325 15.6997 251.028 11.8852 243.724 11.8852H237.708V33.7148H242.113V33.8216Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M378 32.5663V64.5535C370.792 70.149 362.736 73 354.043 73C333.9 73 318 57.0573 318 36.7884C318 16.5196 333.69 1 353.724 1C360.192 1 367.081 2.79497 372.277 5.7509L370.05 16.8363C365.492 13.9854 358.282 11.9795 352.559 11.9795C340.155 11.9795 330.401 23.4863 330.401 37.6324C330.401 51.7785 340.578 62.7592 353.724 62.7592C359.024 62.7592 364.324 61.3858 366.87 59.2747V42.8071H349.483L351.71 32.5663H378Z" fill="#9A7F62"/>
|
||||||
|
<path d="M472 37.0003C472 47.0421 467.546 56.9797 460.019 64.0624C453.129 70.6173 444.33 74 434.257 74C413.904 74 398 57.8248 398 36.7896C398 27.6972 401.818 18.2898 408.497 11.0999C414.963 4.12274 424.611 0 434.684 0C455.144 0 472 16.7038 472 37.0003ZM410.298 37.0003C410.298 45.3519 413.055 51.8002 418.674 56.6625C423.232 60.6797 429.171 62.9001 434.895 62.9001C448.888 62.9001 459.595 51.6937 459.595 37.211C459.595 22.7284 448.782 11.0999 435.001 11.0999C421.218 11.0999 410.298 22.3047 410.298 37.0003Z" fill="#9A7F62"/>
|
||||||
|
<path d="M503.571 1.5V62.0358H524V72.5H492V1.60709H503.571V1.5Z" fill="#9A7F62"/>
|
||||||
|
<path d="M559.868 1.5C566.085 1.5 570.695 2.35658 575.304 4.49821C587.744 10.1738 596 23.3463 596 37.5879C596 51.8318 587.206 65.4308 573.805 70.1428C568.98 71.8574 564.799 72.5 559.01 72.5H544V1.60673H559.868V1.5ZM557.937 61.7907C561.796 61.7907 564.371 61.4693 567.051 60.6132C577.236 57.5078 583.99 48.0838 583.99 37.2666C583.99 25.5956 576.27 15.7423 565.227 12.9583C562.654 12.3158 560.834 12.1017 557.078 12.1017H555.472L555.364 61.7907H557.937Z" fill="#9A7F62"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
BIN
public/logo.png
Before Width: | Height: | Size: 1.3 KiB |
14
public/logo.svg
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<svg width="95" height="48" viewBox="0 0 95 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2.3 35L4.57999 39.6917L6.70002 35H9L5.60002 42.1549V48H3.46V42.1155L0 35H2.3Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M24 41.5001C24 43.2642 23.2178 45.0099 21.8954 46.2542C20.6848 47.4057 19.1389 48 17.3696 48C13.7937 48 11 45.1584 11 41.463C11 39.8657 11.6705 38.2131 12.8439 36.95C13.9799 35.7243 15.6748 35 17.4441 35C21.0387 35 24 37.9344 24 41.5001ZM13.1605 41.5001C13.1605 42.9672 13.6447 44.1 14.6318 44.9542C15.4327 45.6599 16.4756 46.05 17.4814 46.05C19.9399 46.05 21.8209 44.0813 21.8209 41.5371C21.8209 38.9928 19.9212 36.95 17.5 36.95C15.0788 36.95 13.1605 38.9184 13.1605 41.5001Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M28.0385 35V43.4813C28.0385 44.3623 28.0962 44.7645 28.2693 45.0899C28.5769 45.7214 29.1923 46.0661 29.9615 46.0661C30.8077 46.0661 31.4423 45.6639 31.7308 44.9364C31.9231 44.4579 31.9423 44.2474 31.9423 43.0029V35H34V43.2516C34 44.5154 33.9038 45.1855 33.6154 45.7788C32.9807 47.1576 31.6153 48 30.0193 48C28.4231 48 27 47.1001 26.3654 45.6832C26.077 45.0706 26 44.4961 26 43.2134V35H28.0385Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M38.6855 35C39.8134 35 40.6011 35.1747 41.2278 35.5627C42.3197 36.2224 42.9465 37.4062 42.9465 38.803C42.9465 40.6269 41.9261 42.0625 40.2072 42.7224L43 46.8552L41.4426 48L37.9335 42.7998V47.8836H36V35H38.6855ZM38.6855 40.918C39.33 40.918 39.5269 40.8985 39.7954 40.7821C40.4757 40.491 40.8875 39.7341 40.8875 38.7835C40.8875 37.6 40.1714 36.9015 38.954 36.9015H37.9514V40.8985H38.6855V40.918Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M61 40.6995V46.4749C59.6786 47.4852 58.2016 48 56.608 48C52.915 48 50 45.1215 50 41.4618C50 37.8021 52.8764 35 56.5495 35C57.7352 35 58.9981 35.3241 59.9507 35.8578L59.5425 37.8593C58.7069 37.3446 57.3851 36.9824 56.3358 36.9824C54.0618 36.9824 52.2736 39.06 52.2736 41.6142C52.2736 44.1683 54.1394 46.151 56.5495 46.151C57.5211 46.151 58.4928 45.903 58.9596 45.5218V42.5485H55.772L56.1802 40.6995H61Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M76 41.5001C76 43.2642 75.2175 45.0099 73.8952 46.2542C72.6849 47.4057 71.1391 48 69.3694 48C65.7939 48 63 45.1584 63 41.463C63 39.8657 63.6707 38.2131 64.844 36.95C65.9799 35.7243 67.6749 35 69.4445 35C73.0387 35 76 37.9344 76 41.5001ZM65.1605 41.5001C65.1605 42.9672 65.6448 44.1 66.632 44.9542C67.4327 45.6599 68.476 46.05 69.4815 46.05C71.9398 46.05 73.8208 44.0813 73.8208 41.5371C73.8208 38.9928 71.9211 36.95 69.5002 36.95C67.0789 36.95 65.1605 38.9184 65.1605 41.5001Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M80.1695 35V46.084H84V48H78V35.0196H80.1695V35Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M88.7465 35C89.8224 35 90.6202 35.1568 91.4181 35.549C93.5711 36.5882 95 39 95 41.6076C95 44.2157 93.478 46.7056 91.1586 47.5684C90.3235 47.8823 89.5998 48 88.5979 48H86V35.0195H88.7465V35ZM88.4121 46.0391C89.08 46.0391 89.5257 45.9803 89.9896 45.8235C91.7524 45.255 92.9214 43.5294 92.9214 41.5488C92.9214 39.4119 91.5852 37.6077 89.6738 37.098C89.2286 36.9804 88.9136 36.9412 88.2636 36.9412H87.9855L87.9669 46.0391H88.4121Z" fill="#1A1A1A"/>
|
||||||
|
<path d="M60 0H53V25H60V0Z" fill="#004F3D"/>
|
||||||
|
<path d="M50 2H43V25H50V2Z" fill="#D3E0DE"/>
|
||||||
|
<path d="M40 5H33V25H40V5Z" fill="#E8E7E0"/>
|
||||||
|
<path d="M76 29H19V30H76V29Z" fill="#1A1A1A"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
3
public/photo.svg
Normal 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 |
@ -1,2 +0,0 @@
|
|||||||
User-Agent: *
|
|
||||||
Disallow:
|
|
581
stores/checkoutStore.ts
Normal file
@ -0,0 +1,581 @@
|
|||||||
|
import { validation } from '../utils/validation'
|
||||||
|
import { REGEX_PHONE } from '../utils/regex'
|
||||||
|
import type { GenericResponse, GenericResponseItems, UserCart } from '~/types'
|
||||||
|
import type {
|
||||||
|
AddressesList,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Address {
|
||||||
|
is_default: boolean | string
|
||||||
|
country_iso: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultAddress = ref<Address | 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,
|
||||||
|
}
|
||||||
|
})
|
@ -1,192 +1,269 @@
|
|||||||
import { usePB } from "~/composables/usePB";
|
import { useStore } from './store'
|
||||||
import type {
|
import type {
|
||||||
FooterListResponse,
|
Country,
|
||||||
MenuListResponse,
|
Currency,
|
||||||
PBFooterItem,
|
FrontMenu,
|
||||||
PBMenuItem,
|
GenericResponse,
|
||||||
UIMenuItem,
|
GenericResponseItems,
|
||||||
} from "~/types";
|
Language,
|
||||||
import { useStore } from "./store";
|
UIFrontMenu,
|
||||||
import { ref, watch } from "vue";
|
} from '~/types'
|
||||||
|
import { useMyFetch } from '#imports'
|
||||||
|
|
||||||
function buildTreeRecursive(
|
function buildTreeRecursive(
|
||||||
data: (PBMenuItem | UIMenuItem)[],
|
data: (FrontMenu | UIFrontMenu)[],
|
||||||
parentId: string
|
parentId: number,
|
||||||
): UIMenuItem[] {
|
): UIFrontMenu[] {
|
||||||
const children = data.filter(
|
const children = data.filter(
|
||||||
(item): item is UIMenuItem =>
|
(item): item is UIFrontMenu =>
|
||||||
item.id_parent === parentId && !item.is_default
|
item.id_parent === parentId && !item.is_default,
|
||||||
);
|
)
|
||||||
|
|
||||||
return children.map((item) => ({
|
return children.map(item => ({
|
||||||
...item,
|
...item,
|
||||||
children: buildTreeRecursive(data, item.id),
|
children: buildTreeRecursive(data, item.id),
|
||||||
}));
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMenuStore = defineStore("menuStore", () => {
|
export const useMenuStore = defineStore('menuStore', () => {
|
||||||
const pb = usePB();
|
const store = useStore()
|
||||||
const store = useStore();
|
const { $i18n } = useNuxtApp()
|
||||||
const { $i18n } = useNuxtApp();
|
// const session = useSession();
|
||||||
const router = useRouter();
|
const { $session } = useNuxtApp()
|
||||||
const openMenu = ref(false);
|
const router = useRouter()
|
||||||
const openDropDown = ref(false);
|
const route = useRoute()
|
||||||
|
|
||||||
const defaultMenu = ref();
|
const openMenu = ref(false)
|
||||||
const menu = ref<UIMenuItem[]>([]);
|
const openDropDown = ref(false)
|
||||||
const menuItems = ref<MenuListResponse>();
|
|
||||||
const footerItems = ref<FooterListResponse>();
|
const defaultMenu = ref()
|
||||||
|
|
||||||
|
const menu = ref([] as UIFrontMenu[])
|
||||||
|
const menuItems = ref([] as FrontMenu[])
|
||||||
|
|
||||||
|
// curr/country
|
||||||
|
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 () => {
|
const loadMenu = async () => {
|
||||||
try {
|
try {
|
||||||
menuItems.value = (await pb
|
const { data } = await useMyFetch<GenericResponse<FrontMenu[]>>(
|
||||||
.collection("menu_view")
|
`/api/public/front/menu`,
|
||||||
.getList<PBMenuItem>(1, 50, {
|
{
|
||||||
filter: `id_lang="${$i18n.locale.value}"&&active=true`,
|
onErrorOccured: (err, status) => {
|
||||||
sort: "position_id",
|
console.log(err, status)
|
||||||
})) as MenuListResponse;
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const root = menuItems.value.items.find((item) => item.is_root);
|
menuItems.value = data
|
||||||
defaultMenu.value = menuItems.value.items.find((item) => item.is_default);
|
|
||||||
|
|
||||||
|
const root = data.find(item => item.is_root) as UIFrontMenu
|
||||||
|
defaultMenu.value = data.find(item => item.is_default)
|
||||||
if (root) {
|
if (root) {
|
||||||
menu.value = buildTreeRecursive(menuItems.value.items, root.id);
|
menu.value = buildTreeRecursive(data, root.id)
|
||||||
store.currentPageID = menu.value[0]?.id_page || "";
|
}
|
||||||
} else {
|
else {
|
||||||
console.warn("Root menu item not found");
|
console.warn('Root menu item not found')
|
||||||
menu.value = [];
|
menu.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const loadFooter = async () => {
|
const navigateToItem = (item?: UIFrontMenu) => {
|
||||||
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 navigateToItem = (item?: UIMenuItem) => {
|
|
||||||
if (item) {
|
if (item) {
|
||||||
router.push({
|
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}`,
|
name: `id-slug___${$i18n.locale.value}`,
|
||||||
});
|
})
|
||||||
openDropDown.value = false;
|
openDropDown.value = false
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
router.push({
|
router.push({
|
||||||
params: {
|
params: {
|
||||||
slug: defaultMenu.value.link_rewrite,
|
slug: defaultMenu.value.front_menu_lang[0].link_rewrite,
|
||||||
id: defaultMenu.value.id_page,
|
id: defaultMenu.value.id,
|
||||||
},
|
},
|
||||||
name: `id-slug___${$i18n.locale.value}`,
|
name: `id-slug___${$i18n.locale.value}`,
|
||||||
});
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
watch($i18n.locale, async () => {
|
function navigateToShop() {
|
||||||
await loadMenu();
|
navigateToItem(menuItems.value?.find(item => item.id === 5))
|
||||||
await loadFooter();
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const getFirstImage = () => {
|
function getProductMenu() {
|
||||||
const req = useRequestEvent();
|
return menuItems.value?.find(item => item.id === 14)
|
||||||
const url = useRequestURL();
|
}
|
||||||
let img = "";
|
|
||||||
|
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) {
|
for (const s in store.components) {
|
||||||
store.components[s].section_img.map((item) => {
|
if (store.components[s].front_section.img.length === 0) continue
|
||||||
img = `${req?.headers.get("x-forwarded-proto") || url.protocol}://${
|
img.push(
|
||||||
req?.headers.get("x-forwarded-host") || req?.headers.get("host")
|
`/api/public/file/${store.components[s].front_section.img[0]}_${size}.webp`,
|
||||||
}/api/files/${store.components[s].image_collection}/${
|
)
|
||||||
store.components[s].section_id
|
if (img.length > 0) break
|
||||||
}/${item}?thumb=400x0`;
|
}
|
||||||
});
|
if (img.length > 0) {
|
||||||
if (img.length > 0) return img;
|
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 route = useRoute();
|
|
||||||
const headMeta = computed(() => {
|
const headMeta = computed(() => {
|
||||||
const item = menuItems.value?.items.find(
|
const item = menuItems.value?.find(
|
||||||
(item) => item.id_page === route.params.id
|
item => item.id.toString() === route.params.id,
|
||||||
);
|
)
|
||||||
return {
|
|
||||||
title: item?.meta_title,
|
const meta = {
|
||||||
|
title: item?.front_menu_lang[0].meta_title,
|
||||||
htmlAttrs: {
|
htmlAttrs: {
|
||||||
lang: $i18n.locale.value,
|
lang: $i18n.locale.value,
|
||||||
},
|
},
|
||||||
link: [{ rel: "manifest", href: "/api/manifest.json" }],
|
link: [
|
||||||
meta: [
|
// { rel: "manifest", href: "/api/manifest.json" }
|
||||||
|
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.png' },
|
||||||
|
],
|
||||||
|
script: [
|
||||||
{
|
{
|
||||||
hid: "description",
|
src: 'https://leiadmin.com/leitag.js?lei=894500UT83EISNNA8D04&color=dark',
|
||||||
name: "description",
|
defer: true,
|
||||||
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(),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
meta: [
|
||||||
});
|
{
|
||||||
|
hid: 'description',
|
||||||
function redirectToPage(link_rewrite: string) {
|
name: 'description',
|
||||||
const page = menuItems.value?.items.find(
|
content: item?.front_menu_lang[0].meta_description,
|
||||||
(item) => item.link_rewrite === link_rewrite
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!page?.id_page || !page?.link_rewrite) {
|
|
||||||
console.warn(`Page not found or missing data for name: ${link_rewrite}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
router.push({
|
|
||||||
params: {
|
|
||||||
id: page?.id_page,
|
|
||||||
slug: page?.link_rewrite,
|
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const preload = getFirstImage('l', false)
|
||||||
|
if (preload) {
|
||||||
|
meta.link.push({ rel: 'preload', as: 'image', href: preload } as never)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
menu,
|
menu,
|
||||||
menuItems,
|
menuItems,
|
||||||
footerItems,
|
|
||||||
openMenu,
|
openMenu,
|
||||||
openDropDown,
|
openDropDown,
|
||||||
loadMenu,
|
currencies,
|
||||||
loadFooter,
|
languages,
|
||||||
navigateToItem,
|
countries,
|
||||||
redirectToPage,
|
selectedCountry,
|
||||||
|
selectedCurrency,
|
||||||
|
selectedPhoneCountry,
|
||||||
|
selectedLanguage,
|
||||||
defaultMenu,
|
defaultMenu,
|
||||||
headMeta,
|
headMeta,
|
||||||
};
|
|
||||||
});
|
navigateToShop,
|
||||||
|
loadMenu,
|
||||||
|
navigateToItem,
|
||||||
|
getLocales,
|
||||||
|
getProductMenu,
|
||||||
|
formatPrice,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
188
stores/productStore.ts
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
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 { data } = await useMyFetch<GenericResponseItems<[]>>(
|
||||||
|
`/api/public/products/category/${categoryId}?p=1&elems=${count}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
onErrorOccured: async (_, status) => {
|
||||||
|
// await navigateTo("/error", { replace: true });
|
||||||
|
throw createError({
|
||||||
|
statusCode: status,
|
||||||
|
statusMessage: `HTTP error: ${status}`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
productList.value = data.items
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('getList error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getModules() {
|
||||||
|
try {
|
||||||
|
const { data } = await useMyFetch<GenericResponseChildren<[]>>(
|
||||||
|
`/api/public/module/e_shop`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
onErrorOccured: (_, status) => {
|
||||||
|
throw new Error(`HTTP error: ${status}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
modules.value = data.children.find(
|
||||||
|
(item: { id: number, name: string }) =>
|
||||||
|
item.name === 'currency_rates_bar',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('getList error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function incrementCartItem(id: number) {
|
||||||
|
try {
|
||||||
|
const res = await useMyFetch<GenericResponse<object>>(
|
||||||
|
`/api/public/user/cart/item/add/${id}/1`,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
onErrorOccured: (_, status) => {
|
||||||
|
throw new Error(`HTTP error: ${status}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
$toast.success('Item successfully added to your cart.', {
|
||||||
|
autoClose: 5000,
|
||||||
|
dangerouslyHTMLString: true,
|
||||||
|
})
|
||||||
|
await checkoutStore.getUserCart()
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
incrementCartItem,
|
||||||
|
decrementCartItem,
|
||||||
|
deleteCartItem,
|
||||||
|
}
|
||||||
|
})
|