Изменил форму авторизации
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 { Inter } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import Header from "@/components/layout/Header";
|
import Header from "@/components/layout/Header";
|
||||||
|
|
@ -12,6 +12,13 @@ export const metadata: Metadata = {
|
||||||
description: "Прокат яхт и морские прогулки в Балаклаве",
|
description: "Прокат яхт и морские прогулки в Балаклаве",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const viewport: Viewport = {
|
||||||
|
width: 'device-width',
|
||||||
|
initialScale: 1,
|
||||||
|
maximumScale: 1,
|
||||||
|
userScalable: false
|
||||||
|
}
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|
@ -8,143 +8,313 @@ import {
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
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 Image from "next/image";
|
||||||
import useAuthentificate from "@/api/useAuthentificate";
|
import { useSendCode, useVerifyCode } from "@/api/usePhoneAuth";
|
||||||
|
|
||||||
interface AuthDialogProps {
|
interface AuthDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthStep = "phone" | "code";
|
||||||
|
|
||||||
export default function AuthDialog({ isOpen, onClose }: AuthDialogProps) {
|
export default function AuthDialog({ isOpen, onClose }: AuthDialogProps) {
|
||||||
const [email, setEmail] = useState("");
|
const [step, setStep] = useState<AuthStep>("phone");
|
||||||
const [password, setPassword] = useState("");
|
const [phone, setPhone] = useState("");
|
||||||
const [rememberMe, setRememberMe] = useState(false);
|
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) => {
|
const codeInputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||||
e.preventDefault();
|
const prevCodeLengthRef = useRef(0);
|
||||||
mutation.mutate({ email, password, rememberMe });
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mutation.isSuccess) {
|
if (!isOpen) {
|
||||||
onClose();
|
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 (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
<DialogContent className="[&>button]:hidden max-w-4xl w-full h-[600px] rounded-[30px] bg-[#F8F8F8]">
|
<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 gap-10">
|
<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">
|
||||||
<div className="flex-1 rounded-l-[10px] py-8 pl-1 flex flex-col justify-center">
|
{step === "phone" ? (
|
||||||
<DialogHeader className="text-left mb-8">
|
<>
|
||||||
<DialogTitle className="text-2xl font-bold text-gray-800 mb-2">
|
<DialogHeader className="text-center">
|
||||||
Авторизация
|
<DialogTitle className="text-2xl font-bold text-black mb-8">
|
||||||
|
Добро пожаловать
|
||||||
|
<br />
|
||||||
|
в Travel Marine
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
<p className="text-gray-500 text-base mb-4">
|
||||||
|
Бронируйте яхты и катера, управляйте заказами и используйте все возможности платформы
|
||||||
|
</p>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handlePhoneSubmit} className="flex flex-col flex-1">
|
||||||
{/* Поле email */}
|
<div className="relative">
|
||||||
<Field>
|
<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">
|
||||||
<FieldContent>
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
<input
|
<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>
|
||||||
id="email"
|
<span className="text-gray-800 font-medium">+7</span>
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Поле пароля */}
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Чекбокс и ссылка */}
|
|
||||||
<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>
|
</div>
|
||||||
<button
|
<input
|
||||||
type="button"
|
type="tel"
|
||||||
className="text-sm hover:text-[#008299] transition-colors"
|
value={formatPhoneNumber(phone)}
|
||||||
>
|
onChange={handlePhoneChange}
|
||||||
Забыли пароль?
|
placeholder="(000) 000 0000"
|
||||||
</button>
|
className="flex-1 min-w-0 py-4 outline-none text-gray-800 placeholder:text-gray-400 w-0"
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
{/* Кнопка входа */}
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
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"
|
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>
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Ссылка на регистрацию */}
|
{error && (
|
||||||
<div className="text-center">
|
<p className="absolute left-0 right-0 top-full mt-2 text-red-500 text-xs text-center">{error}</p>
|
||||||
<button
|
)}
|
||||||
type="button"
|
</div>
|
||||||
className="hover:text-brand font-bold transition-colors"
|
|
||||||
>
|
<div className="mt-11">
|
||||||
Создать аккаунт
|
<LegalText />
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
||||||
|
|
||||||
{/* Договор */}
|
<form onSubmit={handleCodeSubmit} className="flex flex-col flex-1">
|
||||||
<div className="mt-8 text-xs text-gray-400 text-center">
|
<div className="flex justify-center gap-2 relative mb-6">
|
||||||
Входя в аккаунт или создавая новый, вы соглашаетесь с нашими{" "}
|
{code.map((digit, index) => (
|
||||||
<button className="text-primary hover:underline">
|
<input
|
||||||
Правилами и условиями
|
key={index}
|
||||||
</button>{" "}
|
ref={(el) => {
|
||||||
и{" "}
|
codeInputRefs.current[index] = el;
|
||||||
<button className="text-primary hover:underline">
|
}}
|
||||||
Положением о конфиденциальности
|
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>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<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>
|
</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>
|
</div>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Правая часть - изображение */}
|
<div className="hidden md:block w-[45%] h-full flex-shrink-0 py-[34px] pr-[28px] pl-0">
|
||||||
<div className="hidden md:block flex-1 relative overflow-hidden rounded-[20px]">
|
<div className="relative w-full h-full overflow-hidden rounded-[10px]">
|
||||||
{/* Изображение яхт */}
|
|
||||||
<Image
|
<Image
|
||||||
src="/images/auth.jpg"
|
src="/images/auth.png"
|
||||||
alt="Яхты"
|
alt="Яхты"
|
||||||
fill
|
fill
|
||||||
className="object-cover rounded-lg"
|
className="object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -38,15 +38,18 @@ const DialogContent = React.forwardRef<
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{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">
|
<DialogPrimitive.Close
|
||||||
<X className="h-4 w-4" />
|
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"
|
||||||
<span className="sr-only">Close</span>
|
aria-label="Закрыть"
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
|
|
@ -59,7 +62,7 @@ const DialogHeader = ({
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
"flex flex-col text-center",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue