first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 22:18:17 +03:00
commit 7b2b27a42c
1660 changed files with 123050 additions and 0 deletions

10
app/app.vue Normal file
View File

@@ -0,0 +1,10 @@
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
<script lang="ts" setup>
// Canvas theme: body.stretched required for full-width layout
useHead({ bodyAttrs: { class: 'stretched' } })
</script>

View File

@@ -0,0 +1,248 @@
<template>
<footer id="footer" class="dark">
<div class="container">
<!-- Footer Widgets
============================================= -->
<div class="footer-widgets-wrap">
<div class="row col-mb-50">
<div class="col-lg-8">
<div class="row col-mb-50">
<div class="col-md-4">
<div class="widget">
<img src="/images/footer-widget-logo.png" alt="Image" class="footer-logo">
<p>We believe in <strong>Simple</strong>, <strong>Creative</strong> &amp; <strong>Flexible</strong> Design Standards.</p>
<div style="background: url('/images/world-map.png') no-repeat center center; background-size: 100%;">
<address>
<strong>Headquarters:</strong><br>
795 Folsom Ave, Suite 600<br>
San Francisco, CA 94107<br>
</address>
<abbr title="Phone Number"><strong>Phone:</strong></abbr> (1) 8547 632521<br>
<abbr title="Fax"><strong>Fax:</strong></abbr> (1) 11 4752 1433<br>
<abbr title="Email Address"><strong>Email:</strong></abbr> info@canvas.com
</div>
</div>
</div>
<div class="col-md-4">
<div class="widget widget_links">
<h4>Blogroll</h4>
<ul>
<li><a href="https://codex.wordpress.org/">Documentation</a></li>
<li><a href="https://wordpress.org/support/forum/requests-and-feedback">Feedback</a></li>
<li><a href="https://wordpress.org/extend/plugins/">Plugins</a></li>
<li><a href="https://wordpress.org/support/">Support Forums</a></li>
<li><a href="https://wordpress.org/extend/themes/">Themes</a></li>
<li><a href="https://wordpress.org/news/">Canvas Blog</a></li>
<li><a href="https://planet.wordpress.org/">Canvas Planet</a></li>
</ul>
</div>
</div>
<div class="col-md-4">
<div class="widget">
<h4>Recent Posts</h4>
<div id="post-list-footer" class="posts-sm row col-mb-30">
<div class="entry col-12">
<div class="grid-inner row">
<div class="col">
<div class="entry-title">
<h4><a href="#">Lorem ipsum dolor sit amet, consectetur</a></h4>
</div>
<div class="entry-meta">
<ul>
<li>10th July 2021</li>
</ul>
</div>
</div>
</div>
</div>
<div class="entry col-12">
<div class="grid-inner row">
<div class="col">
<div class="entry-title">
<h4><a href="#">Elit Assumenda vel amet dolorum quasi</a></h4>
</div>
<div class="entry-meta">
<ul>
<li>10th July 2021</li>
</ul>
</div>
</div>
</div>
</div>
<div class="entry col-12">
<div class="grid-inner row">
<div class="col">
<div class="entry-title">
<h4><a href="#">Debitis nihil placeat, illum est nisi</a></h4>
</div>
<div class="entry-meta">
<ul>
<li>10th July 2021</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="row col-mb-50">
<div class="col-md-4 col-lg-12">
<div class="widget">
<div class="row col-mb-30">
<div class="col-lg-6">
<div class="counter counter-small"><span data-from="50" data-to="15065421" data-refresh-interval="80" data-speed="3000" data-comma="true"/></div>
<h5 class="mb-0">Total Downloads</h5>
</div>
<div class="col-lg-6">
<div class="counter counter-small"><span data-from="100" data-to="18465" data-refresh-interval="50" data-speed="2000" data-comma="true"/></div>
<h5 class="mb-0">Clients</h5>
</div>
</div>
</div>
</div>
<div class="col-md-5 col-lg-12">
<div class="widget subscribe-widget">
<h5><strong>Subscribe</strong> to Our Newsletter to get Important News, Amazing Offers &amp; Inside Scoops:</h5>
<div class="widget-subscribe-form-result"/>
<form id="widget-subscribe-form" action="include/subscribe.php" method="post" class="mb-0">
<div class="input-group mx-auto">
<div class="input-group-text"><i class="bi-envelope-plus"/></div>
<input id="widget-subscribe-form-email" type="email" name="widget-subscribe-form-email" class="form-control required email" placeholder="Enter your Email">
<button class="btn btn-success" type="submit">Subscribe</button>
</div>
</form>
</div>
</div>
<div class="col-md-3 col-lg-12">
<div class="widget">
<div class="row col-mb-30">
<div class="col-6 col-md-12 col-lg-6 d-flex align-items-center">
<a href="#" class="social-icon text-white border-transparent bg-facebook me-2 mb-0 float-none">
<i class="fa-brands fa-facebook-f"/>
<i class="fa-brands fa-facebook-f"/>
</a>
<a href="#" class="ms-1"><small class="d-block"><strong>Like Us</strong><br>on Facebook</small></a>
</div>
<div class="col-6 col-md-12 col-lg-6 d-flex align-items-center">
<a href="#" class="social-icon text-white border-transparent bg-rss me-2 mb-0 float-none">
<i class="fa-solid fa-rss"/>
<i class="fa-solid fa-rss"/>
</a>
<a href="#" class="ms-1"><small class="d-block"><strong>Subscribe</strong><br>to RSS Feeds</small></a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div><!-- .footer-widgets-wrap end -->
</div>
<!-- Copyrights
============================================= -->
<div id="copyrights">
<div class="container">
<div class="row col-mb-30">
<div class="col-md-6 text-center text-md-start">
Copyrights &copy; 2023 All Rights Reserved by Canvas Inc.<br>
<div class="copyright-links"><a href="#">Terms of Use</a> / <a href="#">Privacy Policy</a></div>
</div>
<div class="col-md-6 text-center text-md-end">
<div class="d-flex justify-content-center justify-content-md-end mb-2">
<a href="#" class="social-icon border-transparent si-small h-bg-facebook">
<i class="fa-brands fa-facebook-f"/>
<i class="fa-brands fa-facebook-f"/>
</a>
<a href="#" class="social-icon border-transparent si-small h-bg-x-twitter">
<i class="fa-brands fa-x-twitter"/>
<i class="fa-brands fa-x-twitter"/>
</a>
<a href="#" class="social-icon border-transparent si-small h-bg-google">
<i class="fa-brands fa-google"/>
<i class="fa-brands fa-google"/>
</a>
<a href="#" class="social-icon border-transparent si-small h-bg-pinterest">
<i class="fa-brands fa-pinterest-p"/>
<i class="fa-brands fa-pinterest-p"/>
</a>
<a href="#" class="social-icon border-transparent si-small h-bg-vimeo">
<i class="fa-brands fa-vimeo-v"/>
<i class="fa-brands fa-vimeo-v"/>
</a>
<a href="#" class="social-icon border-transparent si-small h-bg-github">
<i class="fa-brands fa-github"/>
<i class="fa-brands fa-github"/>
</a>
<a href="#" class="social-icon border-transparent si-small h-bg-yahoo">
<i class="fa-brands fa-yahoo"/>
<i class="fa-brands fa-yahoo"/>
</a>
<a href="#" class="social-icon border-transparent si-small me-0 h-bg-linkedin">
<i class="fa-brands fa-linkedin"/>
<i class="fa-brands fa-linkedin"/>
</a>
</div>
<i class="bi-envelope"/> info@canvas.com <span class="middot">&middot;</span> <i class="fa-solid fa-phone"/> +1-11-6541-6369 <span class="middot">&middot;</span> <i class="bi-skype"/> CanvasOnSkype
</div>
</div>
</div>
</div><!-- #copyrights end -->
</footer><!-- #footer end -->
</template>
<script setup lang="ts">
</script>

