mirror of
https://github.com/ershisan99/todolist_next.git
synced 2026-01-30 05:12:08 +00:00
initial commit, login page
This commit is contained in:
29
src/components/AuthRedirect.tsx
Normal file
29
src/components/AuthRedirect.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useMeQuery } from "../services/hooks";
|
||||
import { Loader } from "./loader";
|
||||
|
||||
export const AuthRedirect: FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const router = useRouter();
|
||||
const { data: user, isLoading, isError } = useMeQuery();
|
||||
console.log(user);
|
||||
const isAuthPage = router.pathname === "/login";
|
||||
|
||||
useEffect(() => {
|
||||
console.log("here");
|
||||
if (!isLoading && !user && !isAuthPage) {
|
||||
console.log("here");
|
||||
router.push("/login");
|
||||
}
|
||||
}, [user, isError, isLoading, isAuthPage, router]);
|
||||
|
||||
if (isLoading || (!user && !isAuthPage)) {
|
||||
return (
|
||||
<div className={"h-screen"}>
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <>{children}</>;
|
||||
};
|
||||
17
src/components/Button.tsx
Normal file
17
src/components/Button.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { ButtonHTMLAttributes, DetailedHTMLProps, FC } from "react";
|
||||
|
||||
type Props = DetailedHTMLProps<
|
||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
HTMLButtonElement
|
||||
>;
|
||||
|
||||
export const Button: FC<Props> = ({ className, ...rest }) => {
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
className={`rounded-md border border-gray-300 bg-sky-700 px-4 py-2 text-white focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-600 ${className}`}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
17
src/components/Input.tsx
Normal file
17
src/components/Input.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { DetailedHTMLProps, FC, InputHTMLAttributes } from "react";
|
||||
|
||||
type Props = DetailedHTMLProps<
|
||||
InputHTMLAttributes<HTMLInputElement>,
|
||||
HTMLInputElement
|
||||
>;
|
||||
|
||||
export const Input: FC<Props> = ({ className, ...rest }) => {
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
className={` w-full rounded-md border border-gray-300 px-4 py-2 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-600 ${className}`}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
7
src/components/loader.tsx
Normal file
7
src/components/loader.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export const Loader = () => {
|
||||
return (
|
||||
<div className={"flex h-full w-full items-center justify-center"}>
|
||||
<span className="loader"></span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
35
src/env/client.mjs
vendored
Normal file
35
src/env/client.mjs
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
// @ts-check
|
||||
import { clientEnv, clientSchema } from "./schema.mjs";
|
||||
|
||||
const _clientEnv = clientSchema.safeParse(clientEnv);
|
||||
|
||||
export const formatErrors = (
|
||||
/** @type {import('zod').ZodFormattedError<Map<string,string>,string>} */
|
||||
errors,
|
||||
) =>
|
||||
Object.entries(errors)
|
||||
.map(([name, value]) => {
|
||||
if (value && "_errors" in value)
|
||||
return `${name}: ${value._errors.join(", ")}\n`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (!_clientEnv.success) {
|
||||
console.error(
|
||||
"❌ Invalid environment variables:\n",
|
||||
...formatErrors(_clientEnv.error.format()),
|
||||
);
|
||||
throw new Error("Invalid environment variables");
|
||||
}
|
||||
|
||||
for (let key of Object.keys(_clientEnv.data)) {
|
||||
if (!key.startsWith("NEXT_PUBLIC_")) {
|
||||
console.warn(
|
||||
`❌ Invalid public environment variable name: ${key}. It must begin with 'NEXT_PUBLIC_'`,
|
||||
);
|
||||
|
||||
throw new Error("Invalid public environment variable name");
|
||||
}
|
||||
}
|
||||
|
||||
export const env = _clientEnv.data;
|
||||
29
src/env/schema.mjs
vendored
Normal file
29
src/env/schema.mjs
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
// @ts-check
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Specify your server-side environment variables schema here.
|
||||
* This way you can ensure the app isn't built with invalid env vars.
|
||||
*/
|
||||
export const serverSchema = z.object({
|
||||
NODE_ENV: z.enum(["development", "test", "production"]),
|
||||
});
|
||||
|
||||
/**
|
||||
* Specify your client-side environment variables schema here.
|
||||
* This way you can ensure the app isn't built with invalid env vars.
|
||||
* To expose them to the client, prefix them with `NEXT_PUBLIC_`.
|
||||
*/
|
||||
export const clientSchema = z.object({
|
||||
// NEXT_PUBLIC_CLIENTVAR: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* You can't destruct `process.env` as a regular object, so you have to do
|
||||
* it manually here. This is because Next.js evaluates this at build time,
|
||||
* and only used environment variables are included in the build.
|
||||
* @type {{ [k in keyof z.infer<typeof clientSchema>]: z.infer<typeof clientSchema>[k] | undefined }}
|
||||
*/
|
||||
export const clientEnv = {
|
||||
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
|
||||
};
|
||||
27
src/env/server.mjs
vendored
Normal file
27
src/env/server.mjs
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* This file is included in `/next.config.mjs` which ensures the app isn't built with invalid env vars.
|
||||
* It has to be a `.mjs`-file to be imported there.
|
||||
*/
|
||||
import { serverSchema } from "./schema.mjs";
|
||||
import { env as clientEnv, formatErrors } from "./client.mjs";
|
||||
|
||||
const _serverEnv = serverSchema.safeParse(process.env);
|
||||
|
||||
if (!_serverEnv.success) {
|
||||
console.error(
|
||||
"❌ Invalid environment variables:\n",
|
||||
...formatErrors(_serverEnv.error.format()),
|
||||
);
|
||||
throw new Error("Invalid environment variables");
|
||||
}
|
||||
|
||||
for (let key of Object.keys(_serverEnv.data)) {
|
||||
if (key.startsWith("NEXT_PUBLIC_")) {
|
||||
console.warn("❌ You are exposing a server-side env-variable:", key);
|
||||
|
||||
throw new Error("You are exposing a server-side env-variable");
|
||||
}
|
||||
}
|
||||
|
||||
export const env = { ..._serverEnv.data, ...clientEnv };
|
||||
20
src/pages/_app.tsx
Normal file
20
src/pages/_app.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { type AppType } from "next/dist/shared/lib/utils";
|
||||
|
||||
import "../styles/globals.css";
|
||||
import { QueryClient } from "@tanstack/query-core";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { AuthRedirect } from "../components/AuthRedirect";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const MyApp: AppType = ({ Component, pageProps }) => {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthRedirect>
|
||||
<Component {...pageProps} />
|
||||
</AuthRedirect>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyApp;
|
||||
27
src/pages/index.tsx
Normal file
27
src/pages/index.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { type NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import { Button } from "../components/Button";
|
||||
import { useLogoutMutation } from "../services/hooks";
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const { mutate: logout } = useLogoutMutation();
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Todolist</title>
|
||||
<meta name="description" content="Incubator todolist" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<header>
|
||||
<h1>Todolist</h1>
|
||||
<Button onClick={handleLogout}>Logout</Button>
|
||||
</header>
|
||||
<main>Hello</main>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
73
src/pages/login.tsx
Normal file
73
src/pages/login.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from "react";
|
||||
import type { NextPage } from "next";
|
||||
import { Input } from "../components/Input";
|
||||
import { Button } from "../components/Button";
|
||||
import { useLoginMutation } from "../services/hooks";
|
||||
import { useRouter } from "next/router";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
const Login: NextPage = () => {
|
||||
const { mutateAsync: login } = useLoginMutation();
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const [email, setEmail] = React.useState("");
|
||||
const [password, setPassword] = React.useState("");
|
||||
const [remember, setRemember] = React.useState(true);
|
||||
|
||||
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPassword(e.target.value);
|
||||
};
|
||||
|
||||
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setEmail(e.target.value);
|
||||
};
|
||||
|
||||
const handleRememberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRemember(e.target.checked);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
login({
|
||||
email,
|
||||
password,
|
||||
rememberMe: remember,
|
||||
}).then(() => {
|
||||
queryClient.invalidateQueries(["me"]);
|
||||
router.push("/");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={"flex h-screen items-center justify-center"}>
|
||||
<div className={"flex w-52 flex-col gap-3"}>
|
||||
<label className={"flex flex-col gap-1"}>
|
||||
Email
|
||||
<Input value={email} onChange={handleEmailChange} type="email" />
|
||||
</label>
|
||||
|
||||
<label className={"flex flex-col gap-1"}>
|
||||
Password
|
||||
<Input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className={"flex items-center gap-2"}>
|
||||
<input
|
||||
type={"checkbox"}
|
||||
checked={remember}
|
||||
onChange={handleRememberChange}
|
||||
/>
|
||||
Remember me
|
||||
</label>
|
||||
<Button className={"w-full"} onClick={handleSubmit}>
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
35
src/services/hooks.ts
Normal file
35
src/services/hooks.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { PostLoginArgs } from "./index";
|
||||
import { deleteMe, getMe, postLogin } from "./index";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const useLoginMutation = () => {
|
||||
return useMutation({
|
||||
mutationFn: (args: PostLoginArgs) => postLogin(args),
|
||||
});
|
||||
};
|
||||
|
||||
export const useMeQuery = () => {
|
||||
return useQuery({
|
||||
queryFn: () => getMe().then((res) => res.data),
|
||||
queryKey: ["me"],
|
||||
refetchInterval: 1000 * 60 * 60,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
refetchIntervalInBackground: false,
|
||||
retry: false,
|
||||
});
|
||||
};
|
||||
|
||||
export const useLogoutMutation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const router = useRouter();
|
||||
return useMutation({
|
||||
mutationFn: () => deleteMe(),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["me"]);
|
||||
router.push("/login");
|
||||
},
|
||||
});
|
||||
};
|
||||
30
src/services/index.ts
Normal file
30
src/services/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { instance } from "./instance";
|
||||
|
||||
const handleError = (data: any) => {
|
||||
if (data.resultCode === 0) {
|
||||
return data;
|
||||
} else {
|
||||
throw new Error(data.messages[0]);
|
||||
}
|
||||
};
|
||||
|
||||
export type PostLoginArgs = {
|
||||
email: string;
|
||||
password: string;
|
||||
rememberMe: boolean;
|
||||
};
|
||||
|
||||
export const postLogin = async (args: PostLoginArgs) => {
|
||||
const data = await instance.post("auth/login", args);
|
||||
return handleError(data.data);
|
||||
};
|
||||
|
||||
export const getMe = async () => {
|
||||
const data = await instance.get("auth/me");
|
||||
return handleError(data.data);
|
||||
};
|
||||
|
||||
export const deleteMe = async () => {
|
||||
const data = await instance.delete("auth/login");
|
||||
return handleError(data.data);
|
||||
};
|
||||
6
src/services/instance.ts
Normal file
6
src/services/instance.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import axios from "axios";
|
||||
|
||||
export const instance = axios.create({
|
||||
baseURL: "https://social-network.samuraijs.com/api/1.1/",
|
||||
withCredentials: true,
|
||||
});
|
||||
48
src/styles/globals.css
Normal file
48
src/styles/globals.css
Normal file
@@ -0,0 +1,48 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
.loader {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px dotted #000;
|
||||
border-style: solid solid dotted dotted;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
animation: rotation 2s linear infinite;
|
||||
}
|
||||
.loader::after {
|
||||
content: '';
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
border: 3px dotted #FF3D00;
|
||||
border-style: solid solid dotted;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
animation: rotationBack 1s linear infinite;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes rotationBack {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user