This commit is contained in:
Vendored
BIN
Binary file not shown.
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
function AboutPage() {
|
||||
return <div>AboutPage</div>;
|
||||
}
|
||||
|
||||
export default AboutPage;
|
||||
@@ -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 />;
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 />;
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default async function Page() {
|
||||
redirect('/home');
|
||||
}
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user