View File

@@ -0,0 +1,80 @@
<template>
<header id="header" class="full-header">
<div id="header-wrap">
<div class="container">
<div class="header-row top-search-parent">
<!-- Logo -->
<div id="logo">
<a href="/">
<img class="logo-default" srcset="/images/logo.png, /images/logo@2x.png 2x" src="/images/logo@2x.png" alt="Canvas Logo">
<img class="logo-dark" srcset="/images/logo-dark.png, /images/logo-dark@2x.png 2x" src="/images/logo-dark@2x.png" alt="Canvas Logo">
</a>
</div>
<div class="header-misc">
<div id="top-search" class="header-misc-icon">
<a id="top-search-trigger" href="#"><i class="uil uil-search"/><i class="bi-x-lg"/></a>
</div>
<div id="top-cart" class="header-misc-icon d-none d-sm-block">
<a id="top-cart-trigger" href="#"><i class="uil uil-shopping-bag"/><span class="top-cart-number">5</span></a>
<div class="top-cart-content">
<div class="top-cart-title">
<h4>Shopping Cart</h4>
</div>
<div class="top-cart-items">
<div class="top-cart-item">
<div class="top-cart-item-image">
<a href="#"><img src="/images/shop/small/1.jpg" alt="Blue Round-Neck Tshirt"></a>
</div>
<div class="top-cart-item-desc">
<div class="top-cart-item-desc-title">
<a href="#">Blue Round-Neck Tshirt with a Button</a>
<span class="top-cart-item-price d-block">$19.99</span>
</div>
<div class="top-cart-item-quantity">x 2</div>
</div>
</div>
<div class="top-cart-item">
<div class="top-cart-item-image">
<a href="#"><img src="/images/shop/small/6.jpg" alt="Light Blue Denim Dress"></a>
</div>
<div class="top-cart-item-desc">
<div class="top-cart-item-desc-title">
<a href="#">Light Blue Denim Dress</a>
<span class="top-cart-item-price d-block">$24.99</span>
</div>
<div class="top-cart-item-quantity">x 3</div>
</div>
</div>
</div>
<div class="top-cart-action">
<span class="top-checkout-price">$114.95</span>
<a href="#" class="button button-3d button-small m-0">View Cart</a>
</div>
</div>
</div>
</div>
<div class="primary-menu-trigger">
<button class="cnvs-hamburger" type="button" title="Open Mobile Menu">
<span class="cnvs-hamburger-box"><span class="cnvs-hamburger-inner"/></span>
</button>
</div>
<!-- Primary Navigation -->
<AppPrimaryMenu />
<form class="top-search-form" action="search.html" method="get">
<input type="text" name="q" class="form-control" value="" placeholder="Type &amp; Hit Enter.." autocomplete="off">
</form>
</div>
</div>
</div>
<div class="header-wrap-clone" />
</header>
</template>
<script setup lang="ts">
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
<template>
<div id="gotoTop" class="uil uil-angle-up" aria-hidden="true" />
</template>
<script setup lang="ts">
</script>

View File

@@ -0,0 +1,130 @@
<template>
<!-- Page Title -->
<section class="page-title bg-transparent">
<div class="container">
<div class="page-title-row">
<div class="page-title-content">
<h1>Giriş</h1>
</div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><NuxtLink to="/">Ana Sayfa</NuxtLink></li>
<li class="breadcrumb-item active" aria-current="page">Giriş</li>
</ol>
</nav>
</div>
</div>
</section>
<section id="content">
<div class="content-wrap">
<div class="container">
<div class="row g-5 justify-content-center">
<div class="col-md-6 col-lg-5">
<div class="card mb-0 p-2 border-default bg-contrast-100">
<div class="card-body p-4">
<h3 class="mb-4">Hesabınıza giriş yapın</h3>
<form class="row mb-0" @submit.prevent="onSubmit">
<div class="col-12 form-group">
<label for="login-email">E-posta</label>
<input
id="login-email"
v-model="email"
type="email"
class="form-control"
:class="{ 'is-invalid': fieldError.email }"
autocomplete="email"
placeholder="ornek@email.com"
>
<div v-if="fieldError.email" class="invalid-feedback">{{ fieldError.email }}</div>
</div>
<div class="col-12 form-group">
<label for="login-password">Şifre</label>
<input
id="login-password"
v-model="password"
type="password"
class="form-control"
:class="{ 'is-invalid': fieldError.password }"
autocomplete="current-password"
>
<div v-if="fieldError.password" class="invalid-feedback">{{ fieldError.password }}</div>
</div>
<div class="col-12 form-group">
<div ref="turnstileWrapRef" class="turnstile-form-widget">
<NuxtTurnstile
ref="turnstileRef"
v-model="turnstileToken"
:options="{ theme: 'light' }"
/>
</div>
</div>
<div class="col-12 form-group">
<button
type="submit"
class="btn btn-secondary w-100 m-0"
:disabled="loading"
>
{{ loading ? 'Giriş yapılıyor...' : 'Giriş yap' }}
</button>
</div>
</form>
<div class="divider divider-center my-4">
<span>veya</span>
</div>
<div class="d-flex flex-column gap-2">
<button
type="button"
class="btn btn-outline-secondary"
:disabled="loading"
@click="signInWith('github')"
>
<Icon name="mdi:github" class="me-2" size="1.25em" />
GitHub ile giriş yap
</button>
<button
type="button"
class="btn btn-outline-secondary"
:disabled="loading"
@click="signInWith('google')"
>
<Icon name="mdi:google" class="me-2" size="1.25em" />
Google ile giriş yap
</button>
</div>
<p class="mt-4 mb-0 text-center">
Hesabınız yok mu?
<NuxtLink to="/auth/register">Kayıt olun</NuxtLink>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { useLogin } from '~/composables/useLogin'
import { useTurnstileScale } from '~/composables/useTurnstileScale'
const turnstileWrapRef = ref<HTMLElement | null>(null)
useTurnstileScale(turnstileWrapRef)
const {
email,
password,
turnstileToken,
turnstileRef,
fieldError,
loading,
onSubmit,
signInWith,
} = useLogin()
</script>

