This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user