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
+1
View File
@@ -0,0 +1 @@
export { default as ProfileContent } from "./profile-content";
+422
View File
@@ -0,0 +1,422 @@
"use client";
import {
Box,
Flex,
Heading,
Text,
Card,
VStack,
HStack,
Separator,
Spinner,
Input,
Button,
IconButton,
} from "@chakra-ui/react";
import { useTranslations } from "next-intl";
import { useColorModeValue } from "@/components/ui/color-mode";
import { SlideUp } from "@/components/motion";
import { Avatar } from "@/components/ui/data-display/avatar";
import { useSession } from "next-auth/react";
import { useUserBettingStats } from "@/lib/api/coupons/use-hooks";
import type { UserBettingStatsDto } from "@/lib/api/coupons/types";
import {
LuMail,
LuUser,
LuCalendar,
LuShield,
LuTrendingUp,
LuTarget,
LuTicket,
LuPen,
LuCheck,
LuX,
LuLock,
} from "react-icons/lu";
import { useState } from "react";
import { useUpdateProfile, useChangePassword } from "@/lib/api/users/use-hooks";
import { Field } from "@/components/ui/forms/field";
import { useForm } from "react-hook-form";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import { PasswordInput } from "@/components/ui/forms/password-input";
import { useRouter } from "next/navigation";
interface InfoRowProps {
icon: React.ReactNode;
label: string;
value: string;
}
function InfoRow({ icon, label, value }: InfoRowProps) {
return (
<Flex justify="space-between" align="center" py={2}>
<HStack gap={2} color="fg.muted">
{icon}
<Text fontSize="sm">{label}</Text>
</HStack>
<Text fontSize="sm" fontWeight="semibold">
{value}
</Text>
</Flex>
);
}
const profileSchema = yup.object({
firstName: yup.string().required(),
lastName: yup.string().required(),
});
type ProfileForm = yup.InferType<typeof profileSchema>;
const passwordSchema = yup.object({
currentPassword: yup.string().required(),
newPassword: yup.string().min(8).required(),
confirmPassword: yup
.string()
.oneOf([yup.ref("newPassword")], "Passwords must match")
.required(),
});
type PasswordForm = yup.InferType<typeof passwordSchema>;
export default function ProfileContent() {
const t = useTranslations("profile");
const tCommon = useTranslations("common");
const { data: session, update: updateSession } = useSession();
const { data: statsData, isLoading: statsLoading } = useUserBettingStats();
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.100", "gray.700");
const user = session?.user;
const stats = statsData?.data as UserBettingStatsDto | undefined;
// Edit profile state
const [isEditing, setIsEditing] = useState(false);
const updateProfile = useUpdateProfile();
const {
handleSubmit: handleProfileSubmit,
register: profileRegister,
formState: { errors: profileErrors },
reset: resetProfile,
} = useForm<ProfileForm>({
resolver: yupResolver(profileSchema),
mode: "onChange",
defaultValues: {
firstName: user?.name?.split(" ")[0] || "",
lastName: user?.name?.split(" ").slice(1).join(" ") || "",
},
});
const onProfileSubmit = async (data: ProfileForm) => {
await updateProfile.mutateAsync(data);
await updateSession();
setIsEditing(false);
};
// Change password state
const [showPasswordForm, setShowPasswordForm] = useState(false);
const changePassword = useChangePassword();
const router = useRouter();
const {
handleSubmit: handlePasswordSubmit,
register: passwordRegister,
formState: { errors: passwordErrors },
reset: resetPassword,
} = useForm<PasswordForm>({
resolver: yupResolver(passwordSchema),
mode: "onChange",
});
const onPasswordSubmit = async (data: PasswordForm) => {
await changePassword.mutateAsync({
currentPassword: data.currentPassword,
newPassword: data.newPassword,
});
resetPassword();
setShowPasswordForm(false);
};
return (
<SlideUp>
<Box maxW="2xl" mx="auto">
<Heading as="h1" size="xl" fontWeight="bold" mb={6}>
{t("title")}
</Heading>
{/* Profile Card */}
<Card.Root
bg={cardBg}
borderColor={borderColor}
borderRadius="xl"
mb={6}
>
<Card.Body>
<Flex
direction={{ base: "column", sm: "row" }}
align="center"
gap={6}
>
<Avatar name={user?.name || "User"} variant="solid" size="2xl" />
<VStack gap={1} align={{ base: "center", sm: "flex-start" }}>
<Heading as="h2" size="lg">
{user?.name || "—"}
</Heading>
<Text color="fg.muted" fontSize="sm">
{user?.email || "—"}
</Text>
</VStack>
</Flex>
</Card.Body>
</Card.Root>
{/* Account Info */}
<Card.Root
bg={cardBg}
borderColor={borderColor}
borderRadius="xl"
mb={6}
>
<Card.Header>
<Flex justify="space-between" align="center">
<Heading as="h3" size="sm">
{t("personal-info")}
</Heading>
{!isEditing ? (
<IconButton
variant="ghost"
size="sm"
aria-label={tCommon("edit")}
onClick={() => setIsEditing(true)}
>
<LuPen />
</IconButton>
) : null}
</Flex>
</Card.Header>
<Card.Body pt={0}>
{isEditing ? (
<Flex
as="form"
direction="column"
gap={4}
onSubmit={handleProfileSubmit(onProfileSubmit)}
>
<Field
label={t("first-name")}
errorText={profileErrors.firstName?.message}
invalid={!!profileErrors.firstName}
>
<Input
{...profileRegister("firstName")}
placeholder={t("first-name")}
/>
</Field>
<Field
label={t("last-name")}
errorText={profileErrors.lastName?.message}
invalid={!!profileErrors.lastName}
>
<Input
{...profileRegister("lastName")}
placeholder={t("last-name")}
/>
</Field>
<HStack gap={2} justify="flex-end">
<Button
variant="outline"
size="sm"
onClick={() => {
setIsEditing(false);
resetProfile();
}}
>
<LuX />
{tCommon("cancel")}
</Button>
<Button
type="submit"
size="sm"
loading={updateProfile.isPending}
>
<LuCheck />
{tCommon("save")}
</Button>
</HStack>
</Flex>
) : (
<>
<InfoRow
icon={<LuUser />}
label={t("full-name")}
value={user?.name || "—"}
/>
<Separator />
<InfoRow
icon={<LuMail />}
label={t("email")}
value={user?.email || "—"}
/>
<Separator />
<InfoRow
icon={<LuShield />}
label={t("role")}
value={
(user as Record<string, unknown>)?.roles
? String((user as Record<string, unknown>).roles)
: "User"
}
/>
<Separator />
<InfoRow
icon={<LuCalendar />}
label={t("member-since")}
value="—"
/>
</>
)}
</Card.Body>
</Card.Root>
{/* Change Password */}
<Card.Root
bg={cardBg}
borderColor={borderColor}
borderRadius="xl"
mb={6}
>
<Card.Header>
<Flex justify="space-between" align="center">
<Heading as="h3" size="sm">
<HStack gap={2}>
<LuLock />
<Text>{t("change-password")}</Text>
</HStack>
</Heading>
{!showPasswordForm ? (
<Button
variant="outline"
size="sm"
onClick={() => setShowPasswordForm(true)}
>
{tCommon("edit")}
</Button>
) : null}
</Flex>
</Card.Header>
<Card.Body pt={0}>
{showPasswordForm ? (
<Flex
as="form"
direction="column"
gap={4}
onSubmit={handlePasswordSubmit(onPasswordSubmit)}
>
<Field
label={t("current-password")}
errorText={passwordErrors.currentPassword?.message}
invalid={!!passwordErrors.currentPassword}
>
<PasswordInput
{...passwordRegister("currentPassword")}
placeholder={t("current-password")}
/>
</Field>
<Field
label={t("new-password")}
errorText={passwordErrors.newPassword?.message}
invalid={!!passwordErrors.newPassword}
>
<PasswordInput
{...passwordRegister("newPassword")}
placeholder={t("new-password")}
/>
</Field>
<Field
label={t("confirm-password")}
errorText={passwordErrors.confirmPassword?.message}
invalid={!!passwordErrors.confirmPassword}
>
<PasswordInput
{...passwordRegister("confirmPassword")}
placeholder={t("confirm-password")}
/>
</Field>
<HStack gap={2} justify="flex-end">
<Button
variant="outline"
size="sm"
onClick={() => {
setShowPasswordForm(false);
resetPassword();
}}
>
<LuX />
{tCommon("cancel")}
</Button>
<Button
type="submit"
size="sm"
loading={changePassword.isPending}
onClick={() => router.refresh()}
>
<LuCheck />
{tCommon("save")}
</Button>
</HStack>
</Flex>
) : (
<Text fontSize="sm" color="fg.muted" py={2}>
{t("change-password-desc")}
</Text>
)}
</Card.Body>
</Card.Root>
{/* Betting Stats */}
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
<Card.Header>
<Heading as="h3" size="sm">
{t("betting-stats")}
</Heading>
</Card.Header>
<Card.Body pt={0}>
{statsLoading ? (
<Flex justify="center" py={6}>
<Spinner size="sm" color="primary.500" />
</Flex>
) : (
<>
<InfoRow
icon={<LuTicket />}
label={t("total-coupons")}
value={String(stats?.totalCoupons ?? "—")}
/>
<Separator />
<InfoRow
icon={<LuTrendingUp />}
label={t("win-rate")}
value={
stats?.winRate != null
? `${Math.round(stats.winRate)}%`
: "—"
}
/>
<Separator />
<InfoRow
icon={<LuTarget />}
label={t("total-profit")}
value={stats?.wonBets != null ? String(stats.wonBets) : "—"}
/>
</>
)}
</Card.Body>
</Card.Root>
</Box>
</SlideUp>
);
}