View File

@@ -0,0 +1,105 @@
<template>
<!-- Arka plan resmi tam genişlikte section uzerine uygulanir -->
<section class="page-title" :style="props.image && imageType === 'background'
? { backgroundImage: `url(${props.image})`, backgroundSize: 'cover', backgroundPosition: 'center' }
: undefined" :class="{ 'has-bg-image': props.image && imageType === 'background' }">
<div class="container">
<div class="page-title-row">
<!-- Inline resim secenegi -->
<img v-if="props.image && imageType === 'inline'" :src="props.image" alt="page image"
class="page-title-image" />
<div class="page-title-content">
<h1 :style="props.color ? { color: props.color } : undefined">{{ props.title }}</h1>
<span v-if="props.subtitle" :style="props.color ? { color: props.color } : undefined">{{ props.subtitle
}}</span>
</div>
<nav v-if="props.breadcrumbs?.length" aria-label="breadcrumb">
<ol class="breadcrumb">
<li v-for="(item, i) in props.breadcrumbs" :key="i" class="breadcrumb-item"
:class="{ active: i === props.breadcrumbs.length - 1 }"
:aria-current="i === props.breadcrumbs.length - 1 ? 'page' : undefined">
<a v-if="i < props.breadcrumbs.length - 1" :href="item.href">{{ item.label }}</a>
<template v-else>{{ item.label }}</template>
</li>
</ol>
</nav>
</div>
</div>
</section>
</template>
<script setup lang="ts">
interface Breadcrumb {
label: string
href?: string
}
// Provide defaults for props that are not required
const props = defineProps<{
title: string
subtitle?: string
breadcrumbs?: Breadcrumb[]
// Banner'dan gelen renk (#ff2600 gibi hex deger)
color?: string
// Arka plan resim URL'si
image?: string
// Resmin kullanim sekli: background veya inline
imageType?: 'background' | 'inline'
}>()
// computed ile reaktif imageType — prop async güncellendiğinde de doğru çalışır
const imageType = computed(() => props.imageType ?? 'background')
</script>
<style scoped>
/* Tam genislik arka plan - container disinda */
.page-title {
width: 100%;
min-height: 400px;
display: flex;
align-items: center;
background-color: transparent;
}
.page-title.has-bg-image {
color: #fff;
}
.page-title .container {
width: 100%;
}
.page-title-row {
display: flex;
align-items: center;
width: 100%;
padding: 40px 20px;
min-height: 400px;
}
.page-title-image {
max-width: 140px;
max-height: 140px;
object-fit: cover;
margin-right: 20px;
border-radius: 8px;
}
.page-title-content h1 {
margin: 0 0 6px 0;
}
@media (max-width: 768px) {
.page-title-row {
flex-direction: column;
text-align: center;
min-height: 300px;
}
.page-title-image {
margin: 0 0 12px 0;
}
}
</style>

View File

