@leo-h/create-nodejs-app 1.0.21 → 1.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/dist/package.json.js +1 -1
- package/dist/src/compose-app/replace-content-in-file.compose.js +1 -1
- package/dist/src/core/validation.js +1 -1
- package/dist/src/validations/back-end-framework.validation.js +1 -0
- package/dist/src/validations/front-end-framework.validation.js +1 -0
- package/dist/src/validations/project-name-validation.js +1 -1
- package/dist/src/validations/template.validation.js +1 -1
- package/package.json +1 -1
- package/templates/react-vite/.env.example +8 -0
- package/templates/react-vite/.husky/pre-commit +1 -0
- package/templates/react-vite/.lintstagedrc.json +4 -0
- package/templates/react-vite/.prettierignore +7 -0
- package/templates/react-vite/.prettierrc.json +6 -0
- package/templates/react-vite/eslint.config.js +36 -0
- package/templates/react-vite/gitignore +24 -0
- package/templates/react-vite/index.html +12 -0
- package/templates/react-vite/npmrc +1 -0
- package/templates/react-vite/orval.config.ts +51 -0
- package/templates/react-vite/package.json +50 -0
- package/templates/react-vite/pnpm-lock.yaml +5412 -0
- package/templates/react-vite/public/vite.svg +1 -0
- package/templates/react-vite/scripts/orval-generate-api-definition.ts +90 -0
- package/templates/react-vite/src/@types/routes.ts +24 -0
- package/templates/react-vite/src/api/errors/api-error.ts +7 -0
- package/templates/react-vite/src/api/errors/api-unexpected-response-error.ts +8 -0
- package/templates/react-vite/src/api/swr-fetcher.ts +41 -0
- package/templates/react-vite/src/app.tsx +14 -0
- package/templates/react-vite/src/env.ts +19 -0
- package/templates/react-vite/src/hooks/use-current-route-handle-params.ts +16 -0
- package/templates/react-vite/src/index.css +29 -0
- package/templates/react-vite/src/lib/zod-i18n-translation-for-end-users.json +103 -0
- package/templates/react-vite/src/lib/zod-i18n.ts +20 -0
- package/templates/react-vite/src/main.tsx +13 -0
- package/templates/react-vite/src/pages/_layouts/app.tsx +17 -0
- package/templates/react-vite/src/pages/_layouts/auth.tsx +17 -0
- package/templates/react-vite/src/pages/app/dashboard/dashboard.tsx +12 -0
- package/templates/react-vite/src/pages/app/dashboard/styles.css +40 -0
- package/templates/react-vite/src/pages/auth/sign-in/index.tsx +83 -0
- package/templates/react-vite/src/pages/auth/sign-in/styles.css +92 -0
- package/templates/react-vite/src/routes.tsx +65 -0
- package/templates/react-vite/src/vite-env.d.ts +1 -0
- package/templates/react-vite/tsconfig.app.json +32 -0
- package/templates/react-vite/tsconfig.json +13 -0
- package/templates/react-vite/tsconfig.node.json +24 -0
- package/templates/react-vite/vite.config.ts +9 -0
- package/dist/src/validations/framework.validation.js +0 -1
@@ -0,0 +1 @@
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
@@ -0,0 +1,90 @@
|
|
1
|
+
import { execSync } from "node:child_process";
|
2
|
+
import { existsSync } from "node:fs";
|
3
|
+
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
4
|
+
import { dirname, resolve } from "node:path";
|
5
|
+
import { generate } from "orval";
|
6
|
+
import { z } from "zod";
|
7
|
+
import { orvalCustomConfig } from "../orval.config";
|
8
|
+
|
9
|
+
export type OrvalCustomConfig = {
|
10
|
+
apiDocs: {
|
11
|
+
outputPath: string;
|
12
|
+
};
|
13
|
+
endpoints: {
|
14
|
+
outputPath: string;
|
15
|
+
replaceVoidTypeToAnyOnResponse?: boolean;
|
16
|
+
};
|
17
|
+
zodSchemas: {
|
18
|
+
outputPath: string;
|
19
|
+
};
|
20
|
+
};
|
21
|
+
|
22
|
+
const envSchemaFromNodeEnvironment = z.object({
|
23
|
+
API_JSON_DOCS_URL: z
|
24
|
+
.string()
|
25
|
+
.url()
|
26
|
+
.transform(val => new URL(val).toString()),
|
27
|
+
});
|
28
|
+
|
29
|
+
const parsedEnv = envSchemaFromNodeEnvironment.safeParse(process.env);
|
30
|
+
|
31
|
+
if (!parsedEnv.success) {
|
32
|
+
console.error(parsedEnv.error.flatten().fieldErrors);
|
33
|
+
|
34
|
+
throw new Error("Invalid environment variables.");
|
35
|
+
}
|
36
|
+
|
37
|
+
const env = parsedEnv.data;
|
38
|
+
|
39
|
+
(async () => {
|
40
|
+
const apiJsonDocsResponse = await fetch(env.API_JSON_DOCS_URL);
|
41
|
+
const apiJsonDocs = await apiJsonDocsResponse.text();
|
42
|
+
const apiJsonDocsDirPath = dirname(orvalCustomConfig.apiDocs.outputPath);
|
43
|
+
|
44
|
+
if (!existsSync(apiJsonDocsDirPath)) await mkdir(apiJsonDocsDirPath);
|
45
|
+
|
46
|
+
await Promise.all([
|
47
|
+
rm(orvalCustomConfig.endpoints.outputPath, {
|
48
|
+
force: true,
|
49
|
+
recursive: true,
|
50
|
+
}),
|
51
|
+
rm(orvalCustomConfig.zodSchemas.outputPath, {
|
52
|
+
force: true,
|
53
|
+
recursive: true,
|
54
|
+
}),
|
55
|
+
writeFile(orvalCustomConfig.apiDocs.outputPath, apiJsonDocs),
|
56
|
+
]);
|
57
|
+
|
58
|
+
await generate("./orval.config.ts");
|
59
|
+
|
60
|
+
const generatedEndpointFiles = await Promise.all(
|
61
|
+
(await readdir(orvalCustomConfig.endpoints.outputPath)).map(
|
62
|
+
async relativePath => {
|
63
|
+
const filePath = resolve(
|
64
|
+
orvalCustomConfig.endpoints.outputPath,
|
65
|
+
relativePath,
|
66
|
+
);
|
67
|
+
|
68
|
+
return {
|
69
|
+
filePath,
|
70
|
+
fileContent: await readFile(filePath, "utf-8"),
|
71
|
+
};
|
72
|
+
},
|
73
|
+
),
|
74
|
+
);
|
75
|
+
|
76
|
+
await Promise.all(
|
77
|
+
generatedEndpointFiles.map(async endpointFile => {
|
78
|
+
let newContent = endpointFile.fileContent.replace(/data:/g, "body:");
|
79
|
+
|
80
|
+
if (orvalCustomConfig.endpoints.replaceVoidTypeToAnyOnResponse)
|
81
|
+
newContent = newContent.replace(/body: void/g, "body: any");
|
82
|
+
|
83
|
+
await writeFile(endpointFile.filePath, newContent);
|
84
|
+
}),
|
85
|
+
);
|
86
|
+
|
87
|
+
execSync(
|
88
|
+
`pnpm prettier ${orvalCustomConfig.apiDocs.outputPath} ${orvalCustomConfig.endpoints.outputPath} ${orvalCustomConfig.zodSchemas.outputPath} --write`,
|
89
|
+
);
|
90
|
+
})();
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import { RouteObject } from "react-router";
|
2
|
+
import { OverrideProperties } from "type-fest";
|
3
|
+
|
4
|
+
export type CustomRouteDefinition = {
|
5
|
+
path: string;
|
6
|
+
label: string;
|
7
|
+
};
|
8
|
+
|
9
|
+
export type ReactRouterRouteDefinition<Routes extends CustomRouteDefinition[]> =
|
10
|
+
OverrideProperties<
|
11
|
+
RouteObject,
|
12
|
+
{
|
13
|
+
path: Routes[number]["path"] | "*";
|
14
|
+
handle: ReactRouterRouteHandleDefinition;
|
15
|
+
}
|
16
|
+
>;
|
17
|
+
|
18
|
+
export type ReactRouterRouteHandleDefinition = {
|
19
|
+
metadata: {
|
20
|
+
title: string;
|
21
|
+
description?: string;
|
22
|
+
};
|
23
|
+
[K: string]: unknown;
|
24
|
+
};
|
@@ -0,0 +1,8 @@
|
|
1
|
+
const defaultMessage =
|
2
|
+
"Houve uma falha inesperada ao se comunicar com nosso servidor. Por favor, contate nosso suporte." as const;
|
3
|
+
|
4
|
+
export class ApiUnexpectedResponseError {
|
5
|
+
static message = defaultMessage;
|
6
|
+
|
7
|
+
constructor(public message = defaultMessage) {}
|
8
|
+
}
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import { ApiError } from "./errors/api-error";
|
2
|
+
import { ApiUnexpectedResponseError } from "./errors/api-unexpected-response-error";
|
3
|
+
|
4
|
+
type SwrFetcherOutput = {
|
5
|
+
body: unknown;
|
6
|
+
status: number;
|
7
|
+
headers: Headers;
|
8
|
+
};
|
9
|
+
|
10
|
+
export async function swrFetcher<Output extends SwrFetcherOutput>(
|
11
|
+
url: RequestInfo | URL,
|
12
|
+
options?: RequestInit,
|
13
|
+
): Promise<Output> {
|
14
|
+
const response = await fetch(url, options);
|
15
|
+
let body: unknown;
|
16
|
+
|
17
|
+
if (
|
18
|
+
response.headers.get("Content-Type")?.includes("application/json") &&
|
19
|
+
response.status !== 204
|
20
|
+
) {
|
21
|
+
try {
|
22
|
+
body = await response.json();
|
23
|
+
} catch {
|
24
|
+
body = {};
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
28
|
+
if (!response.ok) {
|
29
|
+
const error = body as ApiError | null;
|
30
|
+
|
31
|
+
if (error) throw new ApiError(error.statusCode, error.error, error.message);
|
32
|
+
|
33
|
+
throw new ApiUnexpectedResponseError();
|
34
|
+
}
|
35
|
+
|
36
|
+
return {
|
37
|
+
body,
|
38
|
+
status: response.status,
|
39
|
+
headers: response.headers,
|
40
|
+
} as Awaited<Output>;
|
41
|
+
}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import { Helmet, HelmetProvider } from "react-helmet-async";
|
2
|
+
import { RouterProvider } from "react-router/dom";
|
3
|
+
import { env } from "./env";
|
4
|
+
import { router } from "./routes";
|
5
|
+
|
6
|
+
export function App() {
|
7
|
+
return (
|
8
|
+
<HelmetProvider>
|
9
|
+
<Helmet titleTemplate={`%s | ${env.APP_NAME}`}></Helmet>
|
10
|
+
|
11
|
+
<RouterProvider router={router} />
|
12
|
+
</HelmetProvider>
|
13
|
+
);
|
14
|
+
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import { z } from "zod";
|
2
|
+
|
3
|
+
const schema = z.object({
|
4
|
+
APP_NAME: z.string(),
|
5
|
+
APP_API_BASE_URL: z
|
6
|
+
.string()
|
7
|
+
.url()
|
8
|
+
.transform(val => new URL(val).toString()),
|
9
|
+
});
|
10
|
+
|
11
|
+
const parsedEnv = schema.safeParse(import.meta.env);
|
12
|
+
|
13
|
+
if (!parsedEnv.success) {
|
14
|
+
console.error(parsedEnv.error.flatten().fieldErrors);
|
15
|
+
|
16
|
+
throw new Error("Invalid environment variables.");
|
17
|
+
}
|
18
|
+
|
19
|
+
export const env = parsedEnv.data;
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import { ReactRouterRouteHandleDefinition } from "@/@types/routes";
|
2
|
+
import { useLocation, useMatches } from "react-router";
|
3
|
+
|
4
|
+
export function useCurrentRouteHandleParams() {
|
5
|
+
const { pathname } = useLocation();
|
6
|
+
const routeMatches = useMatches();
|
7
|
+
const currentRouteHandleParams = routeMatches.find(routeMatch => {
|
8
|
+
return routeMatch.pathname === pathname && routeMatch.handle;
|
9
|
+
});
|
10
|
+
|
11
|
+
return {
|
12
|
+
pathname,
|
13
|
+
handleParams:
|
14
|
+
currentRouteHandleParams?.handle as ReactRouterRouteHandleDefinition,
|
15
|
+
};
|
16
|
+
}
|
@@ -0,0 +1,29 @@
|
|
1
|
+
* {
|
2
|
+
box-sizing: border-box;
|
3
|
+
margin: 0;
|
4
|
+
padding: 0;
|
5
|
+
font-size: 1.6rem;
|
6
|
+
line-height: 1.5;
|
7
|
+
|
8
|
+
@media (max-width: 640px) {
|
9
|
+
font-size: 1.5rem;
|
10
|
+
}
|
11
|
+
}
|
12
|
+
|
13
|
+
:root {
|
14
|
+
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
15
|
+
font-size: 62.5%;
|
16
|
+
font-synthesis: none;
|
17
|
+
text-rendering: optimizeLegibility;
|
18
|
+
-webkit-font-smoothing: antialiased;
|
19
|
+
-moz-osx-font-smoothing: grayscale;
|
20
|
+
}
|
21
|
+
|
22
|
+
body {
|
23
|
+
background-color: #0d0d0d;
|
24
|
+
color: #d4d4d8;
|
25
|
+
}
|
26
|
+
|
27
|
+
a {
|
28
|
+
text-decoration: none;
|
29
|
+
}
|
@@ -0,0 +1,103 @@
|
|
1
|
+
{
|
2
|
+
"types": {
|
3
|
+
"number": "número",
|
4
|
+
"string": "texto",
|
5
|
+
"integer": "número inteiro",
|
6
|
+
"boolean": "verdadeiro ou falso",
|
7
|
+
"date": "data",
|
8
|
+
"bigint": "número",
|
9
|
+
"undefined": "vazio",
|
10
|
+
"null": "vazio",
|
11
|
+
"array": "lista",
|
12
|
+
"object": "objeto"
|
13
|
+
},
|
14
|
+
"errors": {
|
15
|
+
"invalid_type": "O campo deve ser {{expected}}, foi recebido {{received}}.",
|
16
|
+
"invalid_type_received_undefined": "O campo é obrigatório.",
|
17
|
+
"invalid_type_received_null": "O campo é obrigatório.",
|
18
|
+
"invalid_literal": "O campo deve ser apenas \"{{expected}}\".",
|
19
|
+
"unrecognized_keys": "Chave(s) não reconhecida(s) no objeto: {{- keys}}.",
|
20
|
+
"invalid_union": "Entrada inválida.",
|
21
|
+
"invalid_union_discriminator": "O campo deve conter apenas os seguintes valores: {{- options}}.",
|
22
|
+
"invalid_enum_value": "O campo deve conter apenas os seguintes valores: {{- options}}.",
|
23
|
+
"invalid_arguments": "Argumento de função inválido.",
|
24
|
+
"invalid_return_type": "Tipo de retorno de função inválido.",
|
25
|
+
"invalid_date": "A data é inválida.",
|
26
|
+
"custom": "A entrada é inválida.",
|
27
|
+
"invalid_intersection_types": "Os valores não podem ser mesclados.",
|
28
|
+
"not_multiple_of": "O número deverá ser múltiplo de {{multipleOf}}.",
|
29
|
+
"not_finite": "O número não pode ser infinito.",
|
30
|
+
"invalid_string": {
|
31
|
+
"email": "O e-mail é inválido.",
|
32
|
+
"url": "A URL é inválida.",
|
33
|
+
"uuid": "O UUID é inválido.",
|
34
|
+
"cuid": "O CUID é inválido.",
|
35
|
+
"regex": "A combinação é inválida.",
|
36
|
+
"datetime": "A data e a hora são inválidos.",
|
37
|
+
"startsWith": "O campo só pode iniciar com \"{{startsWith}}\".",
|
38
|
+
"endsWith": "O campo só pode iniciar com \"{{endsWith}}\"."
|
39
|
+
},
|
40
|
+
"too_small": {
|
41
|
+
"array": {
|
42
|
+
"exact": "A lista deve conter exatamente {{minimum}} item / itens.",
|
43
|
+
"inclusive": "A lista deve conter no mínimo {{minimum}} item / itens.",
|
44
|
+
"not_inclusive": "A lista deve conter mais de {{minimum}} item / itens."
|
45
|
+
},
|
46
|
+
"string": {
|
47
|
+
"exact": "O campo deve conter exatamente {{minimum}} caracter(es).",
|
48
|
+
"inclusive": "O campo é obrigatório com no mínimo {{minimum}} caracter(es).",
|
49
|
+
"not_inclusive": "O campo deve conter mais de {{minimum}} caracter(es)."
|
50
|
+
},
|
51
|
+
"number": {
|
52
|
+
"exact": "O número deve conter exatamente {{minimum}} caracter(es).",
|
53
|
+
"inclusive": "O número deve ser maior ou igual a {{minimum}}.",
|
54
|
+
"not_inclusive": "O número deve ser maior que {{minimum}}."
|
55
|
+
},
|
56
|
+
"set": {
|
57
|
+
"exact": "Entrada inválida.",
|
58
|
+
"inclusive": "Entrada inválida.",
|
59
|
+
"not_inclusive": "Entrada inválida."
|
60
|
+
},
|
61
|
+
"date": {
|
62
|
+
"exact": "A data deve ser exatamente {{- maximum, datetime}}.",
|
63
|
+
"inclusive": "A data deve ser maior ou igual a {{- minimum, datetime}}.",
|
64
|
+
"not_inclusive": "A data deve ser maior que {{- minimum, datetime}}."
|
65
|
+
}
|
66
|
+
},
|
67
|
+
"too_big": {
|
68
|
+
"array": {
|
69
|
+
"exact": "A lista deve conter exatamente {{minimum}} item / itens.",
|
70
|
+
"inclusive": "A lista deve conter no mínimo {{minimum}} item / itens.",
|
71
|
+
"not_inclusive": "A lista deve conter mais de {{minimum}} item / itens."
|
72
|
+
},
|
73
|
+
"string": {
|
74
|
+
"exact": "O campo deve conter exatamente {{minimum}} caracter(es).",
|
75
|
+
"inclusive": "O campo deve conter pelo menos {{minimum}} caracter(es).",
|
76
|
+
"not_inclusive": "O campo deve conter mais de {{minimum}} caracter(es)."
|
77
|
+
},
|
78
|
+
"number": {
|
79
|
+
"exact": "O número deve conter exatamente {{minimum}} caracter(es).",
|
80
|
+
"inclusive": "O número deve ser maior ou igual a {{minimum}}.",
|
81
|
+
"not_inclusive": "O número deve ser maior que {{minimum}}."
|
82
|
+
},
|
83
|
+
"set": {
|
84
|
+
"exact": "Entrada inválida.",
|
85
|
+
"inclusive": "Entrada inválida.",
|
86
|
+
"not_inclusive": "Entrada inválida."
|
87
|
+
},
|
88
|
+
"date": {
|
89
|
+
"exact": "A data deve ser exatamente {{- maximum, datetime}}.",
|
90
|
+
"inclusive": "A data deve ser maior ou igual a {{- minimum, datetime}}.",
|
91
|
+
"not_inclusive": "A data deve ser maior que {{- minimum, datetime}}."
|
92
|
+
}
|
93
|
+
}
|
94
|
+
},
|
95
|
+
"validations": {
|
96
|
+
"email": "email",
|
97
|
+
"url": "url",
|
98
|
+
"uuid": "uuid",
|
99
|
+
"cuid": "cuid",
|
100
|
+
"regex": "regex",
|
101
|
+
"datetime": "datetime"
|
102
|
+
}
|
103
|
+
}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import i18next from "i18next";
|
2
|
+
import { z } from "zod";
|
3
|
+
import { zodI18nMap } from "zod-i18n-map";
|
4
|
+
import translation from "zod-i18n-map/locales/pt/zod.json";
|
5
|
+
import zodI18nTranslationForEndUsers from "./zod-i18n-translation-for-end-users.json";
|
6
|
+
|
7
|
+
export function startZodWithI18n() {
|
8
|
+
i18next.init({
|
9
|
+
lng: "pt",
|
10
|
+
resources: {
|
11
|
+
pt: {
|
12
|
+
zod: { ...translation, ...zodI18nTranslationForEndUsers },
|
13
|
+
},
|
14
|
+
},
|
15
|
+
});
|
16
|
+
|
17
|
+
z.setErrorMap(zodI18nMap);
|
18
|
+
}
|
19
|
+
|
20
|
+
export { z };
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import { StrictMode } from "react";
|
2
|
+
import { createRoot } from "react-dom/client";
|
3
|
+
import { App } from "./app";
|
4
|
+
import "./index.css";
|
5
|
+
import { startZodWithI18n } from "./lib/zod-i18n";
|
6
|
+
|
7
|
+
startZodWithI18n();
|
8
|
+
|
9
|
+
createRoot(document.getElementById("root")!).render(
|
10
|
+
<StrictMode>
|
11
|
+
<App />
|
12
|
+
</StrictMode>,
|
13
|
+
);
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import { useCurrentRouteHandleParams } from "@/hooks/use-current-route-handle-params";
|
2
|
+
import { Helmet } from "react-helmet-async";
|
3
|
+
import { Outlet } from "react-router";
|
4
|
+
|
5
|
+
export function AppLayout() {
|
6
|
+
const { handleParams } = useCurrentRouteHandleParams();
|
7
|
+
|
8
|
+
return (
|
9
|
+
<>
|
10
|
+
<Helmet title={handleParams.metadata.title} />
|
11
|
+
|
12
|
+
<div>
|
13
|
+
<Outlet />
|
14
|
+
</div>
|
15
|
+
</>
|
16
|
+
);
|
17
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import { useCurrentRouteHandleParams } from "@/hooks/use-current-route-handle-params";
|
2
|
+
import { Helmet } from "react-helmet-async";
|
3
|
+
import { Outlet } from "react-router";
|
4
|
+
|
5
|
+
export function AuthLayout() {
|
6
|
+
const { handleParams } = useCurrentRouteHandleParams();
|
7
|
+
|
8
|
+
return (
|
9
|
+
<>
|
10
|
+
<Helmet title={handleParams.metadata.title} />
|
11
|
+
|
12
|
+
<div>
|
13
|
+
<Outlet />
|
14
|
+
</div>
|
15
|
+
</>
|
16
|
+
);
|
17
|
+
}
|
@@ -0,0 +1,40 @@
|
|
1
|
+
.dashboard-wrapper {
|
2
|
+
height: 100%;
|
3
|
+
display: flex;
|
4
|
+
flex-direction: column;
|
5
|
+
justify-content: center;
|
6
|
+
align-items: center;
|
7
|
+
padding-left: 15px;
|
8
|
+
padding-right: 15px;
|
9
|
+
}
|
10
|
+
|
11
|
+
.dashboard-wrapper > h1 {
|
12
|
+
font-size: 4.2rem;
|
13
|
+
line-height: 1.2;
|
14
|
+
letter-spacing: 0.8px;
|
15
|
+
font-weight: 800;
|
16
|
+
color: #fff;
|
17
|
+
margin-bottom: 4rem;
|
18
|
+
|
19
|
+
@media (max-width: 640px) {
|
20
|
+
font-size: 3.6rem;
|
21
|
+
line-height: 1.3;
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
.dashboard-wrapper > a {
|
26
|
+
display: inline-block;
|
27
|
+
padding: 1rem 1.6rem;
|
28
|
+
font-weight: 800;
|
29
|
+
text-align: center;
|
30
|
+
border: 1px solid transparent;
|
31
|
+
border-radius: 6px;
|
32
|
+
background-color: #fff;
|
33
|
+
color: #000;
|
34
|
+
cursor: pointer;
|
35
|
+
transition: opacity 150ms ease-in-out;
|
36
|
+
}
|
37
|
+
|
38
|
+
.dashboard-wrapper > a:hover {
|
39
|
+
opacity: 0.8;
|
40
|
+
}
|
@@ -0,0 +1,83 @@
|
|
1
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
2
|
+
import { useForm } from "react-hook-form";
|
3
|
+
import { useNavigate } from "react-router";
|
4
|
+
import { z } from "zod";
|
5
|
+
import "./styles.css";
|
6
|
+
|
7
|
+
const signInFormSchema = z.object({
|
8
|
+
email: z.string().min(1).email(),
|
9
|
+
password: z.string().min(6).max(255),
|
10
|
+
});
|
11
|
+
|
12
|
+
type SignInForm = z.infer<typeof signInFormSchema>;
|
13
|
+
|
14
|
+
export function SignIn() {
|
15
|
+
const signInForm = useForm<SignInForm>({
|
16
|
+
resolver: zodResolver(signInFormSchema),
|
17
|
+
defaultValues: {
|
18
|
+
email: "",
|
19
|
+
password: "",
|
20
|
+
},
|
21
|
+
});
|
22
|
+
const navigate = useNavigate();
|
23
|
+
const onSignIn = async (input: SignInForm) => {
|
24
|
+
console.log(input);
|
25
|
+
navigate("/dashboard");
|
26
|
+
};
|
27
|
+
|
28
|
+
return (
|
29
|
+
<div className="sign-in-wrapper">
|
30
|
+
<div className="sign-in-container">
|
31
|
+
<div className="sign-in-header">
|
32
|
+
<h1>Sign In</h1>
|
33
|
+
|
34
|
+
<p>Log in to access the dashboard!</p>
|
35
|
+
</div>
|
36
|
+
|
37
|
+
<form
|
38
|
+
className="sign-in-form"
|
39
|
+
onSubmit={signInForm.handleSubmit(onSignIn)}
|
40
|
+
noValidate
|
41
|
+
>
|
42
|
+
<div className="sign-in-form-input-container">
|
43
|
+
<input
|
44
|
+
{...signInForm.register("email")}
|
45
|
+
type="email"
|
46
|
+
placeholder="Your email"
|
47
|
+
autoComplete="email"
|
48
|
+
autoCapitalize="none"
|
49
|
+
autoCorrect="off"
|
50
|
+
/>
|
51
|
+
|
52
|
+
{signInForm.formState.errors.email && (
|
53
|
+
<span className="sign-in-form-input-error">
|
54
|
+
{signInForm.formState.errors.email.message}
|
55
|
+
</span>
|
56
|
+
)}
|
57
|
+
</div>
|
58
|
+
|
59
|
+
<div className="sign-in-form-input-container">
|
60
|
+
<input
|
61
|
+
{...signInForm.register("password")}
|
62
|
+
type="password"
|
63
|
+
placeholder="Your password"
|
64
|
+
autoComplete="current-password"
|
65
|
+
autoCapitalize="none"
|
66
|
+
autoCorrect="off"
|
67
|
+
/>
|
68
|
+
|
69
|
+
{signInForm.formState.errors.password && (
|
70
|
+
<span className="sign-in-form-input-error">
|
71
|
+
{signInForm.formState.errors.password.message}
|
72
|
+
</span>
|
73
|
+
)}
|
74
|
+
</div>
|
75
|
+
|
76
|
+
<button className="sign-in-form-submit" type="submit">
|
77
|
+
Sign In
|
78
|
+
</button>
|
79
|
+
</form>
|
80
|
+
</div>
|
81
|
+
</div>
|
82
|
+
);
|
83
|
+
}
|
@@ -0,0 +1,92 @@
|
|
1
|
+
.sign-in-wrapper {
|
2
|
+
height: 100%;
|
3
|
+
display: flex;
|
4
|
+
flex-direction: column;
|
5
|
+
justify-content: center;
|
6
|
+
align-items: center;
|
7
|
+
padding-left: 15px;
|
8
|
+
padding-right: 15px;
|
9
|
+
}
|
10
|
+
|
11
|
+
.sign-in-container {
|
12
|
+
max-width: 500px;
|
13
|
+
width: 100%;
|
14
|
+
margin-left: auto;
|
15
|
+
margin-right: auto;
|
16
|
+
padding: 8rem 4rem;
|
17
|
+
border-radius: 25px;
|
18
|
+
border: 1px solid #1c1c1e;
|
19
|
+
|
20
|
+
@media (max-width: 640px) {
|
21
|
+
padding: 4rem 2rem;
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
.sign-in-header {
|
26
|
+
display: flex;
|
27
|
+
flex-direction: column;
|
28
|
+
text-align: center;
|
29
|
+
margin-bottom: 4rem;
|
30
|
+
}
|
31
|
+
|
32
|
+
.sign-in-header > h1 {
|
33
|
+
font-size: 4.2rem;
|
34
|
+
line-height: 1.2;
|
35
|
+
letter-spacing: 0.8px;
|
36
|
+
font-weight: 800;
|
37
|
+
color: #fff;
|
38
|
+
margin-bottom: 0.8rem;
|
39
|
+
|
40
|
+
@media (max-width: 640px) {
|
41
|
+
font-size: 3.6rem;
|
42
|
+
line-height: 1.3;
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
.sign-in-header > p {
|
47
|
+
color: #71717a;
|
48
|
+
}
|
49
|
+
|
50
|
+
.sign-in-form-input-container + .sign-in-form-input-container {
|
51
|
+
margin-top: 2rem;
|
52
|
+
}
|
53
|
+
|
54
|
+
.sign-in-form-input-container input {
|
55
|
+
display: block;
|
56
|
+
width: 100%;
|
57
|
+
padding: 1rem 1.6rem;
|
58
|
+
border: 1px solid #1c1c1e;
|
59
|
+
border-radius: 6px;
|
60
|
+
background-color: transparent;
|
61
|
+
color: #fff;
|
62
|
+
}
|
63
|
+
|
64
|
+
.sign-in-form-input-container input:focus {
|
65
|
+
outline: 1px solid #fff;
|
66
|
+
}
|
67
|
+
|
68
|
+
.sign-in-form-input-error {
|
69
|
+
display: block;
|
70
|
+
margin-top: 0.4rem;
|
71
|
+
font-size: 1.48rem;
|
72
|
+
color: #dc2626;
|
73
|
+
}
|
74
|
+
|
75
|
+
.sign-in-form-submit {
|
76
|
+
display: block;
|
77
|
+
width: 100%;
|
78
|
+
padding: 1rem 1.6rem;
|
79
|
+
font-weight: 800;
|
80
|
+
text-align: center;
|
81
|
+
border: 1px solid transparent;
|
82
|
+
border-radius: 6px;
|
83
|
+
background-color: #fff;
|
84
|
+
color: #000;
|
85
|
+
margin-top: 5rem;
|
86
|
+
cursor: pointer;
|
87
|
+
transition: opacity 150ms ease-in-out;
|
88
|
+
}
|
89
|
+
|
90
|
+
.sign-in-form-submit:hover {
|
91
|
+
opacity: 0.8;
|
92
|
+
}
|