mirror of
https://github.com/QuivrHQ/quivr.git
synced 2024-12-15 01:21:48 +03:00
feat: add new signin/login page (#1492)
Issue https://github.com/StanGirard/quivr/issues/1404 https://github.com/StanGirard/quivr/assets/63923024/703fedf3-2f39-407b-afc2-78318829ca0f
This commit is contained in:
parent
c3acb2901c
commit
7e70e4fc84
@ -19,13 +19,17 @@ vi.mock("@/lib/context/SupabaseProvider", () => ({
|
||||
useSupabase: () => mockUseSupabase(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/assets/QuivrLogo", () => ({
|
||||
QuivrLogo: () => <div>LOGO</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/services/analytics/june/useEventTracking", () => ({
|
||||
useEventTracking: () => ({ track: vi.fn() }),
|
||||
}));
|
||||
|
||||
describe("Login component", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("redirects to /upload if user is already signed in and is not coming from another page", () => {
|
||||
@ -68,7 +72,7 @@ describe("Login component", () => {
|
||||
session: { user: undefined },
|
||||
});
|
||||
const { getByTestId } = render(<Login />);
|
||||
const signInForm = getByTestId("sign-in-form");
|
||||
const signInForm = getByTestId("sign-in-card");
|
||||
expect(signInForm).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FcGoogle } from "react-icons/fc";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
|
||||
@ -12,10 +13,11 @@ export const GoogleLoginButton = (): JSX.Element => {
|
||||
<Button
|
||||
onClick={() => void signInWithGoogle()}
|
||||
isLoading={isPending}
|
||||
variant={"danger"}
|
||||
type="button"
|
||||
data-testid="google-login-button"
|
||||
className="font-normal bg-white text-black py-2 hover:text-white"
|
||||
>
|
||||
<FcGoogle />
|
||||
{t("googleLogin", { ns: "login" })}
|
||||
</Button>
|
||||
);
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||
import { useToast } from "@/lib/hooks";
|
||||
|
||||
type UseMagicLinkLoginProps = {
|
||||
email: string;
|
||||
setEmail: (email: string) => void;
|
||||
};
|
||||
|
||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||
import { useToast } from "@/lib/hooks";
|
||||
|
||||
export const useMagicLinkLogin = ({
|
||||
email,
|
||||
setEmail,
|
||||
@ -17,14 +18,14 @@ export const useMagicLinkLogin = ({
|
||||
} => {
|
||||
const { supabase } = useSupabase();
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
|
||||
const { t } = useTranslation("login");
|
||||
const { publish } = useToast();
|
||||
|
||||
const handleMagicLinkLogin = async () => {
|
||||
if (email === "") {
|
||||
publish({
|
||||
variant: "danger",
|
||||
text: "Please enter your email address",
|
||||
text: t("errorMailMissed"),
|
||||
});
|
||||
|
||||
return;
|
||||
|
@ -24,11 +24,11 @@ export const MagicLinkLogin = ({
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant={"tertiary"}
|
||||
onClick={() => void handleMagicLinkLogin()}
|
||||
isLoading={isPending}
|
||||
className="bg-black text-white py-2 font-normal"
|
||||
>
|
||||
{t("magicLink", { ns: "login" })}
|
||||
{t("magicLink")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
@ -1,50 +0,0 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { usePasswordForgotten } from "../usePasswordForgotten";
|
||||
|
||||
const mockResetPasswordForEmail = vi.fn(() => ({ error: null }));
|
||||
|
||||
const mockUseSupabase = () => ({
|
||||
supabase: {
|
||||
auth: {
|
||||
resetPasswordForEmail: mockResetPasswordForEmail,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
vi.mock("@/lib/context/SupabaseProvider", () => ({
|
||||
useSupabase: () => mockUseSupabase(),
|
||||
}));
|
||||
|
||||
const setEmail = vi.fn();
|
||||
|
||||
describe("usePassword", () => {
|
||||
it("should not call resetPasswordForEmail if email is empty", async () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePasswordForgotten({
|
||||
email: "",
|
||||
setEmail,
|
||||
})
|
||||
);
|
||||
|
||||
await act(() => result.current.handleRecoverPassword());
|
||||
expect(mockResetPasswordForEmail).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("should call resetPasswordForEmail with proper arguments", async () => {
|
||||
const email = "user@quivr.app";
|
||||
const { result } = renderHook(() =>
|
||||
usePasswordForgotten({
|
||||
email,
|
||||
setEmail,
|
||||
})
|
||||
);
|
||||
|
||||
await act(() => result.current.handleRecoverPassword());
|
||||
expect(mockResetPasswordForEmail).toHaveBeenCalledTimes(1);
|
||||
expect(mockResetPasswordForEmail).toHaveBeenCalledWith(email, {
|
||||
redirectTo: `${window.location.origin}/recover-password`,
|
||||
});
|
||||
});
|
||||
});
|
@ -1,59 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||
import { useToast } from "@/lib/hooks";
|
||||
|
||||
type UsePasswordForgottenProps = {
|
||||
email: string;
|
||||
setEmail: (email: string) => void;
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const usePasswordForgotten = ({
|
||||
email,
|
||||
setEmail,
|
||||
}: UsePasswordForgottenProps) => {
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const { supabase } = useSupabase();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const {t, i18n} = useTranslation(["login"]);
|
||||
|
||||
const { publish } = useToast();
|
||||
|
||||
const handleRecoverPassword = async () => {
|
||||
if (email === "") {
|
||||
publish({
|
||||
variant: "danger",
|
||||
text: t("errorMailMissed",{ ns: 'login' })
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setIsPending(true);
|
||||
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: `${window.location.origin}/recover-password`,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
publish({
|
||||
variant: "danger",
|
||||
text: error.message,
|
||||
});
|
||||
} else {
|
||||
publish({
|
||||
variant: "success",
|
||||
text: t("recoveryMailSended",{ ns: 'login' })
|
||||
});
|
||||
|
||||
setEmail("");
|
||||
}
|
||||
setIsPending(false);
|
||||
};
|
||||
|
||||
return {
|
||||
isPending,
|
||||
handleRecoverPassword,
|
||||
};
|
||||
};
|
@ -1,33 +0,0 @@
|
||||
/* eslint-disable */
|
||||
"use client";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import { usePasswordForgotten } from "./hooks/usePasswordForgotten";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type PasswordForgottenProps = {
|
||||
email: string;
|
||||
setEmail: (email: string) => void;
|
||||
};
|
||||
|
||||
export const PasswordForgotten = ({
|
||||
email,
|
||||
setEmail,
|
||||
}: PasswordForgottenProps) => {
|
||||
const { isPending, handleRecoverPassword } = usePasswordForgotten({
|
||||
email,
|
||||
setEmail,
|
||||
});
|
||||
const {t, i18n} = useTranslation(["login"]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant={"tertiary"}
|
||||
onClick={handleRecoverPassword}
|
||||
isLoading={isPending}
|
||||
>
|
||||
{t("forgottenPassword",{ ns: 'login' })}
|
||||
</Button>
|
||||
);
|
||||
};
|
@ -1,38 +0,0 @@
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { useLogin } from "../useLogin";
|
||||
|
||||
const mockSignInWithPassword = vi.fn(() => ({ error: null }));
|
||||
|
||||
const mockUseSupabase = () => ({
|
||||
supabase: {
|
||||
auth: {
|
||||
signInWithPassword: mockSignInWithPassword,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
vi.mock("@/lib/context/SupabaseProvider", () => ({
|
||||
useSupabase: () => mockUseSupabase(),
|
||||
}));
|
||||
|
||||
describe("useLogin", () => {
|
||||
it("should call signInWithPassword with user email and password", async () => {
|
||||
const { result } = renderHook(() => useLogin());
|
||||
|
||||
const email = "user@quivr.com";
|
||||
const password = "password";
|
||||
|
||||
act(() => result.current.setEmail(email));
|
||||
act(() => result.current.setPassword(password));
|
||||
|
||||
await act(() => result.current.handleLogin());
|
||||
|
||||
expect(mockSignInWithPassword).toHaveBeenCalledTimes(1);
|
||||
expect(mockSignInWithPassword).toHaveBeenCalledWith({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
});
|
||||
});
|
@ -1,56 +1,16 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||
import { redirectToPreviousPageOrChatPage } from "@/lib/helpers/redirectToPreviousPageOrChatPage";
|
||||
import { useToast } from "@/lib/hooks";
|
||||
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useLogin = () => {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const { publish } = useToast();
|
||||
const { supabase, session } = useSupabase();
|
||||
|
||||
const { session } = useSupabase();
|
||||
|
||||
const { track } = useEventTracking();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { t } = useTranslation(["login"]);
|
||||
|
||||
const handleLogin = async () => {
|
||||
setIsPending(true);
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email: email,
|
||||
password: password,
|
||||
});
|
||||
if (error) {
|
||||
console.log(error.message);
|
||||
if (error.message.includes("Failed")) {
|
||||
publish({
|
||||
variant: "danger",
|
||||
text: t("Failedtofetch", { ns: "login" }),
|
||||
});
|
||||
} else if (error.message.includes("Invalid")) {
|
||||
publish({
|
||||
variant: "danger",
|
||||
text: t("Invalidlogincredentials", { ns: "login" }),
|
||||
});
|
||||
} else {
|
||||
publish({
|
||||
variant: "danger",
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
|
||||
text: error.message,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
publish({
|
||||
variant: "success",
|
||||
text: t("loginSuccess", { ns: "login" }),
|
||||
});
|
||||
}
|
||||
setIsPending(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.user !== undefined) {
|
||||
@ -60,11 +20,7 @@ export const useLogin = () => {
|
||||
}, [session?.user]);
|
||||
|
||||
return {
|
||||
handleLogin,
|
||||
setEmail,
|
||||
setPassword,
|
||||
email,
|
||||
isPending,
|
||||
password,
|
||||
};
|
||||
};
|
||||
|
@ -3,78 +3,49 @@ import Link from "next/link";
|
||||
import { Suspense } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import Card from "@/lib/components/ui/Card";
|
||||
import { QuivrLogo } from "@/lib/assets/QuivrLogo";
|
||||
import { Divider } from "@/lib/components/ui/Divider";
|
||||
import Field from "@/lib/components/ui/Field";
|
||||
import PageHeading from "@/lib/components/ui/PageHeading";
|
||||
|
||||
import { GoogleLoginButton } from "./components/GoogleLogin";
|
||||
import { MagicLinkLogin } from "./components/MagicLinkLogin";
|
||||
import { PasswordForgotten } from "./components/PasswordForgotten";
|
||||
import { useLogin } from "./hooks/useLogin";
|
||||
|
||||
const Main = (): JSX.Element => {
|
||||
const { handleLogin, setEmail, setPassword, email, isPending, password } =
|
||||
useLogin();
|
||||
const { setEmail, email } = useLogin();
|
||||
const { t } = useTranslation(["translation", "login"]);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<section className="w-full min-h-[80vh] h-full outline-none flex flex-col gap-5 items-center justify-center p-6">
|
||||
<PageHeading
|
||||
title={t("title", { ns: "login" })}
|
||||
subtitle={t("subtitle", { ns: "login" })}
|
||||
/>
|
||||
<Card className="max-w-md w-full p-5 sm:p-10 text-left">
|
||||
<form
|
||||
data-testid="sign-in-form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
void handleLogin();
|
||||
}}
|
||||
className="flex flex-col gap-2"
|
||||
>
|
||||
<div className="w-screen h-screen bg-ivory" data-testid="sign-in-card">
|
||||
<main className="h-full flex flex-col items-center justify-center">
|
||||
<section className="w-full md:w-1/2 lg:w-1/3 flex flex-col gap-2">
|
||||
<Link href="/" className="flex justify-center">
|
||||
<QuivrLogo size={80} color="black" />
|
||||
</Link>
|
||||
<p className="text-center text-4xl font-medium">
|
||||
{t("talk_to", { ns: "login" })}{" "}
|
||||
<span className="text-primary">Quivr</span>
|
||||
</p>
|
||||
<div className="mt-5 flex flex-col">
|
||||
<Field
|
||||
name="email"
|
||||
required
|
||||
type="email"
|
||||
placeholder={t("email")}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
value={email}
|
||||
label="Email"
|
||||
inputClassName="py-1 mt-1 mb-3"
|
||||
/>
|
||||
<Field
|
||||
name="password"
|
||||
required
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t("password")}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col items-center justify-center mt-2 gap-2">
|
||||
<Button
|
||||
data-testid="submit-login"
|
||||
type="submit"
|
||||
isLoading={isPending}
|
||||
>
|
||||
{t("loginButton")}
|
||||
</Button>
|
||||
<PasswordForgotten setEmail={setEmail} email={email} />
|
||||
|
||||
<Link href="/signup">{t("signup", { ns: "login" })}</Link>
|
||||
</div>
|
||||
|
||||
<Divider text={t("or")} />
|
||||
<div className="flex flex-col items-center justify-center mt-2 gap-2">
|
||||
<GoogleLoginButton />
|
||||
</div>
|
||||
<Divider text={t("or")} />
|
||||
<MagicLinkLogin email={email} setEmail={setEmail} />
|
||||
</form>
|
||||
</Card>
|
||||
</section>
|
||||
</main>
|
||||
<Divider text={t("or")} className="my-3 uppercase" />
|
||||
<GoogleLoginButton />
|
||||
</div>
|
||||
<p className="text-[10px] text-center">
|
||||
{t("restriction_message", { ns: "login" })}
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,131 +0,0 @@
|
||||
/* eslint-disable max-lines */
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import RecoverPassword from "../page";
|
||||
|
||||
const mockUsePathname = vi.fn(() => "/previous-page");
|
||||
const mockRedirect = vi.fn((url: string) => ({ url }));
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: (url: string) => mockRedirect(url),
|
||||
usePathname: () => mockUsePathname(),
|
||||
}));
|
||||
|
||||
const mockUseSupabase = vi.fn(() => ({
|
||||
supabase: {
|
||||
auth: {
|
||||
updateUser: vi.fn(),
|
||||
},
|
||||
},
|
||||
session: {
|
||||
user: {},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/context/SupabaseProvider", () => ({
|
||||
useSupabase: () => mockUseSupabase(),
|
||||
}));
|
||||
|
||||
const mockPublish = vi.fn();
|
||||
|
||||
vi.mock("@/lib/hooks/useToast", () => ({
|
||||
useToast: () => ({
|
||||
publish: mockPublish,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockTrack = vi.fn();
|
||||
|
||||
vi.mock("@/services/analytics/useEventTracking", () => ({
|
||||
useEventTracking: () => ({
|
||||
track: mockTrack,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("RecoverPassword component", () => {
|
||||
it("should render the password update form", () => {
|
||||
mockUseSupabase.mockReturnValue({
|
||||
//@ts-expect-error doing this for testing purposes
|
||||
session: { user: undefined },
|
||||
});
|
||||
const { getByTestId } = render(<RecoverPassword />);
|
||||
const passwordField = getByTestId("password-field");
|
||||
const updateButton = getByTestId("update-button");
|
||||
|
||||
expect(passwordField).toBeDefined();
|
||||
expect(updateButton).toBeDefined();
|
||||
});
|
||||
|
||||
it.skip("should update the password and shows success toast", async () => {
|
||||
const updateUserMock = vi.fn(() => ({
|
||||
data: {},
|
||||
}));
|
||||
mockUseSupabase.mockReturnValue({
|
||||
supabase: {
|
||||
auth: {
|
||||
updateUser: updateUserMock,
|
||||
},
|
||||
},
|
||||
session: { user: {} },
|
||||
});
|
||||
const { getByTestId } = render(<RecoverPassword />);
|
||||
const passwordField = getByTestId("password-field");
|
||||
const updateButton = getByTestId("update-button");
|
||||
|
||||
const newPassword = "new-password";
|
||||
fireEvent.change(passwordField, { target: { value: newPassword } });
|
||||
fireEvent.click(updateButton);
|
||||
|
||||
expect(mockTrack).toHaveBeenCalledTimes(1);
|
||||
expect(mockTrack).toHaveBeenCalledWith("UPDATE_PASSWORD");
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
expect(mockPublish).toHaveBeenCalledTimes(1);
|
||||
expect(mockPublish).toHaveBeenCalledWith({
|
||||
variant: "success",
|
||||
text: "Password updated successfully!",
|
||||
});
|
||||
expect(updateUserMock).toHaveBeenCalledTimes(1);
|
||||
expect(updateUserMock).toHaveBeenCalledWith({
|
||||
password: newPassword,
|
||||
});
|
||||
resolve();
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
it.skip("should show error toast when password update fails", async () => {
|
||||
const errorMessage = "Password update failed";
|
||||
const updateUserMock = vi.fn(() => ({
|
||||
error: { message: errorMessage },
|
||||
}));
|
||||
mockUseSupabase.mockReturnValue({
|
||||
supabase: {
|
||||
auth: {
|
||||
updateUser: updateUserMock,
|
||||
},
|
||||
},
|
||||
session: { user: {} },
|
||||
});
|
||||
const { getByTestId } = render(<RecoverPassword />);
|
||||
const passwordField = getByTestId("password-field");
|
||||
const updateButton = getByTestId("update-button");
|
||||
|
||||
fireEvent.change(passwordField, { target: { value: "new-password" } });
|
||||
fireEvent.click(updateButton);
|
||||
|
||||
expect(mockPublish).toHaveBeenCalledTimes(1);
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
expect(mockPublish).toHaveBeenCalledWith({
|
||||
variant: "danger",
|
||||
text: `Error: ${errorMessage}`,
|
||||
});
|
||||
resolve();
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,91 +0,0 @@
|
||||
/* eslint-disable */
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import Card from "@/lib/components/ui/Card";
|
||||
import Field from "@/lib/components/ui/Field";
|
||||
import PageHeading from "@/lib/components/ui/PageHeading";
|
||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||
import { useToast } from "@/lib/hooks/useToast";
|
||||
import { redirectToLogin } from "@/lib/router/redirectToLogin";
|
||||
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
|
||||
import { Suspense } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function RecoverPassword() {
|
||||
const { supabase, session } = useSupabase();
|
||||
const [password, setPassword] = useState("");
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const { track } = useEventTracking();
|
||||
|
||||
const { t } = useTranslation(["translation", "updatePassword"]);
|
||||
|
||||
const { publish } = useToast();
|
||||
|
||||
function ChangePassword() {
|
||||
const handleChangePassword = async () => {
|
||||
void track("UPDATE_PASSWORD");
|
||||
setIsPending(true);
|
||||
const { error } = await supabase.auth.updateUser({
|
||||
password: password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Error while resetting password:", error.message);
|
||||
publish({
|
||||
variant: "danger",
|
||||
text: `Error: ${error.message}`,
|
||||
});
|
||||
} else {
|
||||
publish({
|
||||
variant: "success",
|
||||
text: t("passwordUpdated", { ns: "updatePassword" }),
|
||||
});
|
||||
}
|
||||
setIsPending(false);
|
||||
};
|
||||
|
||||
if (session?.user === undefined) {
|
||||
redirectToLogin();
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<section className="min-h-[80vh] w-full h-full outline-none flex flex-col gap-5 items-center justify-center p-6">
|
||||
<PageHeading title={t("title", { ns: "updatePassword" })} />
|
||||
<Card className="max-w-md w-full p-5 sm:p-10 text-left">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleChangePassword();
|
||||
}}
|
||||
className="flex flex-col gap-2"
|
||||
>
|
||||
<Field
|
||||
name="New password"
|
||||
required
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t("newPassword", { ns: "updatePassword" })}
|
||||
data-testid="password-field"
|
||||
/>
|
||||
<div className="flex flex-col items-center justify-center mt-2 gap-2">
|
||||
<Button isLoading={isPending} data-testid="update-button">
|
||||
{t("updateButton")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={"Loading..."}>
|
||||
<ChangePassword />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import SignUp from "../page";
|
||||
|
||||
const mockHandleSignUp = vi.fn(() => ({}));
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ replace: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/context/SupabaseProvider", () => ({
|
||||
useSupabase: () => ({}),
|
||||
}));
|
||||
|
||||
describe("SignUp component", () => {
|
||||
it("should render correctly", () => {
|
||||
const { getByTestId } = render(<SignUp />);
|
||||
const signUpPage = getByTestId("sign-up-page");
|
||||
expect(signUpPage).toBeDefined();
|
||||
|
||||
const signUpForm = getByTestId("sign-up-form");
|
||||
expect(signUpForm).toBeDefined();
|
||||
|
||||
const emailInput = getByTestId("email-field");
|
||||
expect(emailInput).toBeDefined();
|
||||
|
||||
const passwordInput = getByTestId("password-field");
|
||||
expect(passwordInput).toBeDefined();
|
||||
|
||||
const signUpButton = getByTestId("sign-up-button");
|
||||
expect(signUpButton).toBeDefined();
|
||||
});
|
||||
|
||||
it("should correctly fill the email and password fields", () => {
|
||||
const { getByTestId } = render(<SignUp />);
|
||||
const emailInput = getByTestId("email-field") as HTMLInputElement;
|
||||
const passwordInput = getByTestId("password-field") as HTMLInputElement;
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: "user@quivr.app" } });
|
||||
fireEvent.change(passwordInput, { target: { value: "password123" } });
|
||||
expect(emailInput.value).toBe("user@quivr.app");
|
||||
expect(passwordInput.value).toBe("password123");
|
||||
});
|
||||
|
||||
it("should call handleSignUp on submit", () => {
|
||||
vi.mock("../hooks/useSignUp", async () => {
|
||||
const functions = await vi.importActual<
|
||||
typeof import("../hooks/useSignUp")
|
||||
>("../hooks/useSignUp");
|
||||
|
||||
return {
|
||||
useSignUp: () => ({
|
||||
...functions.useSignUp(),
|
||||
handleSignUp: () => mockHandleSignUp(),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const { getByTestId } = render(<SignUp />);
|
||||
const submitForm = getByTestId("sign-up-form") as HTMLFormElement;
|
||||
|
||||
fireEvent.submit(submitForm);
|
||||
expect(mockHandleSignUp).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@ -1,50 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||
import { useToast } from "@/lib/hooks/useToast";
|
||||
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useSignUp = () => {
|
||||
const { supabase } = useSupabase();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const { track } = useEventTracking();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { t } = useTranslation(["signUp"]);
|
||||
|
||||
const { publish } = useToast();
|
||||
const handleSignUp = async () => {
|
||||
void track("SIGNUP");
|
||||
setIsPending(true);
|
||||
const { error } = await supabase.auth.signUp({
|
||||
email: email,
|
||||
password: password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Error signing up:", error.message);
|
||||
publish({
|
||||
variant: "danger",
|
||||
text: t("errorSignUp", { errorMessage: error.message }),
|
||||
});
|
||||
} else {
|
||||
publish({
|
||||
variant: "success",
|
||||
text: t("mailSended"),
|
||||
});
|
||||
}
|
||||
setIsPending(false);
|
||||
};
|
||||
|
||||
return {
|
||||
handleSignUp,
|
||||
setEmail,
|
||||
password,
|
||||
setPassword,
|
||||
isPending,
|
||||
email,
|
||||
};
|
||||
};
|
@ -1,64 +0,0 @@
|
||||
"use client";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import Card from "@/lib/components/ui/Card";
|
||||
import Field from "@/lib/components/ui/Field";
|
||||
import PageHeading from "@/lib/components/ui/PageHeading";
|
||||
|
||||
import { useSignUp } from "./hooks/useSignUp";
|
||||
|
||||
const SignUp = (): JSX.Element => {
|
||||
const { handleSignUp, isPending, email, password, setEmail, setPassword } =
|
||||
useSignUp();
|
||||
const { t } = useTranslation(["translation", "signUp"]);
|
||||
|
||||
return (
|
||||
<main data-testid="sign-up-page">
|
||||
<section className="min-h-[80vh] w-full h-full outline-none flex flex-col gap-5 items-center justify-center p-6">
|
||||
<PageHeading
|
||||
title={t("title", { ns: "signUp" })}
|
||||
subtitle={t("subtitle", { ns: "signUp" })}
|
||||
/>
|
||||
<Card className="max-w-md w-full p-5 sm:p-10 text-left">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
void handleSignUp();
|
||||
}}
|
||||
className="flex flex-col gap-2"
|
||||
data-testid="sign-up-form"
|
||||
>
|
||||
<Field
|
||||
name="email"
|
||||
required
|
||||
type="email"
|
||||
placeholder={t("email")}
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
data-testid="email-field"
|
||||
/>
|
||||
<Field
|
||||
name="password"
|
||||
required
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t("password")}
|
||||
data-testid="password-field"
|
||||
/>
|
||||
<div className="flex flex-col items-center justify-center mt-2 gap-2">
|
||||
<Button data-testid="sign-up-button" isLoading={isPending}>
|
||||
{t("signUpButton", { ns: "signUp" })}
|
||||
</Button>
|
||||
<Link href="/login">{t("login", { ns: "signUp" })}</Link>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignUp;
|
@ -1,9 +1,9 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { QuivrLogo } from "@/lib/assets/QuivrLogo";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { PopoverMenuMobile } from "./components/PopoverMenuMobile";
|
||||
import { QuivrLogo } from "./components/QuivrLogo";
|
||||
import { useHomeHeader } from "./hooks/useHomeHeader";
|
||||
import { linkStyle } from "./styles";
|
||||
|
||||
|
@ -1,10 +1,9 @@
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import { LuMenu, LuX } from "react-icons/lu";
|
||||
|
||||
import { QuivrLogo } from "@/lib/assets/QuivrLogo";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { QuivrLogo } from "./QuivrLogo";
|
||||
|
||||
type PopoverMenuMobileProps = {
|
||||
navLinks: JSX.Element[];
|
||||
color?: "white" | "black";
|
||||
|
@ -33,7 +33,7 @@ export const useHomeHeader = ({ color }: UseHomeHeaderProps) => {
|
||||
rightIcon: null,
|
||||
},
|
||||
{ href: "/blog", label: t("blog"), rightIcon: null, newTab: true },
|
||||
{ href: "/signup", label: t("sign_up") },
|
||||
{ href: "/login", label: t("sign_up") },
|
||||
{ href: "/login", label: t("sign_in") },
|
||||
];
|
||||
|
||||
|
@ -31,11 +31,11 @@ export const DemoSection = (): JSX.Element => {
|
||||
)}
|
||||
</div>
|
||||
<Link
|
||||
href="/signup"
|
||||
href="/login"
|
||||
onClick={(event) => {
|
||||
onLinkClick({
|
||||
href: "/signup",
|
||||
label: "SIGN_UP",
|
||||
href: "/login",
|
||||
label: "SIGN_IN",
|
||||
event,
|
||||
});
|
||||
}}
|
||||
|
@ -21,11 +21,11 @@ export const FooterSection = (): JSX.Element => {
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-5 flex-wrap">
|
||||
<Link
|
||||
href="/signup"
|
||||
href="/login"
|
||||
onClick={(event) => {
|
||||
onLinkClick({
|
||||
href: "/signup",
|
||||
label: "SIGN_UP",
|
||||
href: "/login",
|
||||
label: "SIGN_IN",
|
||||
event,
|
||||
});
|
||||
}}
|
||||
|
@ -26,11 +26,11 @@ export const IntroSection = (): JSX.Element => {
|
||||
</div>
|
||||
<div className="flex flex-col items-start sm:flex-row sm:items-center gap-5">
|
||||
<Link
|
||||
href="/signup"
|
||||
href="/login"
|
||||
onClick={(event) =>
|
||||
onLinkClick({
|
||||
href: "/signup",
|
||||
label: "SIGN_UP",
|
||||
href: "/login",
|
||||
label: "SIGN_IN",
|
||||
event,
|
||||
})
|
||||
}
|
||||
|
@ -57,11 +57,11 @@ export const SecuritySection = (): JSX.Element => {
|
||||
</div>
|
||||
<div className="flex md:justify-end w-full">
|
||||
<Link
|
||||
href="/signup"
|
||||
href="/login"
|
||||
onClick={(event) => {
|
||||
onLinkClick({
|
||||
href: "/signup",
|
||||
label: "SIGN_UP",
|
||||
href: "/login",
|
||||
label: "SIGN_IN",
|
||||
event,
|
||||
});
|
||||
}}
|
||||
|
@ -22,11 +22,11 @@ export const UseCases = (): JSX.Element => {
|
||||
<UseCasesListing />
|
||||
<div className="mt-10 flex md:justify-center">
|
||||
<Link
|
||||
href="/signup"
|
||||
href="/login"
|
||||
onClick={(event) => {
|
||||
onLinkClick({
|
||||
href: "/signup",
|
||||
label: "SIGN_UP",
|
||||
href: "/login",
|
||||
label: "SIGN_IN",
|
||||
event,
|
||||
});
|
||||
}}
|
||||
|
@ -10,8 +10,14 @@ const Footer = (): JSX.Element => {
|
||||
const path = usePathname();
|
||||
const isHomePage = path === "/";
|
||||
const isContactPage = path === "/contact";
|
||||
const isLoginPage = path === "/login";
|
||||
|
||||
if (session?.user !== undefined || isHomePage || isContactPage) {
|
||||
if (
|
||||
session?.user !== undefined ||
|
||||
isHomePage ||
|
||||
isContactPage ||
|
||||
isLoginPage
|
||||
) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ export const AuthButtons = (): JSX.Element => {
|
||||
const pathname = usePathname();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (pathname === "/signup") {
|
||||
if (pathname === "/login") {
|
||||
return (
|
||||
<Link href={"/login"}>
|
||||
<Button variant={"secondary"}>{t("loginButton")}</Button>
|
||||
@ -17,7 +17,7 @@ export const AuthButtons = (): JSX.Element => {
|
||||
}
|
||||
if (pathname === "/login") {
|
||||
return (
|
||||
<Link href={"/signup"}>
|
||||
<Link href={"/login"}>
|
||||
<Button variant={"secondary"}>{t("signUpButton")}</Button>
|
||||
</Link>
|
||||
);
|
||||
|
@ -16,8 +16,9 @@ export const NavBar = (): JSX.Element => {
|
||||
|
||||
const isHomePage = path === "/";
|
||||
const isContactPage = path === "/contact";
|
||||
const isLoginPage = path === "/login";
|
||||
|
||||
if (pageHasSidebar || isHomePage || isContactPage) {
|
||||
if (pageHasSidebar || isHomePage || isContactPage || isLoginPage) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
@ -12,9 +12,7 @@ import knowlegde_en from "../../../public/locales/en/knowledge.json";
|
||||
import login_en from "../../../public/locales/en/login.json";
|
||||
import logout_en from "../../../public/locales/en/logout.json";
|
||||
import monetization_en from "../../../public/locales/en/monetization.json";
|
||||
import signUp_en from "../../../public/locales/en/signUp.json";
|
||||
import translation_en from "../../../public/locales/en/translation.json";
|
||||
import updatePassword_en from "../../../public/locales/en/updatePassword.json";
|
||||
import upload_en from "../../../public/locales/en/upload.json";
|
||||
import user_en from "../../../public/locales/en/user.json";
|
||||
// import all namespaces Spanish
|
||||
@ -30,9 +28,7 @@ import knowlegde_es from "../../../public/locales/es/knowledge.json";
|
||||
import login_es from "../../../public/locales/es/login.json";
|
||||
import logout_es from "../../../public/locales/es/logout.json";
|
||||
import monetization_es from "../../../public/locales/es/monetization.json";
|
||||
import signUp_es from "../../../public/locales/es/signUp.json";
|
||||
import translation_es from "../../../public/locales/es/translation.json";
|
||||
import updatePassword_es from "../../../public/locales/es/updatePassword.json";
|
||||
import upload_es from "../../../public/locales/es/upload.json";
|
||||
import user_es from "../../../public/locales/es/user.json";
|
||||
// import all namespaces French
|
||||
@ -48,9 +44,7 @@ import knowlegde_fr from "../../../public/locales/fr/knowledge.json";
|
||||
import login_fr from "../../../public/locales/fr/login.json";
|
||||
import logout_fr from "../../../public/locales/fr/logout.json";
|
||||
import monetization_fr from "../../../public/locales/fr/monetization.json";
|
||||
import signUp_fr from "../../../public/locales/fr/signUp.json";
|
||||
import translation_fr from "../../../public/locales/fr/translation.json";
|
||||
import updatePassword_fr from "../../../public/locales/fr/updatePassword.json";
|
||||
import upload_fr from "../../../public/locales/fr/upload.json";
|
||||
import user_fr from "../../../public/locales/fr/user.json";
|
||||
// import all namespaces Portuguese
|
||||
@ -66,9 +60,7 @@ import knowlegde_ptbr from "../../../public/locales/pt-br/knowledge.json";
|
||||
import login_ptbr from "../../../public/locales/pt-br/login.json";
|
||||
import logout_ptbr from "../../../public/locales/pt-br/logout.json";
|
||||
import monetization_ptbr from "../../../public/locales/pt-br/monetization.json";
|
||||
import signUp_ptbr from "../../../public/locales/pt-br/signUp.json";
|
||||
import translation_ptbr from "../../../public/locales/pt-br/translation.json";
|
||||
import updatePassword_ptbr from "../../../public/locales/pt-br/updatePassword.json";
|
||||
import upload_ptbr from "../../../public/locales/pt-br/upload.json";
|
||||
import user_ptbr from "../../../public/locales/pt-br/user.json";
|
||||
// import all namespaces Russian
|
||||
@ -84,9 +76,7 @@ import knowlegde_ru from "../../../public/locales/ru/knowledge.json";
|
||||
import login_ru from "../../../public/locales/ru/login.json";
|
||||
import logout_ru from "../../../public/locales/ru/logout.json";
|
||||
import monetization_ru from "../../../public/locales/ru/monetization.json";
|
||||
import signUp_ru from "../../../public/locales/ru/signUp.json";
|
||||
import translation_ru from "../../../public/locales/ru/translation.json";
|
||||
import updatePassword_ru from "../../../public/locales/ru/updatePassword.json";
|
||||
import upload_ru from "../../../public/locales/ru/upload.json";
|
||||
import user_ru from "../../../public/locales/ru/user.json";
|
||||
// import all namespaces Simplified Chinese
|
||||
@ -102,9 +92,7 @@ import knowlegde_zh_cn from "../../../public/locales/zh-cn/knowledge.json";
|
||||
import login_zh_cn from "../../../public/locales/zh-cn/login.json";
|
||||
import logout_zh_cn from "../../../public/locales/zh-cn/logout.json";
|
||||
import monetization_zh_cn from "../../../public/locales/zh-cn/monetization.json";
|
||||
import signUp_zh_cn from "../../../public/locales/zh-cn/signUp.json";
|
||||
import translation_zh_cn from "../../../public/locales/zh-cn/translation.json";
|
||||
import updatePassword_zh_cn from "../../../public/locales/zh-cn/updatePassword.json";
|
||||
import upload_zh_cn from "../../../public/locales/zh-cn/upload.json";
|
||||
import user_zh_cn from "../../../public/locales/zh-cn/user.json";
|
||||
|
||||
@ -121,9 +109,7 @@ export type Translations = {
|
||||
login: typeof import("../../../public/locales/en/login.json");
|
||||
logout: typeof import("../../../public/locales/en/logout.json");
|
||||
monetization: typeof import("../../../public/locales/en/monetization.json");
|
||||
signUp: typeof import("../../../public/locales/en/signUp.json");
|
||||
translation: typeof import("../../../public/locales/en/translation.json");
|
||||
updatePassword: typeof import("../../../public/locales/en/updatePassword.json");
|
||||
upload: typeof import("../../../public/locales/en/upload.json");
|
||||
user: typeof import("../../../public/locales/en/user.json");
|
||||
knowledge: typeof import("../../../public/locales/en/knowledge.json");
|
||||
@ -151,9 +137,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
|
||||
login: login_en,
|
||||
logout: logout_en,
|
||||
monetization: monetization_en,
|
||||
signUp: signUp_en,
|
||||
translation: translation_en,
|
||||
updatePassword: updatePassword_en,
|
||||
upload: upload_en,
|
||||
user: user_en,
|
||||
delete_or_unsubscribe_from_brain: delete_brain_en,
|
||||
@ -170,9 +154,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
|
||||
login: login_es,
|
||||
logout: logout_es,
|
||||
monetization: monetization_es,
|
||||
signUp: signUp_es,
|
||||
translation: translation_es,
|
||||
updatePassword: updatePassword_es,
|
||||
upload: upload_es,
|
||||
user: user_es,
|
||||
delete_or_unsubscribe_from_brain: delete_brain_es,
|
||||
@ -189,9 +171,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
|
||||
login: login_fr,
|
||||
logout: logout_fr,
|
||||
monetization: monetization_fr,
|
||||
signUp: signUp_fr,
|
||||
translation: translation_fr,
|
||||
updatePassword: updatePassword_fr,
|
||||
upload: upload_fr,
|
||||
user: user_fr,
|
||||
delete_or_unsubscribe_from_brain: delete_brain_fr,
|
||||
@ -208,9 +188,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
|
||||
login: login_ptbr,
|
||||
logout: logout_ptbr,
|
||||
monetization: monetization_ptbr,
|
||||
signUp: signUp_ptbr,
|
||||
translation: translation_ptbr,
|
||||
updatePassword: updatePassword_ptbr,
|
||||
upload: upload_ptbr,
|
||||
user: user_ptbr,
|
||||
delete_or_unsubscribe_from_brain: delete_brain_ptbr,
|
||||
@ -227,9 +205,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
|
||||
login: login_ru,
|
||||
logout: logout_ru,
|
||||
monetization: monetization_ru,
|
||||
signUp: signUp_ru,
|
||||
translation: translation_ru,
|
||||
updatePassword: updatePassword_ru,
|
||||
upload: upload_ru,
|
||||
user: user_ru,
|
||||
delete_or_unsubscribe_from_brain: delete_brain_ru,
|
||||
@ -246,9 +222,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
|
||||
login: login_zh_cn,
|
||||
logout: logout_zh_cn,
|
||||
monetization: monetization_zh_cn,
|
||||
signUp: signUp_zh_cn,
|
||||
translation: translation_zh_cn,
|
||||
updatePassword: updatePassword_zh_cn,
|
||||
upload: upload_zh_cn,
|
||||
user: user_zh_cn,
|
||||
delete_or_unsubscribe_from_brain: delete_brain_zh_cn,
|
||||
|
@ -1,15 +1,8 @@
|
||||
{
|
||||
"title": "Login",
|
||||
"subtitle": "Welcome back",
|
||||
"signup": "Don't have an account? Sign up",
|
||||
"forgottenPassword": "Password forgotten",
|
||||
"googleLogin": "Login with Google",
|
||||
"magicLink": "Send Magic Link",
|
||||
"loginSuccess": "Successfully logged in",
|
||||
"googleLogin": "Continue with Google",
|
||||
"magicLink": "Continue with email",
|
||||
"errorMailMissed": "Please enter your email address",
|
||||
"recoveryMailSended": "Recovery mail will be sent if email recognized",
|
||||
"Invalidlogincredentials" : "Invalid login credentials",
|
||||
"password.updated": "Password updated successfully!",
|
||||
"new_password": "New Password",
|
||||
"Failedtofetch": "Failed to fetch data"
|
||||
"talk_to": "Talk to",
|
||||
"restriction_message": "Unpaid users have access to a free and limited demo of Quivr",
|
||||
"email":"Email address"
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
{
|
||||
"title": "Sign Up",
|
||||
"subtitle": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"loginButton": "Login",
|
||||
"signUpButton": "Sign up",
|
||||
"or": "or",
|
||||
"login": "Already registered? Sign in",
|
||||
"googleLogin": "Login with Google",
|
||||
"errorSignUp": "Error signing up: {{errorMessage}}",
|
||||
"mailSended": "Confirmation Email sent, please check your email"
|
||||
}
|
@ -4,7 +4,6 @@
|
||||
"description": "Quivr is your second brain in the cloud, designed to easily store and retrieve unstructured information.",
|
||||
"toastDismiss": "dismiss",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"or": "or",
|
||||
"and": "and",
|
||||
"loginButton": "Login",
|
||||
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"title": "Update Password",
|
||||
"passwordUpdated": "Password updated successfully!",
|
||||
"newPassword": "New Password"
|
||||
}
|
@ -1,15 +1,8 @@
|
||||
{
|
||||
"errorMailMissed": "Por favor ingresa tu dirección de email",
|
||||
"Failedtofetch": "Error al obtener datos",
|
||||
"forgottenPassword": "Olvidé mi contraseña",
|
||||
"googleLogin": "Iniciar con Google",
|
||||
"Invalidlogincredentials": "Credenciales inválidas",
|
||||
"loginSuccess": "Sesión iniciada correctamente",
|
||||
"magicLink": "Enviar Enlace Mágico",
|
||||
"new_password": "Nueva contraseña",
|
||||
"password.updated": "¡Contraseña actualizada!",
|
||||
"recoveryMailSended": "El correo de recuperación será enviado si el email es reconocido",
|
||||
"signup": "¿No tienes una cuenta? Registrate aquí",
|
||||
"subtitle": "Un gusto tenerte de vuelta",
|
||||
"title": "Iniciar sesión"
|
||||
"googleLogin": "Continuar con Google",
|
||||
"magicLink": "Continuar con correo electrónico",
|
||||
"errorMailMissed": "Por favor, ingrese su dirección de correo electrónico",
|
||||
"talk_to": "Hablar con",
|
||||
"restriction_message": "Los usuarios no pagos tienen acceso a una demostración gratuita y limitada de Quivr",
|
||||
"email":"Dirección de correo electrónico"
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
{
|
||||
"email": "Correo electrónico",
|
||||
"errorSignUp": "Error al registrarse: {{errorMessage}}",
|
||||
"googleLogin": "Iniciar con Google",
|
||||
"login": "¿Ya tienes una cuenta? Inicia sesión",
|
||||
"loginButton": "Iniciar sesión",
|
||||
"mailSended": "Correo de confirmación enviado, por favor verifica tu buzón",
|
||||
"or": "o",
|
||||
"password": "Contraseña",
|
||||
"signUpButton": "Registrarse",
|
||||
"subtitle": "Crea tu cuenta",
|
||||
"title": "Registrarse"
|
||||
}
|
@ -18,7 +18,6 @@
|
||||
"or": "o",
|
||||
"and": "y",
|
||||
"Owner": "Propietario",
|
||||
"password": "Contraseña",
|
||||
"resetButton": "Restaurar",
|
||||
"shareButton": "Compartir",
|
||||
"signUpButton": "Registrarse",
|
||||
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"newPassword": "Nueva contraseña",
|
||||
"passwordUpdated": "¡Contraseña actualizada!",
|
||||
"title": "Actualizar Contraseña"
|
||||
}
|
@ -1,15 +1,8 @@
|
||||
{
|
||||
"title": "Connexion",
|
||||
"subtitle": "Bienvenue",
|
||||
"signup": "Vous n'avez pas de compte ? Inscrivez-vous",
|
||||
"forgottenPassword": "Mot de passe oublié",
|
||||
"googleLogin": "Connexion avec Google",
|
||||
"magicLink": "Envoyer le lien magique",
|
||||
"loginSuccess": "Connexion réussie",
|
||||
"errorMailMissed": "Veuillez entrer votre adresse e-mail",
|
||||
"recoveryMailSended": "Un mail de récupération sera envoyé si l'email est reconnu",
|
||||
"Invalidlogincredentials": "Identifiants de connexion invalides",
|
||||
"password.updated": "Mot de passe mis à jour avec succès !",
|
||||
"new_password": "Nouveau mot de passe",
|
||||
"Failedtofetch": "Échec de la récupération"
|
||||
"googleLogin": "Continuer avec Google",
|
||||
"magicLink": "Continuer avec e-mail",
|
||||
"errorMailMissed": "Veuillez saisir votre adresse e-mail",
|
||||
"talk_to": "Parler à",
|
||||
"restriction_message": "Les utilisateurs non payants ont accès à une démonstration gratuite et limitée de Quivr",
|
||||
"email":"Adresse e-mail"
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
{
|
||||
"title": "Inscription",
|
||||
"subtitle": "Créez votre compte",
|
||||
"email": "Email",
|
||||
"password": "Mot de passe",
|
||||
"loginButton": "Connexion",
|
||||
"signUpButton": "S'inscrire",
|
||||
"or": "ou",
|
||||
"login": "Déjà inscrit ? Connectez-vous",
|
||||
"googleLogin": "Connexion avec Google",
|
||||
"errorSignUp": "Erreur lors de l'inscription : {{errorMessage}}",
|
||||
"mailSended": "Email de confirmation envoyé, veuillez vérifier votre email"
|
||||
}
|
@ -4,7 +4,6 @@
|
||||
"description": "Quivr est votre deuxième cerveau dans le nuage, conçu pour stocker et récupérer facilement des informations non structurées.",
|
||||
"toastDismiss": "ignorer",
|
||||
"email": "Email",
|
||||
"password": "Mot de passe",
|
||||
"or": "ou",
|
||||
"and": "et",
|
||||
"loginButton": "Connexion",
|
||||
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"title": "Mettre à jour le mot de passe",
|
||||
"passwordUpdated": "Mot de passe mis à jour avec succès !",
|
||||
"newPassword": "Nouveau mot de passe"
|
||||
}
|
@ -1,15 +1,8 @@
|
||||
{
|
||||
"title": "Login",
|
||||
"subtitle": "Bem-vindo de volta",
|
||||
"signup": "Não tem uma conta? Cadastre-se",
|
||||
"forgottenPassword": "Esqueci a senha",
|
||||
"googleLogin": "Entrar com o Google",
|
||||
"magicLink": "Enviar Link Mágico",
|
||||
"loginSuccess": "Login realizado com sucesso",
|
||||
"errorMailMissed": "Por favor, insira seu endereço de email",
|
||||
"recoveryMailSended": "Um email de recuperação será enviado se o email for reconhecido",
|
||||
"Invalidlogincredentials": "Credenciais de login inválidas",
|
||||
"password.updated": "Senha atualizada com sucesso!",
|
||||
"new_password": "Nova Senha",
|
||||
"Failedtofetch": "Falha ao buscar os dados"
|
||||
"googleLogin": "Continuar com o Google",
|
||||
"magicLink": "Continuar com o e-mail",
|
||||
"errorMailMissed": "Por favor, insira seu endereço de e-mail",
|
||||
"talk_to": "Converse com",
|
||||
"restriction_message": "Usuários não pagos têm acesso a uma demonstração gratuita e limitada do Quivr",
|
||||
"email":"Email address"
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
{
|
||||
"title": "Cadastro",
|
||||
"subtitle": "Crie sua conta",
|
||||
"email": "Email",
|
||||
"password": "Senha",
|
||||
"loginButton": "Login",
|
||||
"signUpButton": "Cadastre-se",
|
||||
"or": "ou",
|
||||
"login": "Já possui cadastro? Faça login",
|
||||
"googleLogin": "Entrar com o Google",
|
||||
"errorSignUp": "Erro ao se cadastrar: {{errorMessage}}",
|
||||
"mailSended": "Email de confirmação enviado, por favor verifique seu email"
|
||||
}
|
@ -4,7 +4,6 @@
|
||||
"description": "Quivr é o seu segundo cérebro na nuvem, projetado para armazenar e recuperar facilmente informações não estruturadas.",
|
||||
"toastDismiss": "fechar",
|
||||
"email": "Email",
|
||||
"password": "Senha",
|
||||
"or": "ou",
|
||||
"and": "e",
|
||||
"loginButton": "Entrar",
|
||||
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"title": "Atualizar Senha",
|
||||
"passwordUpdated": "Senha atualizada com sucesso!",
|
||||
"newPassword": "Nova Senha"
|
||||
}
|
@ -1,15 +1,8 @@
|
||||
{
|
||||
"title": "Вход",
|
||||
"subtitle": "С возвращением",
|
||||
"signup": "Нет аккаунта? Зарегистрируйтесь",
|
||||
"forgottenPassword": "Забыли пароль",
|
||||
"googleLogin": "Войти с Google",
|
||||
"magicLink": "Отправить магическую ссылку",
|
||||
"loginSuccess": "Успешный вход",
|
||||
"errorMailMissed": "Пожалуйста, введите ваш адрес электронной почты",
|
||||
"recoveryMailSended": "Восстановительное письмо будет отправлено, если адрес электронной почты распознан",
|
||||
"Invalidlogincredentials": "Недопустимые учетные данные для входа",
|
||||
"password.updated": "Пароль успешно обновлен!",
|
||||
"new_password": "Новый пароль",
|
||||
"Failedtofetch": "Не удалось получить данные"
|
||||
}
|
||||
"googleLogin": "Продолжить с Google",
|
||||
"magicLink": "Продолжить с электронной почтой",
|
||||
"errorMailMissed": "Пожалуйста, введите ваш адрес электронной почты",
|
||||
"talk_to": "Общение с",
|
||||
"restriction_message": "Неоплаченным пользователям доступен бесплатный и ограниченный демонстрационный доступ к Quivr",
|
||||
"email":"Адрес электронной почты"
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
{
|
||||
"title": "Регистрация",
|
||||
"subtitle": "Создайте свой аккаунт",
|
||||
"email": "Email",
|
||||
"password": "Пароль",
|
||||
"loginButton": "Вход",
|
||||
"signUpButton": "Зарегистрироваться",
|
||||
"or": "или",
|
||||
"login": "Уже зарегистрированы? Войти",
|
||||
"googleLogin": "Войти с Google",
|
||||
"errorSignUp": "Ошибка при регистрации: {{errorMessage}}",
|
||||
"mailSended": "Письмо с подтверждением отправлено, пожалуйста, проверьте вашу почту"
|
||||
}
|
@ -4,7 +4,6 @@
|
||||
"description": "Quivr - это ваш второй мозг в облаке, предназначенный для легкого хранения и извлечения неструктурированной информации.",
|
||||
"toastDismiss": "закрыть",
|
||||
"email": "Email",
|
||||
"password": "Пароль",
|
||||
"or": "или",
|
||||
"and": "и",
|
||||
"loginButton": "Войти",
|
||||
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"title": "Обновление пароля",
|
||||
"passwordUpdated": "Пароль успешно обновлен!",
|
||||
"newPassword": "Новый пароль"
|
||||
}
|
@ -1,15 +1,8 @@
|
||||
{
|
||||
"title": "登录",
|
||||
"subtitle": "欢迎回来",
|
||||
"signup": "没有账户吗?注册",
|
||||
"forgottenPassword": "忘记密码",
|
||||
"googleLogin": "使用Google登录",
|
||||
"magicLink": "Send Magic Link",
|
||||
"loginSuccess": "登录成功",
|
||||
"errorMailMissed": "请输入您的电子邮件地址",
|
||||
"recoveryMailSended": "如果电子邮件被识别,将发送恢复邮件",
|
||||
"Invalidlogincredentials": "无效的登录凭据",
|
||||
"password.updated": "密码更新成功!",
|
||||
"new_password": "新密码",
|
||||
"Failedtofetch": "获取数据失败"
|
||||
"googleLogin": "使用Google继续",
|
||||
"magicLink": "使用电子邮件继续",
|
||||
"errorMailMissed": "请输入您的电子邮件地址",
|
||||
"talk_to": "与之交谈",
|
||||
"restriction_message": "未付费用户可以访问 Quivr 的免费和有限演示",
|
||||
"email":"电子邮件地址"
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
{
|
||||
"title": "注册",
|
||||
"subtitle": "创建您的帐户",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"loginButton": "登录",
|
||||
"signUpButton": "注册",
|
||||
"or": "或",
|
||||
"login": "已经注册?登录",
|
||||
"googleLogin": "使用Google登录",
|
||||
"errorSignUp": "注册时出错: {{errorMessage}}",
|
||||
"mailSended": "确认邮件已发送,请检查您的邮箱"
|
||||
}
|
@ -4,7 +4,6 @@
|
||||
"description": "Quivr 是您在云中的第二个大脑,让您轻松存储和检索非结构化信息。",
|
||||
"toastDismiss": "dismiss",
|
||||
"email": "Email",
|
||||
"password": "密码",
|
||||
"or": "或",
|
||||
"and": "和",
|
||||
"loginButton": "登录",
|
||||
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"title": "更新密码",
|
||||
"passwordUpdated": "密码更新成功!",
|
||||
"newPassword": "新密码"
|
||||
}
|
@ -22,6 +22,7 @@ module.exports = {
|
||||
"msg-header-gray": "#8F8F8F",
|
||||
"msg-purple": "#E0DDFC",
|
||||
"onboarding-yellow-bg": "#F6EFDE",
|
||||
ivory: "#FCFAF6",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user