@@ -0,0 +1,204 @@
<template>
<!-- Page Title -->
<section class="page-title bg-transparent">
<div class="container">
<div class="page-title-row">
<div class="page-title-content">
<h1>Kayıt ol</h1>
</div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><NuxtLink to="/">Ana Sayfa</NuxtLink></li>
<li class="breadcrumb-item active" aria-current="page">Kayıt</li>
</ol>
</nav>
</div>
</div>
</section>
<section id="content">
<div class="content-wrap">
<div class="container">
<div class="row g-5 justify-content-center">
<div class="col-md-8 col-lg-6">
<div class="card mb-0 p-2 border-default bg-contrast-100">
<div class="card-body p-4">
<h3 class="mb-4">Yeni hesap oluşturun</h3>
<p class="text-muted small mb-4">
Kayıt sonrası e-posta adresinize doğrulama linki gönderilecektir. Giriş yapmadan önce e-postanızı doğrulamanız gerekir.
</p>
<div v-if="success" class="alert alert-success" role="alert">
<p class="mb-0">{{ success }}</p>
<button
v-if="!resendSent"
type="button"
class="btn btn-link btn-sm p-0 mt-2"
:disabled="resendLoading"
@click="resendVerification"
>
{{ resendLoading ? 'Gönderiliyor...' : 'Doğrulama e-postasını yeniden gönder' }}
</button>
<span v-else class="d-block mt-2 small text-success">E-posta gönderildi.</span>
</div>
<form v-if="!success" class="row g-3" @submit.prevent="onSubmit">
<div class="col-md-6 form-group">
<label for="reg-first_name">Ad</label>
<input
id="reg-first_name"
v-model="first_name"
type="text"
class="form-control"
:class="{ 'is-invalid': fieldError.first_name }"
autocomplete="given-name"
>
<div v-if="fieldError.first_name" class="invalid-feedback">{{ fieldError.first_name }}</div>
</div>
<div class="col-md-6 form-group">
<label for="reg-last_name">Soyad</label>
<input
id="reg-last_name"
v-model="last_name"
type="text"
class="form-control"
:class="{ 'is-invalid': fieldError.last_name }"
autocomplete="family-name"
>
<div v-if="fieldError.last_name" class="invalid-feedback">{{ fieldError.last_name }}</div>
</div>
<div class="col-12 form-group">
<label for="reg-username">Kullanıcı adı</label>
<input
id="reg-username"
v-model="username"
type="text"
class="form-control"
:class="{ 'is-invalid': fieldError.username }"
autocomplete="username"
>
<div v-if="fieldError.username" class="invalid-feedback">{{ fieldError.username }}</div>
</div>
<div class="col-12 form-group">
<label for="reg-email">E-posta</label>
<input
id="reg-email"
v-model="email"
type="email"
class="form-control"
:class="{ 'is-invalid': fieldError.email }"
autocomplete="email"
placeholder="ornek@email.com"
>
<div v-if="fieldError.email" class="invalid-feedback">{{ fieldError.email }}</div>
</div>
<div class="col-md-6 form-group">
<label for="reg-password">Şifre</label>
<input
id="reg-password"
v-model="password"
type="password"
class="form-control"
:class="{ 'is-invalid': fieldError.password }"
autocomplete="new-password"
>
<div v-if="fieldError.password" class="invalid-feedback">{{ fieldError.password }}</div>
</div>
<div class="col-md-6 form-group">
<label for="reg-password_confirm">Şifre (tekrar)</label>
<input
id="reg-password_confirm"
v-model="password_confirm"
type="password"
class="form-control"
:class="{ 'is-invalid': fieldError.password_confirm }"
autocomplete="new-password"
>
<div v-if="fieldError.password_confirm" class="invalid-feedback">{{ fieldError.password_confirm }}</div>
</div>
<div class="col-12 form-group">
<div ref="turnstileWrapRef" class="turnstile-form-widget">
<NuxtTurnstile
ref="turnstileRef"
v-model="turnstileToken"
:options="{ theme: 'light' }"
/>
</div>
</div>
<div class="col-12 form-group">
<button
type="submit"
class="btn btn-dark w-100 m-0"
:disabled="loading"
>
{{ loading ? 'Kaydediliyor...' : 'Kayıt ol' }}
</button>
</div>
</form>
<template v-if="!success">
<div class="divider divider-center my-4">
<span>veya</span>
</div>
<div class="d-flex flex-column gap-2">
<button
type="button"
class="btn btn-outline-secondary"
:disabled="loading"
@click="signInWith('github')"
>
<Icon name="mdi:github" class="me-2" size="1.25em" />
GitHub ile kayıt ol
</button>
<button
type="button"
class="btn btn-outline-secondary"
:disabled="loading"
@click="signInWith('google')"
>
<Icon name="mdi:google" class="me-2" size="1.25em" />
Google ile kayıt ol
</button>
</div>
</template>
<p class="mt-4 mb-0 text-center">
Zaten hesabınız var mı?
<NuxtLink to="/auth/login">Giriş yapın</NuxtLink>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { useRegister } from '~/composables/useRegister'
import { useTurnstileScale } from '~/composables/useTurnstileScale'
const turnstileWrapRef = ref<HTMLElement | null>(null)
useTurnstileScale(turnstileWrapRef)
const {
email,
first_name,
last_name,
username,
password,
password_confirm,
turnstileToken,
turnstileRef,
fieldError,
success,
loading,
resendLoading,
resendSent,
onSubmit,
resendVerification,
signInWith,
} = useRegister()
</script>

View File

@@ -0,0 +1,80 @@
import Swal from 'sweetalert2'
import { loginSchema, getFirstZodError, getFieldErrors } from '~~/lib/validations/auth'
export function useLogin () {
const { signIn } = useAuth()
const router = useRouter()
const route = useRoute()
const email = ref('')
const password = ref('')
const turnstileToken = ref<string | null>(null)
const turnstileRef = ref<{ reset: () => void } | null>(null)
const fieldError = ref<Record<string, string>>({})
const loading = ref(false)
const callbackUrl = computed(() => (route.query.callbackUrl as string) || '/')
async function onSubmit () {
fieldError.value = {}
const parsed = loginSchema.safeParse({ email: email.value, password: password.value })
if (!parsed.success) {
fieldError.value = getFieldErrors(parsed.error)
await Swal.fire({ icon: 'error', title: 'Doğrulama hatası', text: getFirstZodError(parsed.error) })
return
}
if (!turnstileToken.value) {
await Swal.fire({ icon: 'warning', title: 'Güvenlik doğrulaması', text: 'Lütfen güvenlik kutusunu işaretleyin.' })
return
}
loading.value = true
try {
const result = await signIn('credentials', {
email: email.value,
password: password.value,
turnstile_token: turnstileToken.value,
callbackUrl: callbackUrl.value,
})
if (result?.error) {
const msg = result.error === 'CredentialsSignin' ? 'E-posta veya şifre hatalı.' : String(result.error)
await Swal.fire({ icon: 'error', title: 'Giriş başarısız', text: msg })
turnstileRef.value?.reset()
turnstileToken.value = null
return
}
await Swal.fire({ icon: 'success', title: 'Giriş başarılı', timer: 1500, showConfirmButton: false })
if (result?.url) await router.push(result.url)
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : 'Giriş yapılamadı.'
await Swal.fire({ icon: 'error', title: 'Hata', text: msg })
turnstileRef.value?.reset()
turnstileToken.value = null
} finally {
loading.value = false
}
}
async function signInWith (provider: 'github' | 'google') {
loading.value = true
try {
await signIn(provider, { callbackUrl: callbackUrl.value })
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : 'Giriş yapılamadı.'
await Swal.fire({ icon: 'error', title: 'Hata', text: msg })
} finally {
loading.value = false
}
}
return {
email,
password,
turnstileToken,
turnstileRef,
fieldError,
loading,
callbackUrl,
onSubmit,
signInWith,
}
}

View File

