first
Deploy Iddaai Frontend / build-and-deploy (push) Successful in 4m0s

This commit is contained in:
2026-04-16 13:36:34 +03:00
parent de5e145c4e
commit fc7a1ba567
218 changed files with 32370 additions and 0 deletions
BIN
View File
Binary file not shown.
+15
View File
@@ -0,0 +1,15 @@
'use client';
import Footer from '@/components/layout/footer/footer';
import { Box, Flex } from '@chakra-ui/react';
function AuthLayout({ children }: { children: React.ReactNode }) {
return (
<Flex minH='100vh' direction='column'>
<Box as='main'>{children}</Box>
<Footer />
</Flex>
);
}
export default AuthLayout;
+231
View File
@@ -0,0 +1,231 @@
"use client";
import {
Box,
Flex,
Heading,
Input,
Link as ChakraLink,
Text,
ClientOnly,
} from "@chakra-ui/react";
import { Button } from "@/components/ui/buttons/button";
import { Switch } from "@/components/ui/forms/switch";
import { Field } from "@/components/ui/forms/field";
import { useTranslations } from "next-intl";
import signInImage from "../../../../../public/assets/img/sign-in-image.png";
import { InputGroup } from "@/components/ui/forms/input-group";
import { BiLock } from "react-icons/bi";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { Link, useRouter } from "@/i18n/navigation";
import { MdMail } from "react-icons/md";
import { PasswordInput } from "@/components/ui/forms/password-input";
import { Skeleton } from "@/components/ui/feedback/skeleton";
import { signIn } from "next-auth/react";
import { toaster } from "@/components/ui/feedback/toaster";
import { useState } from "react";
const schema = yup.object({
email: yup.string().email().required(),
password: yup.string().required(),
});
type SignInForm = yup.InferType<typeof schema>;
const defaultValues = {
email: "test@test.com.tr",
password: "test1234",
};
function SignInPage() {
const t = useTranslations();
const router = useRouter();
const [loading, setLoading] = useState(false);
const {
handleSubmit,
register,
formState: { errors },
} = useForm<SignInForm>({
resolver: yupResolver(schema),
mode: "onChange",
defaultValues,
});
const onSubmit = async (formData: SignInForm) => {
try {
setLoading(true);
const res = await signIn("credentials", {
redirect: false,
email: formData.email,
password: formData.password,
});
if (res?.error) {
throw new Error(res.error);
}
router.replace("/home");
} catch (error) {
toaster.error({
title: (error as Error).message || "Giriş yaparken hata oluştu!",
type: "error",
});
} finally {
setLoading(false);
}
};
return (
<Box position="relative">
<Flex
h={{ sm: "initial", md: "75vh", lg: "85vh" }}
w="100%"
maxW="1044px"
mx="auto"
justifyContent="space-between"
mb="30px"
pt={{ sm: "100px", md: "0px" }}
>
<Flex
as="form"
onSubmit={handleSubmit(onSubmit)}
alignItems="center"
justifyContent="start"
style={{ userSelect: "none" }}
w={{ base: "100%", md: "50%", lg: "42%" }}
>
<Flex
direction="column"
w="100%"
background="transparent"
p="10"
mt={{ md: "150px", lg: "80px" }}
>
<Heading
color={{ base: "primary.400", _dark: "primary.200" }}
fontSize="32px"
mb="10px"
fontWeight="bold"
>
{t("auth.welcome-back")}
</Heading>
<Text
mb="36px"
ms="4px"
color={{ base: "gray.400", _dark: "white" }}
fontWeight="bold"
fontSize="14px"
>
{t("auth.subtitle")}
</Text>
<Field
mb="24px"
label={t("email")}
errorText={errors.email?.message}
invalid={!!errors.email}
>
<InputGroup w="full" startElement={<MdMail size="1rem" />}>
<Input
borderRadius="15px"
fontSize="sm"
type="text"
placeholder={t("email")}
size="lg"
{...register("email")}
/>
</InputGroup>
</Field>
<Field
mb="24px"
label={t("password")}
errorText={errors.password?.message}
invalid={!!errors.password}
>
<InputGroup w="full" startElement={<BiLock size="1rem" />}>
<PasswordInput
borderRadius="15px"
fontSize="sm"
placeholder={t("password")}
size="lg"
{...register("password")}
/>
</InputGroup>
</Field>
<Field mb="24px">
<Switch colorPalette="teal" label={t("auth.remember-me")}>
{t("auth.remember-me")}
</Switch>
</Field>
<Field mb="24px">
<ClientOnly fallback={<Skeleton height="45px" width="100%" />}>
<Button
loading={loading}
type="submit"
bg="primary.400"
w="100%"
h="45px"
color="white"
_hover={{
bg: "primary.500",
}}
_active={{
bg: "primary.400",
}}
>
{t("auth.sign-in")}
</Button>
</ClientOnly>
</Field>
<Flex
flexDirection="column"
justifyContent="center"
alignItems="center"
maxW="100%"
>
<Text
color={{ base: "gray.400", _dark: "white" }}
fontWeight="medium"
>
{t("auth.dont-have-account")}
<ChakraLink
as={Link}
href="/signup"
color={{ base: "primary.400", _dark: "primary.200" }}
ms="5px"
fontWeight="bold"
focusRing="none"
>
{t("auth.sign-up")}
</ChakraLink>
</Text>
</Flex>
</Flex>
</Flex>
<Box
display={{ base: "none", md: "block" }}
overflowX="hidden"
h="100%"
w="40vw"
position="absolute"
right="0px"
>
<Box
bgImage={`url(${signInImage.src})`}
w="100%"
h="100%"
bgSize="cover"
bgPos="50%"
position="absolute"
borderBottomLeftRadius="20px"
/>
</Box>
</Flex>
</Box>
);
}
export default SignInPage;
+219
View File
@@ -0,0 +1,219 @@
"use client";
import {
Box,
Flex,
Input,
Link as ChakraLink,
Text,
ClientOnly,
} from "@chakra-ui/react";
import signUpImage from "../../../../../public/assets/img/sign-up-image.png";
import { Button } from "@/components/ui/buttons/button";
import { Field } from "@/components/ui/forms/field";
import { useTranslations } from "next-intl";
import { useForm } from "react-hook-form";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import { InputGroup } from "@/components/ui/forms/input-group";
import { BiLock, BiUser } from "react-icons/bi";
import { Link } from "@/i18n/navigation";
import { MdMail } from "react-icons/md";
import { useRouter } from "next/navigation";
import { PasswordInput } from "@/components/ui/forms/password-input";
import { Skeleton } from "@/components/ui/feedback/skeleton";
import { authService } from "@/lib/api/example/auth/service";
import { useState } from "react";
const schema = yup.object({
name: yup.string().required(),
email: yup.string().email().required(),
password: yup.string().min(8).required(),
});
type SignUpForm = yup.InferType<typeof schema>;
function SignUpPage() {
const t = useTranslations();
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const {
handleSubmit,
register,
formState: { errors },
} = useForm<SignUpForm>({ resolver: yupResolver(schema), mode: "onChange" });
const onSubmit = async (formData: SignUpForm) => {
setIsSubmitting(true);
try {
await authService.register({
email: formData.email,
password: formData.password,
firstName: formData.name,
lastName: "",
});
router.replace("/signin");
} catch {
// Error handled by api-service interceptor (toast + 422 display)
} finally {
setIsSubmitting(false);
}
return formData;
};
return (
<Box>
<Box
position="absolute"
minH={{ base: "70vh", md: "50vh" }}
w={{ md: "calc(100vw - 50px)" }}
borderRadius={{ md: "15px" }}
left="0"
right="0"
bgRepeat="no-repeat"
overflow="hidden"
zIndex="-1"
top="0"
bgImage={`url(${signUpImage.src})`}
bgSize="cover"
mx={{ md: "auto" }}
mt={{ md: "14px" }}
/>
<Flex
w="full"
h="full"
direction="column"
alignItems="center"
justifyContent="center"
>
<Text
fontSize={{ base: "2xl", md: "3xl", lg: "4xl" }}
color="white"
fontWeight="bold"
mt={{ base: "2rem", md: "4.5rem", "2xl": "6.5rem" }}
mb={{ base: "2rem", md: "3rem", "2xl": "4rem" }}
>
{t("auth.create-an-account-now")}
</Text>
<Flex
direction="column"
w={{ base: "100%", md: "445px" }}
background="transparent"
borderRadius="15px"
p="10"
mx={{ base: "100px" }}
bg="bg.panel"
boxShadow="0 20px 27px 0 rgb(0 0 0 / 5%)"
mb="8"
>
<Flex
as="form"
onSubmit={handleSubmit(onSubmit)}
flexDirection="column"
alignItems="center"
justifyContent="start"
w="100%"
>
<Field
mb="24px"
label={t("name")}
errorText={errors.name?.message}
invalid={!!errors.name}
>
<InputGroup w="full" startElement={<BiUser size="1rem" />}>
<Input
borderRadius="15px"
fontSize="sm"
type="text"
placeholder={t("name")}
size="lg"
{...register("name")}
/>
</InputGroup>
</Field>
<Field
mb="24px"
label={t("email")}
errorText={errors.email?.message}
invalid={!!errors.email}
>
<InputGroup w="full" startElement={<MdMail size="1rem" />}>
<Input
borderRadius="15px"
fontSize="sm"
type="text"
placeholder={t("email")}
size="lg"
{...register("email")}
/>
</InputGroup>
</Field>
<Field
mb="24px"
label={t("password")}
errorText={errors.password?.message}
invalid={!!errors.password}
>
<InputGroup w="full" startElement={<BiLock size="1rem" />}>
<PasswordInput
borderRadius="15px"
fontSize="sm"
placeholder={t("password")}
size="lg"
{...register("password")}
/>
</InputGroup>
</Field>
<Field mb="24px">
<ClientOnly fallback={<Skeleton height="45px" width="100%" />}>
<Button
type="submit"
bg="primary.400"
color="white"
fontWeight="bold"
w="100%"
h="45px"
_hover={{
bg: "primary.500",
}}
_active={{
bg: "primary.400",
}}
loading={isSubmitting}
>
{isSubmitting ? t("auth.registering") : t("auth.sign-up")}
</Button>
</ClientOnly>
</Field>
</Flex>
<Flex
flexDirection="column"
justifyContent="center"
alignItems="center"
maxW="100%"
>
<Text
color={{ base: "gray.400", _dark: "white" }}
fontWeight="medium"
>
{t("auth.already-have-an-account")}
<ChakraLink
as={Link}
color={{ base: "primary.400", _dark: "primary.200" }}
ml="2"
href="/signin"
fontWeight="bold"
focusRing="none"
>
{t("auth.sign-in")}
</ChakraLink>
</Text>
</Flex>
</Flex>
</Flex>
</Box>
);
}
export default SignUpPage;
@@ -0,0 +1,5 @@
import { notFound } from 'next/navigation';
export default function CatchAllPage() {
notFound();
}
+7
View File
@@ -0,0 +1,7 @@
import React from 'react';
function AboutPage() {
return <div>AboutPage</div>;
}
export default AboutPage;
+15
View File
@@ -0,0 +1,15 @@
import { getTranslations } from "next-intl/server";
import AdminContent from "@/components/admin/admin-content";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `${t("admin.title")} | Suggest Bet`,
description:
"Admin panel for managing users, monitoring predictions, and system overview.",
};
}
export default function AdminPage() {
return <AdminContent />;
}
+14
View File
@@ -0,0 +1,14 @@
import { getTranslations } from "next-intl/server";
import AnalysisContent from "@/components/analysis/analysis-content";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `${t("analysis.title")} | Suggest Bet`,
description: "AI-powered multi-match analysis for coupon generation.",
};
}
export default function AnalysisPage() {
return <AnalysisContent />;
}
@@ -0,0 +1,15 @@
import { getTranslations } from "next-intl/server";
import CouponBuilderContent from "@/components/coupons/coupon-builder-content";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `${t("coupons.builder-title")} | Suggest Bet`,
description:
"Build your coupon with AI-powered suggestions. Choose your strategy and let AI optimize your bets.",
};
}
export default function CouponBuilderPage() {
return <CouponBuilderContent />;
}
@@ -0,0 +1,15 @@
import { getTranslations } from "next-intl/server";
import CouponHistoryContent from "@/components/coupons/coupon-history-content";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `${t("coupons.history-title")} | Suggest Bet`,
description:
"View your coupon history, track wins and losses, and analyze your betting performance.",
};
}
export default function CouponHistoryPage() {
return <CouponHistoryContent />;
}
@@ -0,0 +1,16 @@
import { getTranslations } from "next-intl/server";
import DashboardContent from "@/components/dashboard/dashboard-content";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `${t("dashboard.title")} | Suggest Bet`,
description:
"Your personalized betting dashboard with predictions, value bets, and match insights.",
};
}
export default function DashboardPage() {
return <DashboardContent />;
}
+14
View File
@@ -0,0 +1,14 @@
import { getTranslations } from "next-intl/server";
import H2HContent from "@/components/h2h/h2h-content";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `${t("matches.head-to-head")} | Suggest Bet`,
description: "Compare two teams and view their head-to-head match history.",
};
}
export default function H2HPage() {
return <H2HContent />;
}
+16
View File
@@ -0,0 +1,16 @@
import { getTranslations } from "next-intl/server";
import HomeContent from "@/components/home/home-content";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `${t("home")} | Suggest Bet`,
description:
"AI-powered betting predictions. Analyze matches, discover value bets, and build winning coupons.",
};
}
export default function Home() {
return <HomeContent />;
}
+21
View File
@@ -0,0 +1,21 @@
'use client';
import { Container, Flex } from '@chakra-ui/react';
import Header from '@/components/layout/header/header';
import Footer from '@/components/layout/footer/footer';
import BackToTop from '@/components/ui/back-to-top';
function MainLayout({ children }: { children: React.ReactNode }) {
return (
<Flex minH='100vh' direction='column'>
<Header />
<Container as='main' maxW='8xl' flex='1' py={4}>
{children}
</Container>
<BackToTop />
<Footer />
</Flex>
);
}
export default MainLayout;
+14
View File
@@ -0,0 +1,14 @@
import { getTranslations } from "next-intl/server";
import LeaguesContent from "@/components/leagues/leagues-content";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `${t("leagues.title")} | Suggest Bet`,
description: "Browse football and basketball leagues, countries, and teams.",
};
}
export default function LeaguesPage() {
return <LeaguesContent />;
}
@@ -0,0 +1,14 @@
import { getTranslations } from "next-intl/server";
import MatchDetailContent from "@/components/matches/match-detail-content";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `${t("matches.match-details")} | Suggest Bet`,
};
}
export default function MatchDetailPage() {
return <MatchDetailContent />;
}
+16
View File
@@ -0,0 +1,16 @@
import { getTranslations } from "next-intl/server";
import MatchesContent from "@/components/matches/matches-content";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `${t("matches.title")} | Suggest Bet`,
description:
"Browse and analyze upcoming football and basketball matches with AI predictions.",
};
}
export default function MatchesPage() {
return <MatchesContent />;
}
@@ -0,0 +1,15 @@
import { getTranslations } from "next-intl/server";
import PredictionsContent from "@/components/predictions/predictions-content";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `${t("predictions.title")} | Suggest Bet`,
description:
"AI-powered match predictions with confidence scores, value bets, and prediction history.",
};
}
export default function PredictionsPage() {
return <PredictionsContent />;
}
+15
View File
@@ -0,0 +1,15 @@
import { getTranslations } from "next-intl/server";
import ProfileContent from "@/components/profile/profile-content";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `${t("profile.title")} | Suggest Bet`,
description:
"Manage your profile, view account info, and track your betting statistics.",
};
}
export default function ProfilePage() {
return <ProfileContent />;
}
@@ -0,0 +1,15 @@
import { getTranslations } from "next-intl/server";
import SporTotoContent from "@/components/spor-toto/spor-toto-content";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `${t("spor-toto.title")} | Suggest Bet`,
description:
"Spor Toto predictions with AI-powered analysis. Generate optimized system coupons with contrarian parimutuel strategy.",
};
}
export default function SporTotoPage() {
return <SporTotoContent />;
}
@@ -0,0 +1,13 @@
import { getTranslations } from "next-intl/server";
import TeamDetailContent from "@/components/teams/team-detail-content";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `${t("nav.teams")} | Suggest Bet`,
};
}
export default function TeamDetailPage() {
return <TeamDetailContent />;
}
+14
View File
@@ -0,0 +1,14 @@
import { getTranslations } from "next-intl/server";
import TeamsContent from "@/components/teams/teams-content";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `${t("nav.teams")} | Suggest Bet`,
description: "Search and explore football teams, view match history and stats.",
};
}
export default function TeamsPage() {
return <TeamsContent />;
}
+160
View File
@@ -0,0 +1,160 @@
/* ═══════════════════════════════════════════════════════
Suggest-Bet — Global CSS
Premium animations, gradients, and utility keyframes
═══════════════════════════════════════════════════════ */
html {
scroll-behavior: smooth;
}
body {
overflow-x: hidden;
}
/* ──────────────────────────────────
Custom Animation Keyframes
────────────────────────────────── */
/* Pulsing live indicator */
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.3); }
}
/* Glow ring for CTAs */
@keyframes glow-ring {
0% { box-shadow: 0 0 0 0 rgba(56, 178, 172, 0.4); }
70% { box-shadow: 0 0 0 12px rgba(56, 178, 172, 0); }
100% { box-shadow: 0 0 0 0 rgba(56, 178, 172, 0); }
}
/* Shimmer for skeleton loading */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* Gradient background shift */
@keyframes gradient-shift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* Sparkle float */
@keyframes sparkle-float {
0%, 100% { transform: translateY(0) rotate(0deg); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translateY(-120px) rotate(360deg); opacity: 0; }
}
/* Subtle float for decorative elements */
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
/* Gradient text shimmer */
@keyframes text-shimmer {
0% { background-position: 0% 50%; }
100% { background-position: 200% 50%; }
}
/* Rotate for spinners */
@keyframes spin-slow {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Fade in up — CSS fallback for non-JS */
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* ──────────────────────────────────
Utility Classes
────────────────────────────────── */
.animate-pulse { animation: pulse 1.5s ease-in-out infinite; }
.animate-glow { animation: glow-ring 2s infinite; }
.animate-shimmer {
background: linear-gradient(90deg, transparent 25%, rgba(255,255,255,0.08) 50%, transparent 75%);
background-size: 200% 100%;
animation: shimmer 1.8s ease-in-out infinite;
}
.animate-float { animation: float 3s ease-in-out infinite; }
.animate-spin-slow { animation: spin-slow 8s linear infinite; }
/* Gradient mesh background */
.gradient-mesh {
background:
radial-gradient(at 20% 20%, rgba(56, 178, 172, 0.15) 0%, transparent 50%),
radial-gradient(at 80% 80%, rgba(128, 90, 213, 0.12) 0%, transparent 50%),
radial-gradient(at 50% 50%, rgba(66, 153, 225, 0.08) 0%, transparent 70%);
}
/* Dark mode gradient mesh */
[data-theme="dark"] .gradient-mesh,
.dark .gradient-mesh {
background:
radial-gradient(at 20% 20%, rgba(56, 178, 172, 0.08) 0%, transparent 50%),
radial-gradient(at 80% 80%, rgba(128, 90, 213, 0.06) 0%, transparent 50%),
radial-gradient(at 50% 50%, rgba(66, 153, 225, 0.04) 0%, transparent 70%);
}
/* Animated gradient text */
.gradient-text {
background: linear-gradient(135deg, #38B2AC, #805AD5, #4299E1, #38B2AC);
background-size: 300% 300%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: text-shimmer 4s ease infinite;
}
/* Glassmorphism card */
.glass {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.3);
}
[data-theme="dark"] .glass,
.dark .glass {
background: rgba(26, 32, 44, 0.7);
border: 1px solid rgba(255, 255, 255, 0.08);
}
/* ──────────────────────────────────
Scrollbar Styling
────────────────────────────────── */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(128, 128, 128, 0.3);
border-radius: 999px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(128, 128, 128, 0.5);
}
/* ──────────────────────────────────
Selection Color
────────────────────────────────── */
::selection {
background: rgba(56, 178, 172, 0.3);
color: inherit;
}
+46
View File
@@ -0,0 +1,46 @@
import { Provider } from "@/components/ui/provider";
import { Bricolage_Grotesque } from "next/font/google";
import { hasLocale, NextIntlClientProvider } from "next-intl";
import { notFound } from "next/navigation";
import { routing } from "@/i18n/routing";
import { dir } from "i18next";
import "./global.css";
const bricolage = Bricolage_Grotesque({
variable: "--font-bricolage",
subsets: ["latin"],
});
export default async function RootLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
if (!hasLocale(routing.locales, locale)) {
notFound();
}
return (
<html
lang={locale}
dir={dir(locale)}
suppressHydrationWarning
data-scroll-behavior="smooth"
>
<head>
{/* <link rel='apple-touch-icon' sizes='180x180' href='/favicon/apple-touch-icon.png' />
<link rel='icon' type='image/png' sizes='32x32' href='/favicon/favicon-32x32.png' />
<link rel='icon' type='image/png' sizes='16x16' href='/favicon/favicon-16x16.png' /> */}
<link rel="manifest" href="/favicon/site.webmanifest" />
</head>
<body className={bricolage.variable}>
<NextIntlClientProvider>
<Provider>{children}</Provider>
</NextIntlClientProvider>
</body>
</html>
);
}
+30
View File
@@ -0,0 +1,30 @@
import { Link } from '@/i18n/navigation';
import { Flex, Text, Button, VStack, Heading } from '@chakra-ui/react';
import { getTranslations } from 'next-intl/server';
export default async function NotFoundPage() {
const t = await getTranslations();
return (
<Flex h='100vh' alignItems='center' justifyContent='center' textAlign='center' px={6}>
<VStack spaceY={6}>
<Heading
as='h1'
fontSize={{ base: '5xl', md: '6xl' }}
fontWeight='bold'
color={{ base: 'primary.600', _dark: 'primary.400' }}
>
{t('error.404')}
</Heading>
<Text fontSize={{ base: 'md', md: 'lg' }} color={{ base: 'fg.muted', _dark: 'white' }}>
{t('error.not-found')}
</Text>
<Link href='/home' passHref>
<Button size={{ base: 'md', md: 'lg' }} rounded='md'>
{t('error.back-to-home')}
</Button>
</Link>
</VStack>
</Flex>
);
}
+5
View File
@@ -0,0 +1,5 @@
import { redirect } from 'next/navigation';
export default async function Page() {
redirect('/home');
}
+123
View File
@@ -0,0 +1,123 @@
import { authService } from "@/lib/api/example/auth/service";
import NextAuth from "next-auth";
import type { NextAuthOptions } from "next-auth";
import type { JWT } from "next-auth/jwt";
import type { Session, User } from "next-auth";
import Credentials from "next-auth/providers/credentials";
function randomToken() {
return Math.random().toString(36).substring(2) + Date.now().toString(36);
}
const isMockMode = process.env.NEXT_PUBLIC_ENABLE_MOCK_MODE === "true";
const authOptions: NextAuthOptions = {
providers: [
Credentials({
name: "Credentials",
credentials: {
email: { label: "Email", type: "text" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
try {
console.log("Starting authorization with:", {
email: credentials?.email,
});
if (!credentials?.email || !credentials?.password) {
throw new Error("Email ve şifre gereklidir.");
}
// Eğer mock mod aktifse backend'e gitme
if (isMockMode) {
console.log("Mock mode active, bypassing backend");
return {
id: credentials.email,
name: credentials.email.split("@")[0],
email: credentials.email,
accessToken: randomToken(),
refreshToken: randomToken(),
};
}
// Normal mod: backend'e istek at
console.log("Sending login request to backend...");
const res = await authService.login({
email: credentials.email,
password: credentials.password,
});
console.log(
"Backend response received:",
JSON.stringify(res, null, 2),
);
const response = res;
// Backend returns ApiResponse<TokenResponseDto>
// Structure: { data: { accessToken, refreshToken, expiresIn, user }, message, statusCode }
if (!res.success || !response?.data?.accessToken) {
console.error("Login failed or no access token in response");
throw new Error(response?.message || "Giriş başarısız");
}
const { accessToken, refreshToken, user } = response.data;
console.log("Login successful, creating user session object");
return {
id: user.id,
name: user.firstName
? `${user.firstName} ${user.lastName || ""}`.trim()
: user.email.split("@")[0],
email: user.email,
accessToken,
refreshToken,
roles: user.roles || [],
};
} catch (error: unknown) {
console.error("Authorize error detailed:", error);
const err = error as Error & {
response?: { data: unknown; status: number };
};
if (err.response) {
console.error("Error response data:", err.response.data);
console.error("Error response status:", err.response.status);
}
throw new Error(
err.message || "An error occurred during authentication",
);
}
},
}),
],
callbacks: {
async jwt({ token, user }: { token: JWT; user?: User }) {
if (user) {
token.accessToken = user.accessToken;
token.refreshToken = user.refreshToken;
token.id = user.id;
token.roles = user.roles;
}
return token;
},
async session({ session, token }: { session: Session; token: JWT }) {
session.user.id = token.id;
session.user.roles = token.roles;
session.accessToken = token.accessToken;
session.refreshToken = token.refreshToken;
return session;
},
},
pages: {
signIn: "/signin",
error: "/signin",
},
session: { strategy: "jwt" },
secret: process.env.NEXTAUTH_SECRET,
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };