first commit
This commit is contained in:
10
app/app.vue
Normal file
10
app/app.vue
Normal 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>
|
||||
248
app/components/AppFooter.vue
Normal file
248
app/components/AppFooter.vue
Normal 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> & <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 & 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 © 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">·</span> <i class="fa-solid fa-phone"/> +1-11-6541-6369 <span class="middot">·</span> <i class="bi-skype"/> CanvasOnSkype
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div><!-- #copyrights end -->
|
||||
</footer><!-- #footer end -->
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
80
app/components/AppHeader.vue
Normal file
80
app/components/AppHeader.vue
Normal 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 & Hit Enter.." autocomplete="off">
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-wrap-clone" />
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
1598
app/components/AppPrimaryMenu.vue
Normal file
1598
app/components/AppPrimaryMenu.vue
Normal file
File diff suppressed because it is too large
Load Diff
6
app/components/GotoTop.vue
Normal file
6
app/components/GotoTop.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<div id="gotoTop" class="uil uil-angle-up" aria-hidden="true" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
130
app/components/LoginComponent.vue
Normal file
130
app/components/LoginComponent.vue
Normal 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>
|
||||
105
app/components/PageTitle.vue
Normal file
105
app/components/PageTitle.vue
Normal 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>
|
||||
204
app/components/RegisterComponent.vue
Normal file
204
app/components/RegisterComponent.vue
Normal 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>
|
||||
80
app/composables/useLogin.ts
Normal file
80
app/composables/useLogin.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
116
app/composables/useRegister.ts
Normal file
116
app/composables/useRegister.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
38
app/composables/useTurnstileScale.ts
Normal file
38
app/composables/useTurnstileScale.ts
Normal 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
15
app/layouts/default.vue
Normal 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
13
app/pages/auth/login.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<LoginComponent />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
13
app/pages/auth/register.vue
Normal file
13
app/pages/auth/register.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<RegisterComponent />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
401
app/pages/index.vue
Normal file
401
app/pages/index.vue
Normal 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)">
|
||||
← 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 →
|
||||
</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>
|
||||
20
app/plugins/01.fetch_setting.ts
Normal file
20
app/plugins/01.fetch_setting.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
20
app/plugins/02.fetch_banner.ts
Normal file
20
app/plugins/02.fetch_banner.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
20
app/plugins/03.fetch_product_tree.ts
Normal file
20
app/plugins/03.fetch_product_tree.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
15
app/plugins/04.cart-persist.client.ts
Normal file
15
app/plugins/04.cart-persist.client.ts
Normal 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 }
|
||||
);
|
||||
});
|
||||
9
app/plugins/body-class.client.ts
Normal file
9
app/plugins/body-class.client.ts
Normal 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')
|
||||
}
|
||||
})
|
||||
5
app/plugins/sweetalert2.client.ts
Normal file
5
app/plugins/sweetalert2.client.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import Swal from 'sweetalert2'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.provide('swal', Swal)
|
||||
})
|
||||
18
app/stores/banner.ts
Normal file
18
app/stores/banner.ts
Normal 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
177
app/stores/cart.ts
Normal 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
17
app/stores/productTree.ts
Normal 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
17
app/stores/setting.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user