@@ -0,0 +1,116 @@
import Swal from 'sweetalert2'
import type { RegisterResponse, RegisterRequestWithTurnstile } from '~~/lib/types/auth'
import { registerSchema, getFirstZodError, getFieldErrors } from '~~/lib/validations/auth'
export function useRegister () {
const { signIn } = useAuth()
const route = useRoute()
const email = ref('')
const first_name = ref('')
const last_name = ref('')
const username = ref('')
const password = ref('')
const password_confirm = ref('')
const turnstileToken = ref<string | null>(null)
const turnstileRef = ref<{ reset: () => void } | null>(null)
const fieldError = ref<Record<string, string>>({})
const success = ref('')
const loading = ref(false)
const resendLoading = ref(false)
const resendSent = ref(false)
const callbackUrl = computed(() => (route.query.callbackUrl as string) || '/')
async function onSubmit () {
fieldError.value = {}
const parsed = registerSchema.safeParse({
email: email.value,
first_name: first_name.value,
last_name: last_name.value,
username: username.value,
password: password.value,
password_confirm: password_confirm.value,
})
if (!parsed.success) {
fieldError.value = getFieldErrors(parsed.error)
await Swal.fire({ icon: 'error', title: 'Doğrulama hatası', text: getFirstZodError(parsed.error) })
return
}
if (!turnstileToken.value) {
await Swal.fire({ icon: 'warning', title: 'Güvenlik doğrulaması', text: 'Lütfen güvenlik kutusunu işaretleyin.' })
return
}
loading.value = true
try {
const body: RegisterRequestWithTurnstile = {
email: email.value,
first_name: first_name.value,
last_name: last_name.value,
username: username.value,
password: password.value,
turnstile_token: turnstileToken.value,
}
const data = await $fetch<RegisterResponse>('/api/auth/register', { method: 'POST', body })
success.value = data.message
await Swal.fire({ icon: 'success', title: 'Kayıt başarılı', text: data.message })
} catch (e: unknown) {
const err = e as { data?: { message?: string; detail?: string | string[] }; message?: string }
const msg = err?.data?.message ?? err?.data?.detail ?? err?.message ?? 'Kayıt yapılamadı.'
const text = Array.isArray(msg) ? msg.join(' ') : String(msg)
await Swal.fire({ icon: 'error', title: 'Kayıt hatası', text })
turnstileRef.value?.reset()
turnstileToken.value = null
} finally {
loading.value = false
}
}
async function resendVerification () {
resendLoading.value = true
try {
await $fetch<{ message: string }>('/api/auth/resend-verification', {
method: 'POST',
body: { email: email.value },
})
resendSent.value = true
await Swal.fire({ icon: 'success', text: 'Doğrulama e-postası gönderildi.', timer: 2000, showConfirmButton: false })
} catch {
await Swal.fire({ icon: 'error', text: 'E-posta gönderilemedi.' })
} finally {
resendLoading.value = false
}
}
async function signInWith (provider: 'github' | 'google') {
loading.value = true
try {
await signIn(provider, { callbackUrl: callbackUrl.value })
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : 'Giriş yapılamadı.'
await Swal.fire({ icon: 'error', title: 'Hata', text: msg })
} finally {
loading.value = false
}
}
return {
email,
first_name,
last_name,
username,
password,
password_confirm,
turnstileToken,
turnstileRef,
fieldError,
success,
loading,
resendLoading,
resendSent,
callbackUrl,
onSubmit,
resendVerification,
signInWith,
}
}

View File

@@ -0,0 +1,38 @@
import { onBeforeUnmount, onMounted, type Ref } from 'vue'
/** Cloudflare Turnstile varsayılan genişliği (px) */
const TURNSTILE_WIDTH = 300
/**
* Turnstile widget'ı sarmalayan elementin genişliğine göre --turnstile-scale CSS değişkenini günceller.
* Böylece widget form alanı genişliğinde görünür.
*/
export function useTurnstileScale(wrapperRef: Ref<HTMLElement | null>) {
function updateScale() {
const el = wrapperRef.value
if (!el) return
const w = el.offsetWidth
if (w > 0) {
const scale = w / TURNSTILE_WIDTH
el.style.setProperty('--turnstile-scale', String(scale))
}
}
let observer: ResizeObserver | null = null
onMounted(() => {
updateScale()
const el = wrapperRef.value
if (el && typeof ResizeObserver !== 'undefined') {
observer = new ResizeObserver(updateScale)
observer.observe(el)
}
})
onBeforeUnmount(() => {
if (observer && wrapperRef.value) {
observer.unobserve(wrapperRef.value)
observer = null
}
})
}

15
app/layouts/default.vue Normal file
View File

@@ -0,0 +1,15 @@
<template>
<div id="wrapper">
<AppHeader />
<slot />
<AppFooter />
<GotoTop />
</div>
</template>
<script setup lang="ts">
// Explicit imports so header/footer/gototop are not lazy-loaded (avoids hydration issues)
import AppHeader from '~/components/AppHeader.vue'
import AppFooter from '~/components/AppFooter.vue'
import GotoTop from '~/components/GotoTop.vue'
</script>

13
app/pages/auth/login.vue Normal file
View File

@@ -0,0 +1,13 @@
<template>
<div>
<LoginComponent />
</div>
</template>
<script lang="ts" setup>
</script>
<style>
</style>

View File

@@ -0,0 +1,13 @@
<template>
<div>
<RegisterComponent />
</div>
</template>
<script lang="ts" setup>
</script>
<style>
</style>

401
app/pages/index.vue Normal file
View File

