mirror of
https://github.com/ershisan99/todolist_next.git
synced 2025-12-16 20:59:24 +00:00
initial commit, login page
This commit is contained in:
11
.eslintrc.json
Normal file
11
.eslintrc.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"project": "./tsconfig.json"
|
||||||
|
},
|
||||||
|
"plugins": ["@typescript-eslint"],
|
||||||
|
"extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/consistent-type-imports": "warn"
|
||||||
|
}
|
||||||
|
}
|
||||||
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# database
|
||||||
|
/prisma/db.sqlite
|
||||||
|
/prisma/db.sqlite-journal
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
58
.idea/codeStyles/Project.xml
generated
Normal file
58
.idea/codeStyles/Project.xml
generated
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<HTMLCodeStyleSettings>
|
||||||
|
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||||
|
<option name="HTML_ENFORCE_QUOTES" value="true" />
|
||||||
|
</HTMLCodeStyleSettings>
|
||||||
|
<JSCodeStyleSettings version="0">
|
||||||
|
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||||
|
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||||
|
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||||
|
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||||
|
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||||
|
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||||
|
</JSCodeStyleSettings>
|
||||||
|
<TypeScriptCodeStyleSettings version="0">
|
||||||
|
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||||
|
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||||
|
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||||
|
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||||
|
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||||
|
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||||
|
</TypeScriptCodeStyleSettings>
|
||||||
|
<VueCodeStyleSettings>
|
||||||
|
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
||||||
|
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
||||||
|
</VueCodeStyleSettings>
|
||||||
|
<codeStyleSettings language="HTML">
|
||||||
|
<option name="SOFT_MARGINS" value="80" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="JavaScript">
|
||||||
|
<option name="SOFT_MARGINS" value="80" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="TypeScript">
|
||||||
|
<option name="SOFT_MARGINS" value="80" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="Vue">
|
||||||
|
<option name="SOFT_MARGINS" value="80" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
||||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
6
.idea/jsLinters/eslint.xml
generated
Normal file
6
.idea/jsLinters/eslint.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="EslintConfiguration">
|
||||||
|
<option name="fix-on-save" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/todolist_next.iml" filepath="$PROJECT_DIR$/.idea/todolist_next.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
7
.idea/prettier.xml
generated
Normal file
7
.idea/prettier.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="PrettierConfiguration">
|
||||||
|
<option name="myRunOnSave" value="true" />
|
||||||
|
<option name="myRunOnReformat" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
0
.idea/sonarlint/issuestore/index.pb
generated
Normal file
0
.idea/sonarlint/issuestore/index.pb
generated
Normal file
12
.idea/todolist_next.iml
generated
Normal file
12
.idea/todolist_next.iml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
27
README.md
Normal file
27
README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Create T3 App
|
||||||
|
|
||||||
|
This is an app bootstrapped according to the [init.tips](https://init.tips) stack, also known as the T3-Stack.
|
||||||
|
|
||||||
|
## What's next? How do I make an app with this?
|
||||||
|
|
||||||
|
We try to keep this project as simple as possible, so you can start with the most basic configuration and then move on to more advanced configuration.
|
||||||
|
|
||||||
|
If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help.
|
||||||
|
|
||||||
|
- [Next-Auth.js](https://next-auth.js.org)
|
||||||
|
- [Prisma](https://prisma.io)
|
||||||
|
- [TailwindCSS](https://tailwindcss.com)
|
||||||
|
- [tRPC](https://trpc.io)
|
||||||
|
|
||||||
|
We also [roll our own docs](https://create.t3.gg) with some summary information and links to the respective documentation.
|
||||||
|
|
||||||
|
Also checkout these awesome tutorials on `create-t3-app`.
|
||||||
|
|
||||||
|
- [Build a Blog With the T3 Stack - tRPC, TypeScript, Next.js, Prisma & Zod](https://www.youtube.com/watch?v=syEWlxVFUrY)
|
||||||
|
- [Build a Live Chat Application with the T3 Stack - TypeScript, Tailwind, tRPC](https://www.youtube.com/watch?v=dXRRY37MPuk)
|
||||||
|
- [Build a full stack app with create-t3-app](https://www.nexxel.dev/blog/ct3a-guestbook)
|
||||||
|
- [A first look at create-t3-app](https://dev.to/ajcwebdev/a-first-look-at-create-t3-app-1i8f)
|
||||||
|
|
||||||
|
## How do I deploy this?
|
||||||
|
|
||||||
|
Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel) and [Docker](https://create.t3.gg/en/deployment/docker) for more information.
|
||||||
17
next.config.mjs
Normal file
17
next.config.mjs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// @ts-check
|
||||||
|
/**
|
||||||
|
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
|
||||||
|
* This is especially useful for Docker builds.
|
||||||
|
*/
|
||||||
|
!process.env.SKIP_ENV_VALIDATION && (await import("./src/env/server.mjs"));
|
||||||
|
|
||||||
|
/** @type {import("next").NextConfig} */
|
||||||
|
const config = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
swcMinify: true,
|
||||||
|
i18n: {
|
||||||
|
locales: ["en"],
|
||||||
|
defaultLocale: "en",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
3989
package-lock.json
generated
Normal file
3989
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
package.json
Normal file
37
package.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "todolist_next",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "next build",
|
||||||
|
"dev": "next dev",
|
||||||
|
"lint": "next lint",
|
||||||
|
"start": "next start"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/react-query": "^4.16.1",
|
||||||
|
"axios": "^1.1.3",
|
||||||
|
"next": "13.0.2",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"zod": "^3.18.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^18.0.0",
|
||||||
|
"@types/react": "^18.0.14",
|
||||||
|
"@types/react-dom": "^18.0.5",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.33.0",
|
||||||
|
"@typescript-eslint/parser": "^5.33.0",
|
||||||
|
"autoprefixer": "^10.4.7",
|
||||||
|
"eslint": "^8.26.0",
|
||||||
|
"eslint-config-next": "13.0.2",
|
||||||
|
"postcss": "^8.4.14",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.1.13",
|
||||||
|
"tailwindcss": "^3.2.0",
|
||||||
|
"typescript": "^4.8.4"
|
||||||
|
},
|
||||||
|
"ct3aMetadata": {
|
||||||
|
"initVersion": "6.10.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.cjs
Normal file
6
postcss.config.cjs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
4
prettier.config.cjs
Normal file
4
prettier.config.cjs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/** @type {import("prettier").Config} */
|
||||||
|
module.exports = {
|
||||||
|
plugins: [require.resolve("prettier-plugin-tailwindcss")],
|
||||||
|
};
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
tailwind.config.cjs
Normal file
8
tailwind.config.cjs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ["./src/**/*.{js,ts,jsx,tsx}"],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"noUncheckedIndexedAccess": true
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.mjs"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user