first commit
This commit is contained in:
99
components/AboutSection.tsx
Normal file
99
components/AboutSection.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface AboutSectionProps {
|
||||
isAuthenticated: boolean;
|
||||
}
|
||||
|
||||
export default function AboutSection({ isAuthenticated }: AboutSectionProps) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
|
||||
// Initialize WOW.js after mount
|
||||
if (typeof window !== "undefined" && window.WOW) {
|
||||
new window.WOW().init();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="about-section" suppressHydrationWarning>
|
||||
<div className="right-shape">
|
||||
<img src="/assets/img/core-img/shape.png" alt="" />
|
||||
</div>
|
||||
<div className="divider"></div>
|
||||
<div className="container">
|
||||
<div className="row g-5 align-items-center">
|
||||
<div className="col-12 col-lg-6">
|
||||
<div className="about-content ps-md-4">
|
||||
<div className="section-heading">
|
||||
<span className="sub-title">About Us</span>
|
||||
<h2 className="mb-4">We Are About to Witness Something Great</h2>
|
||||
<p className="mb-5">
|
||||
Empower your business with our cutting-edge IT services and unmatched support, tailored for transformative growth and harness innovation.
|
||||
</p>
|
||||
<ul className="about-list ps-0 d-flex flex-column gap-3 list-unstyled mb-5">
|
||||
<li className="d-flex align-items-center gap-2">
|
||||
<div className="icon"><i className="ti ti-arrow-right"></i></div>
|
||||
<h5 className="mb-0">Complete authentication system with email verification</h5>
|
||||
</li>
|
||||
<li className="d-flex align-items-center gap-2">
|
||||
<div className="icon"><i className="ti ti-arrow-right"></i></div>
|
||||
<h5 className="mb-0">Social login with Google and GitHub integration</h5>
|
||||
</li>
|
||||
<li className="d-flex align-items-center gap-2">
|
||||
<div className="icon"><i className="ti ti-arrow-right"></i></div>
|
||||
<h5 className="mb-0">Secure JWT token management and refresh</h5>
|
||||
</li>
|
||||
</ul>
|
||||
<Link className="btn btn-primary" href={isAuthenticated ? "/dashboard" : "/auth/register"}>
|
||||
{isAuthenticated ? "View Dashboard" : "Get Started"} <i className="ti ti-arrow-up-right"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<div
|
||||
className={mounted ? "about-video-content wow fadeInUp" : "about-video-content"}
|
||||
data-wow-duration="1000ms"
|
||||
data-wow-delay="500ms"
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<img src="/assets/img/bg-img/25.jpg" alt="" />
|
||||
<div className="play-video-btn video-btn" data-video="https://youtu.be/4GUFkrHvZdE">
|
||||
<div className="icon"><i className="ti ti-player-play-filled"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={mounted ? "about-images d-flex px-5 mt-5 wow fadeInUp" : "about-images d-flex px-5 mt-5"}
|
||||
data-wow-duration="1000ms"
|
||||
data-wow-delay="800ms"
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<div>
|
||||
<img className="w-100" src="/assets/img/bg-img/26.jpg" alt="" />
|
||||
</div>
|
||||
<div>
|
||||
<svg className="rotatingImage" xmlns="http://www.w3.org/2000/svg" width={70} height={70} viewBox="0 0 70 70" fill="none">
|
||||
<path d="M35 0L46.1369 23.8631L70 35L46.1369 46.1369L35 70L23.8631 46.1369L0 35L23.8631 23.8631L35 0Z" fill="#222222" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="divider"></div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Extend Window interface
|
||||
declare global {
|
||||
interface Window {
|
||||
WOW: any;
|
||||
}
|
||||
}
|
||||
|
||||
429
components/BlogSection.tsx
Normal file
429
components/BlogSection.tsx
Normal file
@@ -0,0 +1,429 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { PostType, Categorie, PaginatedResponse } from "@/Type/post";
|
||||
import { getBlogPosts, getBlogCategories, extractPageNumber } from "@/lib/blogApi";
|
||||
|
||||
export default function BlogSection() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [posts, setPosts] = useState<PostType[]>([]);
|
||||
const [recentPosts, setRecentPosts] = useState<PostType[]>([]);
|
||||
const [categories, setCategories] = useState<Categorie[]>([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [nextPage, setNextPage] = useState<string | null>(null);
|
||||
const [prevPage, setPrevPage] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
|
||||
// Initialize WOW.js after mount
|
||||
if (typeof window !== "undefined" && window.WOW) {
|
||||
new window.WOW().init();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Fetch posts, recent posts (from different page) and categories in parallel
|
||||
const [postsData, recentPostsData, categoriesData] = await Promise.all([
|
||||
getBlogPosts(currentPage),
|
||||
getBlogPosts(currentPage === 1 ? 2 : 1), // Get from different page
|
||||
getBlogCategories()
|
||||
]);
|
||||
|
||||
setPosts(postsData.results);
|
||||
setNextPage(postsData.next);
|
||||
setPrevPage(postsData.previous);
|
||||
|
||||
// Calculate total pages (assuming 10 items per page, adjust if needed)
|
||||
const itemsPerPage = postsData.results.length || 10;
|
||||
setTotalPages(Math.ceil(postsData.count / itemsPerPage));
|
||||
|
||||
// Set recent posts (exclude current page posts)
|
||||
const currentPostSlugs = new Set(postsData.results.map(p => p.slug));
|
||||
const filteredRecentPosts = recentPostsData.results
|
||||
.filter(post => !currentPostSlugs.has(post.slug))
|
||||
.slice(0, 3);
|
||||
|
||||
// If we don't have enough recent posts, try to get more from page 3
|
||||
if (filteredRecentPosts.length < 3 && currentPage !== 3) {
|
||||
try {
|
||||
const additionalPostsData = await getBlogPosts(3);
|
||||
const additionalFiltered = additionalPostsData.results
|
||||
.filter(post => !currentPostSlugs.has(post.slug))
|
||||
.slice(0, 3 - filteredRecentPosts.length);
|
||||
setRecentPosts([...filteredRecentPosts, ...additionalFiltered]);
|
||||
} catch {
|
||||
setRecentPosts(filteredRecentPosts);
|
||||
}
|
||||
} else {
|
||||
setRecentPosts(filteredRecentPosts);
|
||||
}
|
||||
|
||||
setCategories(categoriesData);
|
||||
} catch (err) {
|
||||
console.error("Error fetching blog data:", err);
|
||||
setError("Blog yazıları yüklenirken bir hata oluştu.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [currentPage]);
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
if (page >= 1 && page <= totalPages) {
|
||||
setCurrentPage(page);
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString("tr-TR", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric"
|
||||
});
|
||||
};
|
||||
|
||||
const getPageNumbers = () => {
|
||||
const pages: (number | string)[] = [];
|
||||
const maxVisible = 5;
|
||||
|
||||
if (totalPages <= maxVisible) {
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
if (currentPage <= 3) {
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
pages.push("...");
|
||||
pages.push(totalPages);
|
||||
} else if (currentPage >= totalPages - 2) {
|
||||
pages.push(1);
|
||||
pages.push("...");
|
||||
for (let i = totalPages - 3; i <= totalPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
pages.push(1);
|
||||
pages.push("...");
|
||||
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
}
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
// Get unique tags from all posts
|
||||
const allTags = Array.from(
|
||||
new Set(
|
||||
posts.flatMap(post => post.tags.map(tag => tag.tag))
|
||||
)
|
||||
);
|
||||
|
||||
// Get category count from API data
|
||||
const getCategoryCount = (category: Categorie): number => {
|
||||
if (category.posts && category.posts.length > 0) {
|
||||
return category.posts.length;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Flatten categories to show both parent and child categories
|
||||
const flattenCategories = (categories: Categorie[]): Categorie[] => {
|
||||
const result: Categorie[] = [];
|
||||
|
||||
categories.forEach(category => {
|
||||
if (category.is_active) {
|
||||
// Add parent category
|
||||
result.push(category);
|
||||
|
||||
// Add child categories if they exist
|
||||
if (category.child && category.child.length > 0) {
|
||||
category.child.forEach(child => {
|
||||
if (child.is_active) {
|
||||
result.push(child);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="blog-section" suppressHydrationWarning>
|
||||
{/* Divider */}
|
||||
<div className="divider"></div>
|
||||
|
||||
<div className="container">
|
||||
<div className="row g-5 g-md-4 g-xl-5">
|
||||
<div className="col-12 col-md-7 col-lg-8">
|
||||
{/* Blog List Wrapper */}
|
||||
<div className="blog-list-wrapper pe-lg-3">
|
||||
{isLoading ? (
|
||||
<div className="text-center py-5">
|
||||
<p>Yükleniyor...</p>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center py-5">
|
||||
<p className="text-danger">{error}</p>
|
||||
</div>
|
||||
) : posts.length === 0 ? (
|
||||
<div className="text-center py-5">
|
||||
<p>Henüz blog yazısı bulunmamaktadır.</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{posts.map((post, index) => (
|
||||
<div
|
||||
key={post.slug}
|
||||
className={`blog-card-two ${mounted ? 'wow fadeInUp' : ''}`}
|
||||
data-wow-duration="1000ms"
|
||||
data-wow-delay={`${(index + 1) * 200}ms`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{/* Post Image */}
|
||||
{post.image && (
|
||||
<div className="post-img">
|
||||
<img src={post.image} alt={post.title} />
|
||||
</div>
|
||||
)}
|
||||
{/* Post Body */}
|
||||
<div className="post-body">
|
||||
<div className="blog-meta flex-wrap d-flex align-items-center gap-4 mb-3">
|
||||
<a href="#">
|
||||
<svg width="20" height="20">
|
||||
<use xlinkHref="#icon-user-profile"></use>
|
||||
</svg>
|
||||
By Admin
|
||||
</a>
|
||||
<a href="#">
|
||||
<svg width="18" height="18">
|
||||
<use xlinkHref="#icon-message-box"></use>
|
||||
</svg>
|
||||
{formatDate(post.created_at)}
|
||||
</a>
|
||||
{post.categories.length > 0 && (
|
||||
<a href="#">
|
||||
<svg width="18" height="18">
|
||||
<use xlinkHref="#icon-calendar"></use>
|
||||
</svg>
|
||||
{post.categories[0].title}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<Link className="post-title" href={`/blog/${post.slug}`}>
|
||||
{post.title}
|
||||
</Link>
|
||||
<p className="mt-3 mb-5">
|
||||
{post.content.length > 200
|
||||
? `${post.content.substring(0, 200)}...`
|
||||
: post.content}
|
||||
</p>
|
||||
<Link className="btn btn-primary" href={`/blog/${post.slug}`}>
|
||||
Read More <i className="ti ti-arrow-up-right"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<ul className="softora-pagination list-unstyled justify-content-start">
|
||||
{prevPage && (
|
||||
<li>
|
||||
<a
|
||||
className="magnet-link"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handlePageChange(currentPage - 1);
|
||||
}}
|
||||
>
|
||||
<i className="ti ti-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{getPageNumbers().map((page, index) => {
|
||||
const isActive = typeof page === "number" && currentPage === page;
|
||||
return (
|
||||
<li key={index} className={isActive ? "active" : ""}>
|
||||
{page === "..." ? (
|
||||
<span className="magnet-link">{page}</span>
|
||||
) : (
|
||||
<a
|
||||
className="magnet-link"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handlePageChange(page as number);
|
||||
}}
|
||||
>
|
||||
{page}
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
{nextPage && (
|
||||
<li>
|
||||
<a
|
||||
className="magnet-link"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handlePageChange(currentPage + 1);
|
||||
}}
|
||||
>
|
||||
<i className="ti ti-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-5 col-lg-4">
|
||||
<div className="d-flex flex-column gap-5">
|
||||
{/* Widget */}
|
||||
<div className="blog-widget">
|
||||
<div className="h4 fw-bold mb-4">Search Here</div>
|
||||
|
||||
{/* Form */}
|
||||
<form action="#" method="get">
|
||||
<input type="search" placeholder="Search here..." className="form-control" />
|
||||
<button type="submit">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"
|
||||
fill="none">
|
||||
<g clipPath="url(#clip0_1_17841)">
|
||||
<path
|
||||
d="M2.5 8.33333C2.5 9.09938 2.65088 9.85792 2.94404 10.5657C3.23719 11.2734 3.66687 11.9164 4.20854 12.4581C4.75022 12.9998 5.39328 13.4295 6.10101 13.7226C6.80875 14.0158 7.56729 14.1667 8.33333 14.1667C9.09938 14.1667 9.85792 14.0158 10.5657 13.7226C11.2734 13.4295 11.9164 12.9998 12.4581 12.4581C12.9998 11.9164 13.4295 11.2734 13.7226 10.5657C14.0158 9.85792 14.1667 9.09938 14.1667 8.33333C14.1667 7.56729 14.0158 6.80875 13.7226 6.10101C13.4295 5.39328 12.9998 4.75022 12.4581 4.20854C11.9164 3.66687 11.2734 3.23719 10.5657 2.94404C9.85792 2.65088 9.09938 2.5 8.33333 2.5C7.56729 2.5 6.80875 2.65088 6.10101 2.94404C5.39328 3.23719 4.75022 3.66687 4.20854 4.20854C3.66687 4.75022 3.23719 5.39328 2.94404 6.10101C2.65088 6.80875 2.5 7.56729 2.5 8.33333Z"
|
||||
stroke="white" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M17.5 17.5L12.5 12.5" stroke="white" strokeLinecap="round"
|
||||
strokeLinejoin="round" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1_17841">
|
||||
<rect width="20" height="20" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Widget - Categories */}
|
||||
<div className="blog-widget">
|
||||
<div className="h4 fw-bold mb-4">Categories</div>
|
||||
|
||||
<ul className="blog-list style-two">
|
||||
{categories.map((category) => {
|
||||
if (!category.is_active) return null;
|
||||
|
||||
return (
|
||||
<li key={category.slug}>
|
||||
<Link href={`/blog/category/${category.slug}`}>
|
||||
{category.title}
|
||||
<span>{getCategoryCount(category)}</span>
|
||||
</Link>
|
||||
{/* Child Categories */}
|
||||
{category.child && category.child.length > 0 && (
|
||||
<ul className="blog-list style-two ms-3 mt-2 mb-0" style={{ listStyle: 'none', paddingLeft: '1.5rem', borderLeft: '2px solid rgba(31, 30, 33, 0.2)' }}>
|
||||
{category.child
|
||||
.filter(child => child.is_active)
|
||||
.map((child) => (
|
||||
<li key={child.slug} style={{ marginTop: '0.5rem' }}>
|
||||
<Link href={`/blog/category/${child.slug}`} style={{ fontSize: '0.95em', color: '#666' }}>
|
||||
{child.title}
|
||||
<span>{getCategoryCount(child)}</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Widget - Recent Posts */}
|
||||
<div className="blog-widget">
|
||||
<div className="h4 fw-bold mb-4">Recent Posts</div>
|
||||
|
||||
<div className="d-flex flex-column gap-4">
|
||||
{recentPosts.length > 0 ? (
|
||||
recentPosts.map((post) => (
|
||||
<div key={post.slug} className="widget-blog-post">
|
||||
{post.thumb && (
|
||||
<div className="blog-thumbnail">
|
||||
<img src={post.thumb} alt={post.title} />
|
||||
</div>
|
||||
)}
|
||||
<div className="blog-content">
|
||||
<p className="mb-1 text-primary">{formatDate(post.created_at)}</p>
|
||||
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-muted">Henüz yazı bulunmamaktadır.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Widget - Tags */}
|
||||
{allTags.length > 0 && (
|
||||
<div className="blog-widget">
|
||||
<div className="h4 fw-bold mb-4">Tags</div>
|
||||
|
||||
<ul className="tag-list list-unstyled">
|
||||
{allTags.slice(0, 8).map((tag, index) => (
|
||||
<li key={index}>
|
||||
<a href={`/blog/tag/${tag.toLowerCase().replace(/\s+/g, '-')}`}>
|
||||
{tag}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="divider"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Extend Window interface
|
||||
declare global {
|
||||
interface Window {
|
||||
WOW: any;
|
||||
}
|
||||
}
|
||||
72
components/CTABottom.tsx
Normal file
72
components/CTABottom.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface CTABottomProps {
|
||||
userEmail?: string | null;
|
||||
isAuthenticated: boolean;
|
||||
}
|
||||
|
||||
export default function CTABottom({ userEmail, isAuthenticated }: CTABottomProps) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
|
||||
// Initialize WOW.js after mount
|
||||
if (typeof window !== "undefined" && window.WOW) {
|
||||
new window.WOW().init();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="cta-wrapper bg-img" style={{backgroundImage: 'url("/assets/img/core-img/grid.jpg")'}} suppressHydrationWarning>
|
||||
<div className="divider"></div>
|
||||
<div className="container">
|
||||
<div className="row g-4 g-xl-5 align-items-center">
|
||||
<div className="col-12 col-lg-6 col-xl-7">
|
||||
<h2
|
||||
className={`mb-0 ${mounted ? 'wow fadeInUp' : ''}`}
|
||||
data-wow-duration="1000ms"
|
||||
data-wow-delay="400ms"
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{isAuthenticated ? `Welcome back, ${userEmail}!` : "Start Building Your Business Now"}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6 col-xl-5">
|
||||
<p
|
||||
className={mounted ? 'wow fadeInUp' : ''}
|
||||
data-wow-duration="1000ms"
|
||||
data-wow-delay="600ms"
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{isAuthenticated
|
||||
? "Access your dashboard to manage your profile and explore all features."
|
||||
: "Create your account today and experience the power of our authentication system."}
|
||||
</p>
|
||||
<Link
|
||||
href={isAuthenticated ? "/dashboard" : "/auth/register"}
|
||||
className={`btn btn-primary btn-hover-border ${mounted ? 'wow fadeInUp' : ''}`}
|
||||
data-wow-duration="1000ms"
|
||||
data-wow-delay="800ms"
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{isAuthenticated ? "Go to Dashboard" : "Get Started"} <i className="ti ti-arrow-up-right"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="divider"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Extend Window interface
|
||||
declare global {
|
||||
interface Window {
|
||||
WOW: any;
|
||||
}
|
||||
}
|
||||
|
||||
135
components/CTASection.tsx
Normal file
135
components/CTASection.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
export default function CTASection() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const ctaRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
|
||||
// Initialize Jarallax manually after mount
|
||||
const initJarallax = () => {
|
||||
if (ctaRef.current && typeof window !== 'undefined' && (window as any).jarallax) {
|
||||
(window as any).jarallax(ctaRef.current, {
|
||||
speed: 0.6
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Delay to ensure jarallax library is loaded
|
||||
setTimeout(initJarallax, 200);
|
||||
|
||||
return () => {
|
||||
// Cleanup jarallax on unmount
|
||||
if (ctaRef.current && typeof window !== 'undefined' && (window as any).jarallax) {
|
||||
(window as any).jarallax(ctaRef.current, 'destroy');
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
// Server-side render: simple div without jarallax
|
||||
return (
|
||||
<div
|
||||
className="cta-wrap"
|
||||
style={{backgroundImage: 'url("/assets/img/bg-img/20.jpg")', backgroundSize: 'cover', backgroundPosition: 'center'}}
|
||||
>
|
||||
<div className="divider"></div>
|
||||
<div className="container">
|
||||
<div className="row justify-content-end">
|
||||
<div className="col-12 col-sm-11 col-md-10 col-lg-7 col-xl-6 col-xxl-5">
|
||||
<div className="cta-card">
|
||||
<div className="total-clients-wrap">
|
||||
<div className="total-number">
|
||||
<h3>200+</h3>
|
||||
<p className="mb-0">Satisfied Customers</p>
|
||||
</div>
|
||||
<div className="clients-images">
|
||||
<img src="/assets/img/bg-img/21.png" alt="" />
|
||||
<img src="/assets/img/bg-img/22.png" alt="" />
|
||||
<img src="/assets/img/bg-img/23.png" alt="" />
|
||||
<img src="/assets/img/bg-img/24.png" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="cta-stats">
|
||||
<div>
|
||||
<h2>100+</h2>
|
||||
<p className="mb-0">Global Clients</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>150+</h2>
|
||||
<p className="mb-0">Team Members</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>15+</h2>
|
||||
<p className="mb-0">Business Experience</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>300+</h2>
|
||||
<p className="mb-0">Projects Complete</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="divider"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Client-side render: with jarallax
|
||||
return (
|
||||
<div
|
||||
ref={ctaRef}
|
||||
className="cta-wrap jarallax"
|
||||
data-jarallax=""
|
||||
data-speed="0.6"
|
||||
style={{backgroundImage: 'url("/assets/img/bg-img/20.jpg")'}}
|
||||
>
|
||||
<div className="divider"></div>
|
||||
<div className="container">
|
||||
<div className="row justify-content-end">
|
||||
<div className="col-12 col-sm-11 col-md-10 col-lg-7 col-xl-6 col-xxl-5">
|
||||
<div className="cta-card">
|
||||
<div className="total-clients-wrap">
|
||||
<div className="total-number">
|
||||
<h3>200+</h3>
|
||||
<p className="mb-0">Satisfied Customers</p>
|
||||
</div>
|
||||
<div className="clients-images">
|
||||
<img src="/assets/img/bg-img/21.png" alt="" />
|
||||
<img src="/assets/img/bg-img/22.png" alt="" />
|
||||
<img src="/assets/img/bg-img/23.png" alt="" />
|
||||
<img src="/assets/img/bg-img/24.png" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="cta-stats">
|
||||
<div>
|
||||
<h2>100+</h2>
|
||||
<p className="mb-0">Global Clients</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>150+</h2>
|
||||
<p className="mb-0">Team Members</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>15+</h2>
|
||||
<p className="mb-0">Business Experience</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>300+</h2>
|
||||
<p className="mb-0">Projects Complete</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="divider"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
49
components/CookieAlert.tsx
Normal file
49
components/CookieAlert.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function CookieAlert() {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if cookie exists
|
||||
const cookieExists = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("acceptCookies="));
|
||||
|
||||
if (!cookieExists) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const acceptCookies = () => {
|
||||
// Set cookie for 365 days
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + 365 * 24 * 60 * 60 * 1000);
|
||||
document.cookie = `acceptCookies=true; expires=${date.toUTCString()}; path=/`;
|
||||
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<div className="cookiealert shadow-lg show">
|
||||
<p className="mb-4">
|
||||
We use cookies for the best experience on our website.{" "}
|
||||
<Link href="#" target="_blank">
|
||||
Cookies Policy.
|
||||
</Link>
|
||||
</p>
|
||||
<button
|
||||
className="btn btn-primary btn-sm acceptcookies"
|
||||
type="button"
|
||||
aria-label="Close"
|
||||
onClick={acceptCookies}
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
176
components/Header.tsx
Normal file
176
components/Header.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useSession, signOut } from "next-auth/react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export default function Header() {
|
||||
const { data: session } = useSession();
|
||||
const [isSticky, setIsSticky] = useState(false);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
// Track open state for dropdowns by their ID or Label
|
||||
const [openDropdowns, setOpenDropdowns] = useState<Record<string, boolean>>({});
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (window.scrollY > 10) {
|
||||
setIsSticky(true);
|
||||
} else {
|
||||
setIsSticky(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
const toggleMobileMenu = () => {
|
||||
setIsMobileMenuOpen(!isMobileMenuOpen);
|
||||
};
|
||||
|
||||
const toggleDropdown = (key: string, e?: React.MouseEvent) => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
setOpenDropdowns((prev) => ({
|
||||
...prev,
|
||||
[key]: !prev[key],
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`header-area style-two ${isSticky ? "sticky-on" : ""} ${isMobileMenuOpen ? "mobile-menu-open" : ""}`}
|
||||
>
|
||||
<div className="header-top">
|
||||
<div className="container h-100 d-flex align-items-center justify-content-between">
|
||||
{/* Left Side */}
|
||||
<div className="left-side d-flex align-items-center gap-4 gap-lg-5">
|
||||
<div className="d-flex align-items-center gap-2 text-white">
|
||||
<i className="ti ti-mail-filled"></i>
|
||||
<span className="d-none d-lg-block">info@example.com</span>
|
||||
</div>
|
||||
<div className="d-flex align-items-center gap-2 text-white">
|
||||
<i className="ti ti-map-pin-filled"></i>
|
||||
<span className="d-none d-lg-block">629 Elgin St.Celina,2202</span>
|
||||
</div>
|
||||
<div className="d-flex align-items-center gap-2 text-white">
|
||||
<i className="ti ti-phone"></i>
|
||||
<span className="d-none d-lg-block">(888).123.456.7894</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side */}
|
||||
<div className="right-side">
|
||||
<div className="social-nav d-flex align-items-center gap-3">
|
||||
<a href="#"><i className="ti ti-brand-facebook"></i></a>
|
||||
<a href="#"><i className="ti ti-brand-x"></i></a>
|
||||
<a href="#"><i className="ti ti-brand-linkedin"></i></a>
|
||||
<a href="#"><i className="ti ti-brand-instagram"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="navbar navbar-expand-lg">
|
||||
<div className="container">
|
||||
{/* Navbar Brand */}
|
||||
<Link className="navbar-brand" href="/">
|
||||
<img src="/assets/img/core-img/logo.png" alt="" />
|
||||
</Link>
|
||||
|
||||
{/* Navbar Toggler */}
|
||||
<button
|
||||
className="navbar-toggler"
|
||||
type="button"
|
||||
onClick={toggleMobileMenu}
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<i className="ti ti-category"></i>
|
||||
</button>
|
||||
|
||||
{/* Navbar Nav */}
|
||||
<div className={`collapse navbar-collapse justify-content-between ${isMobileMenuOpen ? "show" : ""}`} id="softoraNav">
|
||||
<ul className="navbar-nav navbar-nav-scroll">
|
||||
<li className="softora-dd">
|
||||
<Link href="/">Home</Link>
|
||||
</li>
|
||||
|
||||
{/* Features Dropdown Example based on Template */}
|
||||
<li className="softora-dd">
|
||||
<a href="#" onClick={(e) => toggleDropdown('features', e)}>
|
||||
Features <i className="ti ti-caret-down-filled"></i>
|
||||
</a>
|
||||
{/* Mobile Toggler */}
|
||||
<div className="dropdown-toggler d-lg-none" onClick={(e) => toggleDropdown('features', e)}>
|
||||
<i className="ti ti-caret-down-filled"></i>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
className={`softora-dd-menu ${openDropdowns['features'] ? 'menu-open' : ''}`}
|
||||
>
|
||||
<li><Link href="#services">Services</Link></li>
|
||||
<li><Link href="/auth/register">Register</Link></li>
|
||||
<li><Link href="/auth/login">Login</Link></li>
|
||||
<li><Link href="/auth/forgot-password">Reset Password</Link></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li className="softora-dd">
|
||||
<a href="#" onClick={(e) => toggleDropdown('utils', e)}>
|
||||
Yardımcılar <i className="ti ti-caret-down-filled"></i>
|
||||
</a>
|
||||
{/* Mobile Toggler */}
|
||||
<div className="dropdown-toggler d-lg-none" onClick={(e) => toggleDropdown('utils', e)}>
|
||||
<i className="ti ti-caret-down-filled"></i>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
className={`softora-dd-menu ${openDropdowns['utils'] ? 'menu-open' : ''}`}
|
||||
>
|
||||
<li><Link href="/assistants/converters/jsontotype">Json To Type</Link></li>
|
||||
<li><Link href="/auth/login">Login</Link></li>
|
||||
<li><Link href="/auth/forgot-password">Reset Password</Link></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
{session ? (
|
||||
<>
|
||||
<li className="softora-dd">
|
||||
<Link href="/dashboard">Dashboard</Link>
|
||||
</li>
|
||||
<li className="softora-dd">
|
||||
<Link href="/profile">Profile</Link>
|
||||
</li>
|
||||
</>
|
||||
) : null}
|
||||
</ul>
|
||||
|
||||
<div className="d-flex align-items-center mt-4 mt-lg-0">
|
||||
{/* Search Button */}
|
||||
<div className="header-search-btn" id="searchButton">
|
||||
<button className="btn">
|
||||
<i className="ti ti-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Button */}
|
||||
{session ? (
|
||||
<button
|
||||
onClick={() => signOut({ callbackUrl: "/auth/login" })}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
Logout <i className="ti ti-logout"></i>
|
||||
</button>
|
||||
) : (
|
||||
<Link href="/auth/register" className="btn btn-primary">
|
||||
Get Started <i className="ti ti-arrow-up-right"></i>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
71
components/HeroSection.tsx
Normal file
71
components/HeroSection.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface HeroSectionProps {
|
||||
isAuthenticated: boolean;
|
||||
}
|
||||
|
||||
export default function HeroSection({ isAuthenticated }: HeroSectionProps) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
|
||||
// Initialize WOW.js after mount
|
||||
if (typeof window !== "undefined" && window.WOW) {
|
||||
new window.WOW().init();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="hero-section" style={{backgroundImage: 'url("/assets/img/core-img/grid3.png")'}} suppressHydrationWarning>
|
||||
<div className="divider"></div>
|
||||
<div className="container">
|
||||
<div className="hero-content">
|
||||
<div className="row g-5">
|
||||
<div className="col-12 col-md-6">
|
||||
<h2
|
||||
className={`mb-0 text-white ${mounted ? 'wow fadeInUp' : ''}`}
|
||||
data-wow-duration="1000ms"
|
||||
data-wow-delay="400ms"
|
||||
suppressHydrationWarning
|
||||
>
|
||||
Best IT <span>Solution</span> Agency For Your Business
|
||||
</h2>
|
||||
</div>
|
||||
<div className="col-12 col-md-6 col-xl-5 offset-xl-1 col-xxl-4 offset-xxl-1">
|
||||
<p
|
||||
className={`text-white ${mounted ? 'wow fadeInUp' : ''}`}
|
||||
data-wow-duration="1000ms"
|
||||
data-wow-delay="600ms"
|
||||
suppressHydrationWarning
|
||||
>
|
||||
At Solvexa, we are dedicated transforming your digital aspirations into reality. With a passion for innovation and a commitment to excellence.
|
||||
</p>
|
||||
<Link
|
||||
className={`btn border-2 btn-outline-light ${mounted ? 'wow fadeInUp' : ''}`}
|
||||
data-wow-duration="1000ms"
|
||||
data-wow-delay="800ms"
|
||||
href={isAuthenticated ? "/dashboard" : "/auth/register"}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{isAuthenticated ? "Go to Dashboard" : "Get Started"} <i className="ti ti-arrow-up-right"></i>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="divider"></div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Extend Window interface
|
||||
declare global {
|
||||
interface Window {
|
||||
WOW: any;
|
||||
}
|
||||
}
|
||||
|
||||
75
components/PreloaderAndSearch.tsx
Normal file
75
components/PreloaderAndSearch.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function PreloaderAndSearch() {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Preloader'ı kapat
|
||||
const timer = setTimeout(() => {
|
||||
setIsLoaded(true);
|
||||
}, 500);
|
||||
|
||||
// Search button click handler
|
||||
const searchButton = document.getElementById('searchButton');
|
||||
const searchClose = document.getElementById('searchClose');
|
||||
const searchOverlay = document.getElementById('searchOverlay');
|
||||
const searchPopup = document.querySelector('.search-form-popup');
|
||||
|
||||
const openSearch = () => {
|
||||
searchOverlay?.classList.add('active');
|
||||
searchPopup?.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
};
|
||||
|
||||
const closeSearch = () => {
|
||||
searchOverlay?.classList.remove('active');
|
||||
searchPopup?.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
|
||||
searchButton?.addEventListener('click', openSearch);
|
||||
searchClose?.addEventListener('click', closeSearch);
|
||||
searchOverlay?.addEventListener('click', closeSearch);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
searchButton?.removeEventListener('click', openSearch);
|
||||
searchClose?.removeEventListener('click', closeSearch);
|
||||
searchOverlay?.removeEventListener('click', closeSearch);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Preloader */}
|
||||
{!isLoaded && (
|
||||
<div className="preloader" id="preloader">
|
||||
<div className="spinner-grow" role="status">
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search Form Overlay */}
|
||||
<div className="search-bg-overlay" id="searchOverlay"></div>
|
||||
|
||||
{/* Search Form Popup */}
|
||||
<div className="search-form-popup">
|
||||
<h2 className="mb-4">How can I help you, Today?</h2>
|
||||
<button type="button" className="close-btn" id="searchClose" aria-label="Close">
|
||||
<i className="ti ti-x"></i>
|
||||
</button>
|
||||
<form className="search-form">
|
||||
<input type="search" className="form-control" placeholder="Search..." />
|
||||
<button type="submit" className="btn btn-primary">
|
||||
<i className="ti ti-search"></i> Search
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user