@@ -0,0 +1,401 @@
<script setup lang="ts">
import { storeToRefs } from "pinia";
import type { Banner } from "~~/types/banner";
import type { PostResponse } from "~~/types/post";
import { useSettingStore } from '~/stores/setting';
import { useBannerStore } from '~/stores/banner';
const _settings = useSettingStore();
const { settings } = storeToRefs(_settings);
const _bannerStore = useBannerStore();
const { banner } = storeToRefs(_bannerStore);
// API base URL — goreceli image path'ini tam URL'e donustur
const config = useRuntimeConfig();
const apiBase = (config.public.BASE_API_URL as string) || 'http://127.0.0.1:8080';
// Banner'dan gelen ilk banner'in resim URL'si (tam URL)
const bannerImageUrl = computed(() => {
const img = banner.value?.[0]?.image;
if (!img) return undefined;
if (img.startsWith('http')) return img;
return `${apiBase.replace(/\/$/, '')}${img}`;
});
// Resim path'ini tam URL'e donustur
const imgUrl = (path: string) => {
if (!path) return '';
if (path.startsWith('http')) return path;
return `${apiBase.replace(/\/$/, '')}${path}`;
};
// Tarihi gg.aa.yyyy formatina cevir
const formatDate = (dateStr?: string) => {
if (!dateStr) return '';
return new Date(dateStr).toLocaleDateString('tr-TR', {
day: '2-digit', month: 'long', year: 'numeric'
});
};
const breadcrumbs = [
{ label: 'Home', href: '#' },
{ label: 'Blog' }
]
useSeoMeta({
title: 'Blog',
description: 'En son haberler ve makaleler',
})
// Postlari API'dan cek
const route = useRoute();
const page = computed(() => Number(route.query.page) || 1);
const { data: postsData, refresh } = await useFetch<PostResponse>(() => `${apiBase}/api/v1/posts?page=${page.value}`);
const posts = computed(() => postsData.value?.data ?? []);
const postMeta = computed(() => postsData.value?.meta);
// Sayfa degistirme fonksiyonu
const changePage = (newPage: number) => {
if (newPage < 1 || (postMeta.value && newPage > Math.ceil(postMeta.value.total / postMeta.value.per_page))) return;
navigateTo({ query: { ...route.query, page: newPage } });
};
// Toplam sayfa sayisi
const totalPages = computed(() => {
if (!postMeta.value) return 1;
return Math.ceil(postMeta.value.total / postMeta.value.per_page);
});
</script>
<template>
<div>
<PageTitle title="Blog" subtitle="Our Latest News" :breadcrumbs="breadcrumbs" :image="bannerImageUrl"
:color="banner?.[0]?.color" imageType="background" />
<!-- Content
============================================= -->
<section id="content">
<div class="content-wrap">
<div class="container">
<div class="row gx-5 col-mb-80">
<!-- Post Content
============================================= -->
<main class="postcontent col-lg-9">
<!-- Postlar API'dan geliyor -->
<div id="posts" class="row gutter-40">
<div v-for="post in posts" :key="post.ID" class="entry col-12">
<div class="grid-inner row g-0">
<!-- Sol: Post resmi -->
<div class="col-md-4">
<div class="entry-image">
<NuxtLink :to="`/blog/${post.slug}`">
<img :src="imgUrl(post.images_min || post.images)" :alt="post.title"
style="width:100%; height:200px; object-fit:cover;" />
</NuxtLink>
</div>
</div>
<!-- Sag: Post icerigi -->
<div class="col-md-8 ps-md-4">
<div class="entry-title title-sm">
<h2>
<NuxtLink :to="`/blog/${post.slug}`">{{ post.title }}</NuxtLink>
</h2>
</div>
<div class="entry-meta">
<ul>
<li><i class="uil uil-schedule" /> {{ formatDate(post.UpdatedAt || post.CreatedAt) }}
<span v-if="post.UpdatedAt && post.UpdatedAt !== post.CreatedAt"
style="font-size: 0.8em; opacity: 0.8;">
(Güncellendi)
</span>
</li>
<li v-if="post.categories?.length">
<i class="uil uil-folder-open" />
<template v-for="(cat, ci) in post.categories" :key="cat.ID">
<NuxtLink :to="`/kategori/${cat.slug}`">{{ cat.title }}</NuxtLink>
<span v-if="ci < post.categories.length - 1">, </span>
</template>
</li>
<li v-if="post.tags?.length">
<i class="uil uil-tag" />
<template v-for="(tag, ti) in post.tags" :key="tag.ID">
<span>{{ tag.name }}</span>
<span v-if="ti < post.tags.length - 1">, </span>
</template>
</li>
</ul>
</div>
<div class="entry-content">
<p>{{ post.content.substring(0, 150) }}{{ post.content.length > 150 ? '...' : '' }}</p>
<NuxtLink :to="`/blog/${post.slug}`" class="more-link">Devamını Oku</NuxtLink>
</div>
</div>
</div>
</div>
<!-- Post bulunamadiysa -->
<div v-if="!posts.length" class="col-12 text-center py-5">
<p>Henüz gönderi bulunamadı.</p>
</div>
</div><!-- #posts end -->
<!-- Pager
============================================= -->
<div v-if="totalPages > 1" class="d-flex justify-content-between mt-5">
<button class="btn btn-outline-secondary" :disabled="page <= 1" @click="changePage(page - 1)">
&larr; Daha Eski
</button>
<div class="align-self-center">
Sayfa {{ page }} / {{ totalPages }}
</div>
<button class="btn btn-outline-dark" :disabled="page >= totalPages" @click="changePage(page + 1)">
Daha Yeni &rarr;
</button>
</div>
<!-- .pager end -->
</main><!-- .postcontent end -->
<!-- Sidebar
============================================= -->
<aside class="sidebar col-lg-3">
<div class="sidebar-widgets-wrap">
<div class="widget">
<ul id="canvas-tab" class="nav canvas-tabs tabs nav-tabs size-sm mb-3" role="tablist">
<li class="nav-item" role="presentation">
<button id="canvas-tab-1" class="nav-link active" data-bs-toggle="pill" data-bs-target="#tab-1"
type="button" role="tab" aria-controls="canvas-tab-1" aria-selected="true">Popular</button>
</li>
<li class="nav-item" role="presentation">
<button id="canvas-tab-2" class="nav-link" data-bs-toggle="pill" data-bs-target="#tab-2"
type="button" role="tab" aria-controls="canvas-tab-2" aria-selected="false">Recents</button>
</li>
<li class="nav-item" role="presentation">
<button id="canvas-tab-3" class="nav-link uil uil-comments-alt" data-bs-toggle="pill"
data-bs-target="#tab-3" type="button" role="tab" aria-controls="canvas-tab-3"
aria-selected="false" />
</li>
</ul>
<div id="canvas-TabContent" class="tab-content">
<div id="tab-1" class="tab-pane show active" role="tabpanel" aria-labelledby="canvas-tab-1"
tabindex="0">
<div id="popular-post-list-sidebar" class="posts-sm row col-mb-30">
<div class="entry col-12">
<div class="grid-inner row g-0">
<div class="col-auto">
<div class="entry-image">
<a href="#"><img class="rounded-circle" src="/images/magazine/small/3.jpg"
alt="Image"></a>
</div>
</div>
<div class="col ps-3">
<div class="entry-title">
<h4><a href="#">Lorem ipsum dolor sit amet, consectetur</a></h4>
</div>
<div class="entry-meta">
<ul>
<li><i class="uil uil-comments-alt" /> 35 Comments</li>
</ul>
</div>
</div>
</div>
</div>
<div class="entry col-12">
<div class="grid-inner row g-0">
<div class="col-auto">
<div class="entry-image">
<a href="#"><img class="rounded-circle" src="/images/magazine/small/2.jpg"
alt="Image"></a>
</div>
</div>
<div class="col ps-3">
<div class="entry-title">
<h4><a href="#">Elit Assumenda vel amet dolorum quasi</a></h4>
</div>
<div class="entry-meta">
<ul>
<li><i class="uil uil-comments-alt" /> 24 Comments</li>
</ul>
</div>
</div>
</div>
</div>
<div class="entry col-12">
<div class="grid-inner row g-0">
<div class="col-auto">
<div class="entry-image">
<a href="#"><img class="rounded-circle" src="/images/magazine/small/1.jpg"
alt="Image"></a>
</div>
</div>
<div class="col ps-3">
<div class="entry-title">
<h4><a href="#">Debitis nihil placeat, illum est nisi</a></h4>
</div>
<div class="entry-meta">
<ul>
<li><i class="uil uil-comments-alt" /> 19 Comments</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="tab-2" class="tab-pane" role="tabpanel" aria-labelledby="canvas-tab-2" tabindex="0">
<div id="recent-post-list-sidebar" class="posts-sm row col-mb-30">
<div class="entry col-12">
<div class="grid-inner row g-0">
<div class="col-auto">
<div class="entry-image">
<a href="#"><img class="rounded-circle" src="/images/magazine/small/1.jpg"
alt="Image"></a>
</div>
</div>
<div class="col ps-3">
<div class="entry-title">
<h4><a href="#">Lorem ipsum dolor sit amet, consectetur</a></h4>
</div>
<div class="entry-meta">
<ul>
<li>10th July 2021</li>
</ul>
</div>
</div>
</div>
</div>
<div class="entry col-12">
<div class="grid-inner row g-0">
<div class="col-auto">
<div class="entry-image">
<a href="#"><img class="rounded-circle" src="/images/magazine/small/2.jpg"
alt="Image"></a>
</div>
</div>
<div class="col ps-3">
<div class="entry-title">
<h4><a href="#">Elit Assumenda vel amet dolorum quasi</a></h4>
</div>
<div class="entry-meta">
<ul>
<li>10th July 2021</li>
</ul>
</div>
</div>
</div>
</div>
<div class="entry col-12">
<div class="grid-inner row g-0">
<div class="col-auto">
<div class="entry-image">
<a href="#"><img class="rounded-circle" src="/images/magazine/small/3.jpg"
alt="Image"></a>
</div>
</div>
<div class="col ps-3">
<div class="entry-title">
<h4><a href="#">Debitis nihil placeat, illum est nisi</a></h4>
</div>
<div class="entry-meta">
<ul>
<li>10th July 2021</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="tab-3" class="tab-pane" role="tabpanel" aria-labelledby="canvas-tab-3" tabindex="0">
<div id="recent-comments-list-sidebar" class="posts-sm row col-mb-30">
<div class="entry col-12">
<div class="grid-inner row g-0">
<div class="col-auto">
<div class="entry-image">
<a href="#"><img class="rounded-circle" src="/images/icons/avatar.jpg"
alt="User Avatar"></a>
</div>
</div>
<div class="col ps-3">
<strong>John Doe:</strong> Veritatis recusandae sunt repellat distinctio...
</div>
</div>
</div>
<div class="entry col-12">
<div class="grid-inner row g-0">
<div class="col-auto">
<div class="entry-image">
<a href="#"><img class="rounded-circle" src="/images/icons/avatar.jpg"
alt="User Avatar"></a>
</div>
</div>
<div class="col ps-3">
<strong>Mary Jane:</strong> Possimus libero, earum officia architecto maiores....
</div>
</div>
</div>
<div class="entry col-12">
<div class="grid-inner row g-0">
<div class="col-auto">
<div class="entry-image">
<a href="#"><img class="rounded-circle" src="/images/icons/avatar.jpg"
alt="User Avatar"></a>
</div>
</div>
<div class="col ps-3">
<strong>Site Admin:</strong> Deleniti magni labore laboriosam odio...
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="widget">
<h4>Tag Cloud</h4>
<div class="tagcloud">
<a href="#">general</a>
<a href="#">videos</a>
<a href="#">music</a>
<a href="#">media</a>
<a href="#">photography</a>
<a href="#">parallax</a>
<a href="#">ecommerce</a>
<a href="#">terms</a>
<a href="#">coupons</a>
<a href="#">modern</a>
</div>
</div>
</div>
</aside><!-- .sidebar end -->
</div>
</div>
</div>
</section><!-- #content end -->
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,20 @@
import type {Setting} from "~~/types/setting";
export default defineNuxtPlugin(async () => {
const { useSettingStore } = await import('@/stores/setting')
const settingStore = useSettingStore()
const config = useRuntimeConfig();
const apiUrl = config.public.BASE_API_URL;
try {
const data = await $fetch<Setting>(`${apiUrl}/api/v1/setting`)
if (data) {
settingStore.setSettings(data)
}
} catch (error) {
console.error('Failed to fetch settings:', error)
}
})

