initial commit, login page

This commit is contained in:
2022-11-19 17:30:58 +01:00
commit 41b49d6306
35 changed files with 4674 additions and 0 deletions

11
.eslintrc.json Normal file
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View 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
View 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
View 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
View 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
View File

12
.idea/todolist_next.iml generated Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

37
package.json Normal file
View 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
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

4
prettier.config.cjs Normal file
View File

@@ -0,0 +1,4 @@
/** @type {import("prettier").Config} */
module.exports = {
plugins: [require.resolve("prettier-plugin-tailwindcss")],
};

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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
View 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
View 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>
);
};

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"]
}