Изменил форму авторизации
This commit is contained in:
parent
efd0dab792
commit
950e0f3964
Binary file not shown.
|
Before Width: | Height: | Size: 361 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 261 KiB |
|
|
@ -0,0 +1,98 @@
|
|||
import { useMutation } from "@tanstack/react-query";
|
||||
import useAuthStore from "@/stores/useAuthStore";
|
||||
import useApiClient from "@/hooks/useApiClient";
|
||||
|
||||
interface SendCodeDto {
|
||||
phone: string;
|
||||
}
|
||||
|
||||
interface VerifyCodeDto {
|
||||
phone: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
interface VerifyCodeResponse {
|
||||
access_token: string;
|
||||
}
|
||||
|
||||
interface JWTPayload {
|
||||
user_id?: number;
|
||||
userId?: number;
|
||||
sub?: string;
|
||||
id?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const parseJWT = (token: string): JWTPayload | null => {
|
||||
try {
|
||||
const base64Url = token.split(".")[1];
|
||||
if (!base64Url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split("")
|
||||
.map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
|
||||
.join("")
|
||||
);
|
||||
|
||||
return JSON.parse(jsonPayload);
|
||||
} catch (error) {
|
||||
console.error("Ошибка при парсинге JWT токена:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const useSendCode = () => {
|
||||
const client = useApiClient();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ["auth", "send-code"],
|
||||
mutationFn: async (data: SendCodeDto) => {
|
||||
const response = await client.post("/auth/phone/send-code", data);
|
||||
return response.data;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useVerifyCode = () => {
|
||||
const client = useApiClient();
|
||||
const setToken = useAuthStore((state) => state.setToken);
|
||||
const setUserId = useAuthStore((state) => state.setUserId);
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ["auth", "verify-code"],
|
||||
mutationFn: async (data: VerifyCodeDto) => {
|
||||
const response = await client.post<VerifyCodeResponse>(
|
||||
"/auth/phone/verify-code",
|
||||
data
|
||||
);
|
||||
|
||||
const { access_token } = response.data;
|
||||
|
||||
if (!access_token) {
|
||||
throw new Error("Не удалось получить токен авторизации");
|
||||
}
|
||||
|
||||
setToken(access_token, true);
|
||||
|
||||
const payload = parseJWT(access_token);
|
||||
if (payload) {
|
||||
const userId =
|
||||
payload.user_id ||
|
||||
payload.userId ||
|
||||
payload.sub ||
|
||||
payload.id ||
|
||||
null;
|
||||
|
||||
if (userId) {
|
||||
setUserId(userId, true);
|
||||
}
|
||||
}
|
||||
|
||||
return access_token;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Metadata } from "next";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import Header from "@/components/layout/Header";
|
||||
|
|
@ -12,6 +12,13 @@ export const metadata: Metadata = {
|
|||
description: "Прокат яхт и морские прогулки в Балаклаве",
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
userScalable: false
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -8,141 +8,311 @@ import {
|
|||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Field, FieldContent } from "@/components/ui/field";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import Image from "next/image";
|
||||
import useAuthentificate from "@/api/useAuthentificate";
|
||||
import { useSendCode, useVerifyCode } from "@/api/usePhoneAuth";
|
||||
|
||||
interface AuthDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
type AuthStep = "phone" | "code";
|
||||
|
||||
export default function AuthDialog({ isOpen, onClose }: AuthDialogProps) {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [rememberMe, setRememberMe] = useState(false);
|
||||
const [step, setStep] = useState<AuthStep>("phone");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [code, setCode] = useState(["", "", "", ""]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [resendSeconds, setResendSeconds] = useState(0);
|
||||
|
||||
const mutation = useAuthentificate();
|
||||
const sendCodeMutation = useSendCode();
|
||||
const verifyCodeMutation = useVerifyCode();
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
mutation.mutate({ email, password, rememberMe });
|
||||
};
|
||||
const codeInputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
const prevCodeLengthRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (mutation.isSuccess) {
|
||||
onClose();
|
||||
if (!isOpen) {
|
||||
setStep("phone");
|
||||
setPhone("");
|
||||
setCode(["", "", "", ""]);
|
||||
setError(null);
|
||||
setResendSeconds(0);
|
||||
prevCodeLengthRef.current = 0;
|
||||
}
|
||||
}, [mutation.isSuccess]);
|
||||
}, [isOpen]);
|
||||
|
||||
// Таймер повторной отправки кода
|
||||
useEffect(() => {
|
||||
if (step !== "code" || resendSeconds <= 0) return;
|
||||
const t = setInterval(() => setResendSeconds((s) => (s <= 0 ? 0 : s - 1)), 1000);
|
||||
return () => clearInterval(t);
|
||||
}, [step, resendSeconds]);
|
||||
|
||||
const formatPhoneNumber = (value: string): string => {
|
||||
const digits = value.replace(/\D/g, "");
|
||||
|
||||
if (digits.length === 0) return "";
|
||||
|
||||
let formatted = "";
|
||||
if (digits.length > 0) {
|
||||
formatted = `(${digits.slice(0, 3)}`;
|
||||
}
|
||||
if (digits.length > 3) {
|
||||
formatted += `) ${digits.slice(3, 6)}`;
|
||||
}
|
||||
if (digits.length > 6) {
|
||||
formatted += ` ${digits.slice(6, 10)}`;
|
||||
}
|
||||
|
||||
return formatted;
|
||||
};
|
||||
|
||||
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const input = e.target.value;
|
||||
const digitsOnly = input.replace(/\D/g, "").slice(0, 10);
|
||||
setPhone(digitsOnly);
|
||||
};
|
||||
|
||||
const handlePhoneSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (phone.length < 10) return;
|
||||
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// await sendCodeMutation.mutateAsync({ phone: `7${phone}` });
|
||||
setResendSeconds(60);
|
||||
setStep("code");
|
||||
|
||||
setTimeout(() => {
|
||||
codeInputRefs.current[0]?.focus();
|
||||
}, 100);
|
||||
} catch {
|
||||
setError("Не удалось отправить код. Попробуйте позже.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleCodeChange = (index: number, value: string) => {
|
||||
if (!/^\d*$/.test(value)) return;
|
||||
|
||||
const newCode = [...code];
|
||||
newCode[index] = value.slice(-1);
|
||||
setCode(newCode);
|
||||
|
||||
if (value && index < 3) {
|
||||
codeInputRefs.current[index + 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCodeKeyDown = (
|
||||
index: number,
|
||||
e: React.KeyboardEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (e.key === "Backspace" && !code[index] && index > 0) {
|
||||
codeInputRefs.current[index - 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCodePaste = (e: React.ClipboardEvent) => {
|
||||
e.preventDefault();
|
||||
const pastedData = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, 4);
|
||||
const newCode = [...code];
|
||||
for (let i = 0; i < pastedData.length; i++) {
|
||||
newCode[i] = pastedData[i];
|
||||
}
|
||||
setCode(newCode);
|
||||
const nextEmptyIndex = newCode.findIndex((c) => !c);
|
||||
if (nextEmptyIndex !== -1) {
|
||||
codeInputRefs.current[nextEmptyIndex]?.focus();
|
||||
} else {
|
||||
codeInputRefs.current[3]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const submitCode = async () => {
|
||||
const fullCode = code.join("");
|
||||
if (fullCode.length !== 4) return;
|
||||
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await verifyCodeMutation.mutateAsync({
|
||||
phone: `7${phone}`,
|
||||
code: fullCode,
|
||||
});
|
||||
onClose();
|
||||
} catch {
|
||||
setError("Неверный код. Попробуйте ещё раз.");
|
||||
}
|
||||
};
|
||||
|
||||
// Автоотправка кода, когда все 4 поля заполнены
|
||||
useEffect(() => {
|
||||
if (step !== "code" || verifyCodeMutation.isPending) return;
|
||||
const fullCode = code.join("");
|
||||
if (fullCode.length === 4 && prevCodeLengthRef.current < 4) {
|
||||
prevCodeLengthRef.current = 4;
|
||||
submitCode();
|
||||
} else {
|
||||
prevCodeLengthRef.current = fullCode.length;
|
||||
}
|
||||
}, [step, code, verifyCodeMutation.isPending]);
|
||||
|
||||
const handleCodeSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
submitCode();
|
||||
};
|
||||
|
||||
const getFormattedPhone = () => {
|
||||
return `+7 ${phone.slice(0, 3)} ${phone.slice(3)}`;
|
||||
};
|
||||
|
||||
const handleResendCode = () => {
|
||||
if (resendSeconds > 0) return;
|
||||
setResendSeconds(60);
|
||||
sendCodeMutation.mutate({ phone: `7${phone}` });
|
||||
};
|
||||
|
||||
const LegalText = () => (
|
||||
<p className="text-xs text-gray-500 text-center">
|
||||
Входя в аккаунт или создавая новый, вы соглашаетесь с нашими{" "}
|
||||
<button type="button" className="cursor-pointer text-gray-700 underline hover:text-gray-900">
|
||||
Правилами и условиями
|
||||
</button>
|
||||
{" и "}
|
||||
<button type="button" className="cursor-pointer text-gray-700 underline hover:text-gray-900">
|
||||
Положением о конфиденциальности
|
||||
</button>
|
||||
</p>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="[&>button]:hidden max-w-4xl w-full h-[600px] rounded-[30px] bg-[#F8F8F8]">
|
||||
<div className="flex h-full gap-10">
|
||||
{/* Левая часть - форма */}
|
||||
<div className="flex-1 rounded-l-[10px] py-8 pl-1 flex flex-col justify-center">
|
||||
<DialogHeader className="text-left mb-8">
|
||||
<DialogTitle className="text-2xl font-bold text-gray-800 mb-2">
|
||||
Авторизация
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogContent className="w-full max-w-[100vw] min-w-0 h-full min-h-[100dvh] rounded-none p-0 overflow-hidden bg-white md:min-h-0 md:max-w-4xl md:h-[414px] md:rounded-[30px]">
|
||||
<div className="flex h-full items-start md:items-center min-w-0">
|
||||
<div className="flex-1 flex flex-col bg-white rounded-l-[30px] py-6 px-4 sm:py-[34px] sm:px-[28px] min-w-0 overflow-auto">
|
||||
{step === "phone" ? (
|
||||
<>
|
||||
<DialogHeader className="text-center">
|
||||
<DialogTitle className="text-2xl font-bold text-black mb-8">
|
||||
Добро пожаловать
|
||||
<br />
|
||||
в Travel Marine
|
||||
</DialogTitle>
|
||||
<p className="text-gray-500 text-base mb-4">
|
||||
Бронируйте яхты и катера, управляйте заказами и используйте все возможности платформы
|
||||
</p>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Поле email */}
|
||||
<Field>
|
||||
<FieldContent>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Электронная почта"
|
||||
className="w-full bg-white px-5 py-4 border border-gray-300 rounded-full focus:ring-2 focus:ring-[#008299] focus:border-transparent outline-none transition-colors"
|
||||
required
|
||||
/>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
<form onSubmit={handlePhoneSubmit} className="flex flex-col flex-1">
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-2 px-3 sm:px-4 rounded-full border border-gray-200 overflow-hidden bg-white min-w-0">
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<div className="flex items-center justify-center h-[36px] w-[36px] rounded-[16px] border border-gray-200" aria-hidden><div className="text-xl">🇷🇺</div></div>
|
||||
<span className="text-gray-800 font-medium">+7</span>
|
||||
</div>
|
||||
<input
|
||||
type="tel"
|
||||
value={formatPhoneNumber(phone)}
|
||||
onChange={handlePhoneChange}
|
||||
placeholder="(000) 000 0000"
|
||||
className="flex-1 min-w-0 py-4 outline-none text-gray-800 placeholder:text-gray-400 w-0"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={phone.length < 10 || sendCodeMutation.isPending}
|
||||
className="h-[40px] px-4 sm:px-8 bg-brand hover:bg-brand-hover text-white rounded-full font-semibold transition-colors disabled:opacity-50 shrink-0"
|
||||
>
|
||||
Далее
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Поле пароля */}
|
||||
<Field>
|
||||
<FieldContent>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Пароль"
|
||||
className="w-full bg-white px-5 py-4 border border-gray-300 rounded-full focus:ring-2 focus:ring-[#008299] focus:border-transparent outline-none transition-colors"
|
||||
required
|
||||
/>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
{error && (
|
||||
<p className="absolute left-0 right-0 top-full mt-2 text-red-500 text-xs text-center">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Чекбокс и ссылка */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="remember"
|
||||
checked={rememberMe}
|
||||
onCheckedChange={(checked) =>
|
||||
setRememberMe(checked as boolean)
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="remember" className="text-sm">
|
||||
Запомнить меня
|
||||
</Label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm hover:text-[#008299] transition-colors"
|
||||
>
|
||||
Забыли пароль?
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-11">
|
||||
<LegalText />
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DialogHeader className="text-center">
|
||||
<DialogTitle className="text-2xl font-bold text-black mb-8">
|
||||
Подтверждение номера
|
||||
</DialogTitle>
|
||||
<p className="text-gray-500 text-sm">
|
||||
Введите 4-значный код, отправленный на номер:
|
||||
</p>
|
||||
<p className="text-black text-sm font-bold mb-4">{getFormattedPhone()}</p>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Кнопка входа */}
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full h-[56px] bg-brand px-5 py-4 hover:bg-brand-hover text-white rounded-full font-semibold transition-colors"
|
||||
>
|
||||
Войти
|
||||
</Button>
|
||||
<form onSubmit={handleCodeSubmit} className="flex flex-col flex-1">
|
||||
<div className="flex justify-center gap-2 relative mb-6">
|
||||
{code.map((digit, index) => (
|
||||
<input
|
||||
key={index}
|
||||
ref={(el) => {
|
||||
codeInputRefs.current[index] = el;
|
||||
}}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
maxLength={1}
|
||||
value={digit}
|
||||
onChange={(e) => handleCodeChange(index, e.target.value)}
|
||||
onKeyDown={(e) => handleCodeKeyDown(index, e)}
|
||||
onPaste={handleCodePaste}
|
||||
className="w-14 h-14 text-center text-2xl font-semibold bg-white rounded-[4px] border border-gray-200 focus:border-brand focus:ring-2 focus:ring-brand/20 outline-none transition-all"
|
||||
/>
|
||||
))}
|
||||
{error && (
|
||||
<p className="absolute left-0 right-0 top-full mt-2 text-red-500 text-xs text-center">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Ссылка на регистрацию */}
|
||||
<div className="text-center">
|
||||
<button
|
||||
type="button"
|
||||
className="hover:text-brand font-bold transition-colors"
|
||||
>
|
||||
Создать аккаунт
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Договор */}
|
||||
<div className="mt-8 text-xs text-gray-400 text-center">
|
||||
Входя в аккаунт или создавая новый, вы соглашаетесь с нашими{" "}
|
||||
<button className="text-primary hover:underline">
|
||||
Правилами и условиями
|
||||
</button>{" "}
|
||||
и{" "}
|
||||
<button className="text-primary hover:underline">
|
||||
Положением о конфиденциальности
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-black text-center mb-1">
|
||||
{resendSeconds > 0 ? (
|
||||
<>Отправить код повторно через {String(Math.floor(resendSeconds / 60)).padStart(2, "0")}:{String(resendSeconds % 60).padStart(2, "0")}</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleResendCode}
|
||||
className="text-brand hover:underline"
|
||||
>
|
||||
Отправить код повторно
|
||||
</button>
|
||||
)}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setStep("phone"); setError(null); setCode(["", "", "", ""]) }}
|
||||
className="text-brand text-sm hover:underline text-center"
|
||||
>
|
||||
Изменить номер
|
||||
</button>
|
||||
|
||||
<div className="mt-5">
|
||||
<LegalText />
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Правая часть - изображение */}
|
||||
<div className="hidden md:block flex-1 relative overflow-hidden rounded-[20px]">
|
||||
{/* Изображение яхт */}
|
||||
<Image
|
||||
src="/images/auth.jpg"
|
||||
alt="Яхты"
|
||||
fill
|
||||
className="object-cover rounded-lg"
|
||||
/>
|
||||
<div className="hidden md:block w-[45%] h-full flex-shrink-0 py-[34px] pr-[28px] pl-0">
|
||||
<div className="relative w-full h-full overflow-hidden rounded-[10px]">
|
||||
<Image
|
||||
src="/images/auth.png"
|
||||
alt="Яхты"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
|
|||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"fixed inset-0 z-50 min-h-[100dvh] bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -38,15 +38,18 @@ const DialogContent = React.forwardRef<
|
|||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-[10px] overflow-hidden",
|
||||
"fixed inset-0 z-50 grid w-full max-w-[100vw] min-w-0 h-full min-h-[100dvh] rounded-none gap-4 border-0 bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom overflow-hidden",
|
||||
"md:inset-auto md:left-[50%] md:top-[50%] md:translate-x-[-50%] md:translate-y-[-50%] md:max-w-lg md:min-w-0 md:h-auto md:rounded-[10px] md:border md:data-[state=closed]:slide-out-to-left-1/2 md:data-[state=closed]:slide-out-to-top-[48%] md:data-[state=open]:slide-in-from-left-1/2 md:data-[state=open]:slide-in-from-top-[48%]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-3 top-3 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
<DialogPrimitive.Close
|
||||
className="absolute right-4 top-4 z-10 rounded-sm text-brand opacity-90 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground md:hidden"
|
||||
aria-label="Закрыть"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
|
|
@ -59,7 +62,7 @@ const DialogHeader = ({
|
|||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
"flex flex-col text-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
Loading…
Reference in New Issue