View File

@@ -0,0 +1,20 @@
import type { Banner } from "~~/types/banner";
export default defineNuxtPlugin(async () => {
const { useBannerStore } = await import('@/stores/banner')
const bannerStore = useBannerStore()
const config = useRuntimeConfig();
const apiUrl = config.public.BASE_API_URL;
try {
const data = await $fetch<Banner[]>(`${apiUrl}/api/v1/hero`)
if (data) {
bannerStore.setBanner(data)
}
} catch (error) {
console.error('Failed to fetch banners:', error)
}
})

View File

@@ -0,0 +1,20 @@
import type {ProductTree} from "~~/types/banner";
export default defineNuxtPlugin(async () => {
const { useProductTreeStore } = await import('@/stores/productTree')
const productTreeStore = useProductTreeStore()
const config = useRuntimeConfig();
const apiUrl = config.public.BASE_API_URL;
try {
const data = await $fetch<ProductTree[]>(`${apiUrl}/api/v1/products-tree/`)
if (data) {
productTreeStore.setProductTree(data)
}
} catch (error) {
console.error('Failed to fetch product tree:', error)
}
})

View File

@@ -0,0 +1,15 @@
import { useCartStore } from '~/stores/cart';
export default defineNuxtPlugin(() => {
const cartStore = useCartStore();
const storageKey = cartStore.getStorageKey();
cartStore.hydrateFromStorage(localStorage.getItem(storageKey));
cartStore.$subscribe(
() => {
localStorage.setItem(storageKey, cartStore.toStoragePayload());
},
{ detached: true }
);
});

View File

@@ -0,0 +1,9 @@
/**
* Canvas theme requires body.stretched for full-width layout.
* Ensures body has the class on client (useHead bodyAttrs may not apply in all cases).
*/
export default defineNuxtPlugin(() => {
if (import.meta.client && document?.body) {
document.body.classList.add('stretched')
}
})

View File

@@ -0,0 +1,5 @@
import Swal from 'sweetalert2'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.provide('swal', Swal)
})

18
app/stores/banner.ts Normal file
View File

@@ -0,0 +1,18 @@
import { defineStore } from 'pinia';
import type { Banner } from '~~/types/banner';
interface BannerState {
banner: Banner[];
}
export const useBannerStore = defineStore('banner', {
state: (): BannerState => ({
banner: [],
}),
actions: {
setBanner(newBanner: Banner[]): void {
this.banner = newBanner;
}
}
});

177
app/stores/cart.ts Normal file
View File

@@ -0,0 +1,177 @@
import { defineStore } from 'pinia';
import type { Product } from '~~/types/product';
export interface CartItem {
product: Product;
quantity: number;
}
export type CouponType = 'percent' | 'fixed';
export interface CartCoupon {
code: string;
type: CouponType;
value: number;
description?: string;
minSubtotal?: number;
}
interface CartState {
items: CartItem[];
coupon: CartCoupon | null;
}
const CART_STORAGE_KEY = 'nuxt-shop-cart-v1';
const AVAILABLE_COUPONS: CartCoupon[] = [
{
code: 'SALE10',
type: 'percent',
value: 10,
description: '10% indirim',
},
{
code: 'WELCOME50',
type: 'fixed',
value: 50,
description: '50 TL indirim',
minSubtotal: 300,
},
];
export const useCartStore = defineStore('cart', {
state: (): CartState => ({
items: [],
coupon: null,
}),
getters: {
totalQuantity: (state): number =>
state.items.reduce((total, item) => total + item.quantity, 0),
subtotal: (state): number =>
state.items.reduce(
(total, item) => total + item.product.price * item.quantity,
0
),
discountAmount(): number {
if (!this.coupon) return 0;
if (this.coupon.minSubtotal && this.subtotal < this.coupon.minSubtotal) {
return 0;
}
const rawDiscount =
this.coupon.type === 'percent'
? (this.subtotal * this.coupon.value) / 100
: this.coupon.value;
return Math.min(rawDiscount, this.subtotal);
},
total(): number {
return Math.max(this.subtotal - this.discountAmount, 0);
},
totalPrice(): number {
return this.total;
},
getItem:
(state) =>
(slug: string): CartItem | undefined =>
state.items.find((item) => item.product.slug === slug),
},
actions: {
addItem(product: Product, quantity = 1): void {
if (!Number.isFinite(quantity) || quantity <= 0) {
return;
}
const existingItem = this.items.find(
(item) => item.product.slug === product.slug
);
if (existingItem) {
existingItem.quantity += quantity;
existingItem.product = product;
return;
}
this.items.push({ product, quantity });
},
setItemQuantity(slug: string, quantity: number): void {
if (!Number.isFinite(quantity)) {
return;
}
const item = this.items.find((entry) => entry.product.slug === slug);
if (!item) {
return;
}
if (quantity <= 0) {
this.items = this.items.filter(
(entry) => entry.product.slug !== slug
);
return;
}
item.quantity = Math.floor(quantity);
},
removeItem(slug: string): void {
this.items = this.items.filter(
(item) => item.product.slug !== slug
);
},
clearCart(): void {
this.items = [];
this.coupon = null;
},
setItems(items: CartItem[]): void {
this.items = items;
},
applyCouponCode(code: string): boolean {
const normalizedCode = code.trim().toUpperCase();
if (!normalizedCode) return false;
const coupon = AVAILABLE_COUPONS.find(
(entry) => entry.code === normalizedCode
);
if (!coupon) return false;
if (coupon.minSubtotal && this.subtotal < coupon.minSubtotal) {
return false;
}
this.coupon = coupon;
return true;
},
clearCoupon(): void {
this.coupon = null;
},
setCoupon(coupon: CartCoupon | null): void {
this.coupon = coupon;
},
hydrateFromStorage(payload: string | null): void {
if (!payload) return;
try {
const parsed = JSON.parse(payload) as {
items?: CartItem[];
coupon?: CartCoupon | null;
};
if (Array.isArray(parsed.items)) {
this.items = parsed.items.filter(
(entry) =>
entry &&
entry.product &&
typeof entry.product.slug === 'string' &&
typeof entry.quantity === 'number'
);
}
if (parsed.coupon && typeof parsed.coupon.code === 'string') {
this.coupon = parsed.coupon;
}
} catch {
// Ignore invalid storage payloads
}
},
toStoragePayload(): string {
return JSON.stringify({
items: this.items,
coupon: this.coupon,
});
},
getStorageKey(): string {
return CART_STORAGE_KEY;
},
},
});

17
app/stores/productTree.ts Normal file
View File

@@ -0,0 +1,17 @@
import { defineStore } from 'pinia';
import type { ProductTree } from '~~/types/banner';
interface ProductTreeState {
productTree: ProductTree[];
}
export const useProductTreeStore = defineStore('productTree', {
state: (): ProductTreeState => ({
productTree: [],
}),
actions: {
setProductTree(newProductTree: ProductTree[]): void {
this.productTree = newProductTree;
}
}
});

17
app/stores/setting.ts Normal file
View File

@@ -0,0 +1,17 @@
import { defineStore } from 'pinia';
import type { Setting } from '~~/types/setting';
interface SettingState {
settings: Setting | null;
}
export const useSettingStore = defineStore('setting', {
state: (): SettingState => ({
settings: null,
}),
actions: {
setSettings(newSettings: Setting | null): void {
this.settings = newSettings;
}
}
});