refactor(lint): code styled with prettier

This commit is contained in:
2024-11-03 18:14:48 +01:00
parent 712ffd7ef8
commit 3d0a996744
41 changed files with 938 additions and 942 deletions

View File

@@ -8,10 +8,6 @@
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules"
]
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@@ -24,8 +24,8 @@ This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-opti
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

View File

@@ -2,23 +2,23 @@ import path from "path";
/** @type {import('next').NextConfig} */
const nextConfig = {
/**
* https://nextjs.org/docs/app/building-your-application/styling/sass
*/
sassOptions: {
// optional: prependData: `@import "@/styles/variables";`,
includePaths: [path.join("@", "styles")],
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "picsum.photos",
port: "",
pathname: "/**",
},
],
},
/**
* https://nextjs.org/docs/app/building-your-application/styling/sass
*/
sassOptions: {
// optional: prependData: `@import "@/styles/variables";`,
includePaths: [path.join("@", "styles")],
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "picsum.photos",
port: "",
pathname: "/**",
},
],
},
};
export default nextConfig;

View File

@@ -1,15 +1,15 @@
export default function Home() {
return (
<main
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
}}
>
Main page
</main>
);
return (
<main
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
}}
>
Main page
</main>
);
}

View File

@@ -2,38 +2,38 @@ import { withAuth } from "next-auth/middleware";
import { authOptions } from "./modules/auth/configs/auth.options";
export default withAuth({
pages: authOptions.pages,
callbacks: {
authorized({ req, token }) {
if (token && token.apiSession.exp * 1000 > Date.now()) {
return true;
}
const pathname = req.nextUrl.pathname;
return (
pathname.startsWith("/_next/") ||
pathname.startsWith("/favicon.ico") ||
pathname.startsWith("/assets/")
);
pages: authOptions.pages,
callbacks: {
authorized({ req, token }) {
if (token && token.apiSession.exp * 1000 > Date.now()) {
return true;
}
const pathname = req.nextUrl.pathname;
return (
pathname.startsWith("/_next/") ||
pathname.startsWith("/favicon.ico") ||
pathname.startsWith("/assets/")
);
},
},
},
});
const value = {
token: {
name: "dqnid",
picture: "https://picsum.photos/200/300",
sub: "dqnid",
user: {
id: "dqnid",
roles: ["user", "manager", "admin"],
image: "https://picsum.photos/200/300",
name: "dqnid",
token: {
name: "dqnid",
picture: "https://picsum.photos/200/300",
sub: "dqnid",
user: {
id: "dqnid",
roles: ["user", "manager", "admin"],
image: "https://picsum.photos/200/300",
name: "dqnid",
},
apiSession: {
exp: 1725398177,
},
iat: 1725394577,
exp: 1727986577,
jti: "3203d3c7-dc27-4599-b37e-16737b3a6674",
},
apiSession: {
exp: 1725398177,
},
iat: 1725394577,
exp: 1727986577,
jti: "3203d3c7-dc27-4599-b37e-16737b3a6674",
},
};

View File

@@ -1,179 +1,179 @@
.container {
max-width: 50em;
height: fit-content;
max-width: 50em;
height: fit-content;
display: flex;
flex-direction: row;
font-size: 1.4rem;
overflow: hidden;
border-radius: 0.4rem;
border: 2px solid rgba(var(--color-grey-90-rgb), 0.4);
background-color: var(--color-white);
.side__illustration {
display: flex;
flex-direction: column;
justify-content: space-between;
max-width: 16em;
box-sizing: content-box;
opacity: 0.8;
flex-direction: row;
text-shadow: rgba(var(--color-grey-10), 0.3);
padding: 2rem 2rem;
font-size: 1.4rem;
overflow: hidden;
--s: 100px;
--c1: rgba(var(--color-grey-70-rgb), 0.4);
--c2: var(--color-white);
border-radius: 0.4rem;
border: 2px solid rgba(var(--color-grey-90-rgb), 0.4);
background-color: var(--color-white);
--_g: #0000 90deg, var(--c1) 0;
background: conic-gradient(from 90deg at 2px 2px, var(--_g)),
conic-gradient(from 90deg at 1px 1px, var(--_g)), var(--c2);
background-size:
var(--s) var(--s),
calc(var(--s) / 5) calc(var(--s) / 5);
background-position: -2px -2px;
.side__illustration {
display: flex;
flex-direction: column;
justify-content: space-between;
max-width: 16em;
box-sizing: content-box;
opacity: 0.8;
& > h1 {
font-size: 3.4rem;
font-weight: bold;
text-align: left;
text-shadow: rgba(var(--color-grey-10), 0.3);
padding: 2rem 2rem;
padding-right: 2rem;
--s: 100px;
--c1: rgba(var(--color-grey-70-rgb), 0.4);
--c2: var(--color-white);
& > span {
position: relative;
--_g: #0000 90deg, var(--c1) 0;
background: conic-gradient(from 90deg at 2px 2px, var(--_g)),
conic-gradient(from 90deg at 1px 1px, var(--_g)), var(--c2);
background-size:
var(--s) var(--s),
calc(var(--s) / 5) calc(var(--s) / 5);
background-position: -2px -2px;
&::after {
content: "";
position: absolute;
height: 40%;
width: 110%;
top: 70%;
left: -5%;
& > h1 {
font-size: 3.4rem;
font-weight: bold;
text-align: left;
z-index: -1;
padding-right: 2rem;
background-color: var(--color-primary);
}
}
}
& > span {
position: relative;
& > h4 {
font-size: 1.4rem;
&::after {
content: "";
position: absolute;
height: 40%;
width: 110%;
top: 70%;
left: -5%;
text-align: right;
padding-left: 2rem;
}
}
z-index: -1;
.form {
display: flex;
flex-direction: column;
align-items: center;
gap: 1em;
padding: 3.5em 2.5em;
background: rgba(var(--color-grey-90-rgb), 0.4);
&__group {
display: flex;
flex-direction: column;
gap: 0.2em;
}
&__label {
margin-left: 0.8em;
color: rgba(var(--color-grey-60-rgb), 0.8);
transition: all 0.3s;
}
&__input {
padding: 0.8em;
background-color: transparent;
border: none;
border-bottom: 2px solid var(--color-grey-70);
border-radius: 2px;
transition: all 0.2s;
&::placeholder {
opacity: 1;
color: rgba(var(--color-grey-60-rgb), 0.8);
}
&:focus {
outline: none;
border-bottom: 2px solid var(--color-primary);
box-shadow: 0 1px 4px rgba(var(--color-primary-dark-rgb), 0.3);
}
}
&__group:has(.form__input:placeholder-shown) .form__label {
opacity: 0;
transform: translateY(1rem);
}
&__actions {
width: 100%;
display: flex;
flex-direction: column;
gap: 0.5em;
& .submit__button {
all: unset;
width: fit-content;
font-size: 1.4rem;
text-transform: uppercase;
padding: 0.5em 1em;
border-radius: 2px;
align-self: center;
border: none;
color: var(--color-black);
border: 2px solid var(--color-primary);
transition: all 0.2s;
&:hover {
color: var(--color-white);
background-color: var(--color-primary);
cursor: pointer;
transform: translateY(-2px);
box-shadow: 0 2px 6px rgba(var(--color-primary-dark-rgb), 0.4);
background-color: var(--color-primary);
}
}
}
&:active {
transform: translateY(0);
& > h4 {
font-size: 1.4rem;
text-align: right;
padding-left: 2rem;
}
}
}
&__message {
opacity: 0;
height: 2.5rem;
.form {
display: flex;
flex-direction: column;
align-items: center;
gap: 1em;
align-self: flex-start;
padding: 3.5em 2.5em;
transition: opacity 0.3s ease-in;
background: rgba(var(--color-grey-90-rgb), 0.4);
&--wrong {
color: var(--color-error);
}
&__group {
display: flex;
flex-direction: column;
gap: 0.2em;
}
&__label {
margin-left: 0.8em;
color: rgba(var(--color-grey-60-rgb), 0.8);
transition: all 0.3s;
}
&__input {
padding: 0.8em;
background-color: transparent;
border: none;
border-bottom: 2px solid var(--color-grey-70);
border-radius: 2px;
transition: all 0.2s;
&::placeholder {
opacity: 1;
color: rgba(var(--color-grey-60-rgb), 0.8);
}
&:focus {
outline: none;
border-bottom: 2px solid var(--color-primary);
box-shadow: 0 1px 4px rgba(var(--color-primary-dark-rgb), 0.3);
}
}
&__group:has(.form__input:placeholder-shown) .form__label {
opacity: 0;
transform: translateY(1rem);
}
&__actions {
width: 100%;
display: flex;
flex-direction: column;
gap: 0.5em;
& .submit__button {
all: unset;
width: fit-content;
font-size: 1.4rem;
text-transform: uppercase;
padding: 0.5em 1em;
border-radius: 2px;
align-self: center;
border: none;
color: var(--color-black);
border: 2px solid var(--color-primary);
transition: all 0.2s;
&:hover {
color: var(--color-white);
background-color: var(--color-primary);
cursor: pointer;
transform: translateY(-2px);
box-shadow: 0 2px 6px rgba(var(--color-primary-dark-rgb), 0.4);
}
&:active {
transform: translateY(0);
}
}
}
&__message {
opacity: 0;
height: 2.5rem;
align-self: flex-start;
transition: opacity 0.3s ease-in;
&--wrong {
color: var(--color-error);
}
}
}
}
}
.form--error {
.form__input {
border-bottom: 2px solid var(--color-error);
}
.form__input {
border-bottom: 2px solid var(--color-error);
}
.form__message {
opacity: 1;
}
.form__message {
opacity: 1;
}
}

View File

@@ -7,101 +7,101 @@ import { signIn, useSession } from "next-auth/react";
import { useRouter, useSearchParams } from "next/navigation";
type LogInWidgetsProps = {
afterSuccess?: Function;
afterSuccess?: Function;
};
export const LogInWidget: React.FC<LogInWidgetsProps> = ({ afterSuccess }) => {
const router = useRouter();
const searchParams = useSearchParams();
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl");
const callbackUrl = searchParams.get("callbackUrl");
const [loginStatus, setLoginStatus] = useState<
"idle" | "check" | "confirm" | "error"
>("idle");
const [loginStatus, setLoginStatus] = useState<
"idle" | "check" | "confirm" | "error"
>("idle");
const submit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setLoginStatus("check");
const username = (
event.currentTarget.elements.namedItem("username") as HTMLInputElement
).value;
const password = (
event.currentTarget.elements.namedItem("password") as HTMLInputElement
).value;
const submit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setLoginStatus("check");
const username = (
event.currentTarget.elements.namedItem("username") as HTMLInputElement
).value;
const password = (
event.currentTarget.elements.namedItem("password") as HTMLInputElement
).value;
const result = await signIn("credentials", {
redirect: false,
username: username,
password: password,
});
const result = await signIn("credentials", {
redirect: false,
username: username,
password: password,
});
if (!result?.ok) {
setLoginStatus("error");
} else {
setLoginStatus("confirm");
}
};
if (!result?.ok) {
setLoginStatus("error");
} else {
setLoginStatus("confirm");
}
};
useEffect(() => {
if (loginStatus === "confirm") {
router.push(callbackUrl ?? "/");
}
}, [loginStatus]);
useEffect(() => {
if (loginStatus === "confirm") {
router.push(callbackUrl ?? "/");
}
}, [loginStatus]);
return (
<div className={styles.container}>
<div className={styles["side__illustration"]}>
<h1>
Welcome to this <span>page!</span>
</h1>
<h4>Use your credentials to log-in</h4>
</div>
<form
className={`${styles.form} ${loginStatus === "error" && styles["form--error"]} ${loginStatus === "check" && styles["form--loading"]}`}
onSubmit={submit}
>
<div className={`${styles["form__group"]}`}>
<label htmlFor="username" className={styles["form__label"]}>
username
</label>
<input
type="text"
id="username"
name="username"
placeholder="username"
className={styles["form__input"]}
required
/>
return (
<div className={styles.container}>
<div className={styles["side__illustration"]}>
<h1>
Welcome to this <span>page!</span>
</h1>
<h4>Use your credentials to log-in</h4>
</div>
<form
className={`${styles.form} ${loginStatus === "error" && styles["form--error"]} ${loginStatus === "check" && styles["form--loading"]}`}
onSubmit={submit}
>
<div className={`${styles["form__group"]}`}>
<label htmlFor="username" className={styles["form__label"]}>
username
</label>
<input
type="text"
id="username"
name="username"
placeholder="username"
className={styles["form__input"]}
required
/>
</div>
<div className={styles["form__group"]}>
<label htmlFor="password" className={styles["form__label"]}>
password
</label>
<input
type="password"
id="password"
name="password"
placeholder="password"
className={styles["form__input"]}
required
/>
</div>
<div className={styles["form__actions"]}>
<div className={styles["form__message"]}>
{loginStatus === "error" ? (
<p className={styles["form__message--wrong"]}>
Wrong credentials, try again
</p>
) : (
<>Loading...</>
)}
</div>
<button type="submit" className={styles["submit__button"]}>
Log-in
</button>
</div>
</form>
</div>
<div className={styles["form__group"]}>
<label htmlFor="password" className={styles["form__label"]}>
password
</label>
<input
type="password"
id="password"
name="password"
placeholder="password"
className={styles["form__input"]}
required
/>
</div>
<div className={styles["form__actions"]}>
<div className={styles["form__message"]}>
{loginStatus === "error" ? (
<p className={styles["form__message--wrong"]}>
Wrong credentials, try again
</p>
) : (
<>Loading...</>
)}
</div>
<button type="submit" className={styles["submit__button"]}>
Log-in
</button>
</div>
</form>
</div>
);
);
};

View File

@@ -2,9 +2,9 @@ import { signIn } from "next-auth/react";
import styles from "./sign-in-button.module.scss";
export const SignInButton = () => {
return (
<button className={styles.button} onClick={() => void signIn()}>
Sign in
</button>
);
return (
<button className={styles.button} onClick={() => void signIn()}>
Sign in
</button>
);
};

View File

@@ -2,12 +2,12 @@ import { signOut } from "next-auth/react";
import styles from "./sign-out-button.module.scss";
export const SignOutButton = () => {
return (
<button
className={styles.button}
onClick={() => void signOut({ callbackUrl: "/" })}
>
Sign out
</button>
);
return (
<button
className={styles.button}
onClick={() => void signOut({ callbackUrl: "/" })}
>
Sign out
</button>
);
};

View File

@@ -3,14 +3,14 @@ import UserPanel from "../user-panel";
import { UserFrame } from "../user-frame";
export const UserDropdown = async () => {
return (
<div className={styles.container}>
<div className={styles["avatar__container"]}>
<UserFrame />
</div>
<div className={styles["dropdown__container"]}>
<UserPanel />
</div>
</div>
);
return (
<div className={styles.container}>
<div className={styles["avatar__container"]}>
<UserFrame />
</div>
<div className={styles["dropdown__container"]}>
<UserPanel />
</div>
</div>
);
};

View File

@@ -1,26 +1,26 @@
.container {
position: relative;
overflow: visible;
position: relative;
overflow: visible;
.avatar__container {
height: var(--header-item-height, 4rem);
width: var(--header-item-height, 4rem);
.avatar__container {
height: var(--header-item-height, 4rem);
width: var(--header-item-height, 4rem);
font-size: 3.2rem;
}
font-size: 3.2rem;
}
.dropdown__container {
position: absolute;
right: 0;
.dropdown__container {
position: absolute;
right: 0;
opacity: 0;
visibility: hidden;
opacity: 0;
visibility: hidden;
transition: all 0.2s;
}
transition: all 0.2s;
}
&:hover > .dropdown__container {
opacity: 1;
visibility: visible;
}
&:hover > .dropdown__container {
opacity: 1;
visibility: visible;
}
}

View File

@@ -5,39 +5,39 @@ import { getServerSession } from "next-auth";
import SignInButton from "../sign-in-button";
export async function UserFrame() {
const session = await getServerSession();
const session = await getServerSession();
if (!session) {
return <SignInButton />;
}
if (!session) {
return <SignInButton />;
}
return (
<div className={styles.frame}>
<span className={styles.initials}>
{getInitialsFromName(session?.user?.name || "00")}
</span>
<Image
className={styles["profile__picture"]}
src={session?.user?.image || ""}
alt="user profile picture"
fill={true}
sizes="(max-width: 768px) 10rem, 6rem"
priority={true}
/>
</div>
);
return (
<div className={styles.frame}>
<span className={styles.initials}>
{getInitialsFromName(session?.user?.name || "00")}
</span>
<Image
className={styles["profile__picture"]}
src={session?.user?.image || ""}
alt="user profile picture"
fill={true}
sizes="(max-width: 768px) 10rem, 6rem"
priority={true}
/>
</div>
);
}
const getInitialsFromName = (name: string): string => {
if (name.length < 1) return "00";
if (name.length < 1) return "00";
// TODO: this can be done in one line probably
const words = name.split(" ");
let initials = "";
words.forEach((word) => (initials += word[0]));
if (initials.length < 2) {
initials += name[1];
}
// TODO: this can be done in one line probably
const words = name.split(" ");
let initials = "";
words.forEach((word) => (initials += word[0]));
if (initials.length < 2) {
initials += name[1];
}
return initials.slice(0, 2);
return initials.slice(0, 2);
};

View File

@@ -1,23 +1,23 @@
.frame {
position: relative;
font-size: inherit;
width: 100%;
height: 100%;
border-radius: 50%;
overflow: hidden;
box-shadow: 1px 1px 3px var(--color-grey-30);
box-sizing: border-box;
& > span {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
position: relative;
font-size: inherit;
text-transform: uppercase;
color: var(--color-grey-30);
}
width: 100%;
height: 100%;
border-radius: 50%;
overflow: hidden;
box-shadow: 1px 1px 3px var(--color-grey-30);
box-sizing: border-box;
& > span {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: inherit;
text-transform: uppercase;
color: var(--color-grey-30);
}
}

View File

@@ -4,20 +4,20 @@ import { useSession } from "next-auth/react";
import ThemeSwitcher from "@/modules/common/components/theme-switcher";
export function UserPanel() {
const session = useSession();
const session = useSession();
return (
<ul className={styles.frame}>
<li className={styles["user__name"]}>{session.data?.user?.name}</li>
<ul className={styles["user__roles"]}>
{session.data?.user?.roles.map((role) => (
<li key={role} className={styles["role__chip"]}>
{role}
</li>
))}
</ul>
<ThemeSwitcher />
<SignOutButton />
</ul>
);
return (
<ul className={styles.frame}>
<li className={styles["user__name"]}>{session.data?.user?.name}</li>
<ul className={styles["user__roles"]}>
{session.data?.user?.roles.map((role) => (
<li key={role} className={styles["role__chip"]}>
{role}
</li>
))}
</ul>
<ThemeSwitcher />
<SignOutButton />
</ul>
);
}

View File

@@ -1,39 +1,39 @@
.frame {
list-style: none;
min-width: 15rem;
display: flex;
flex-direction: column;
gap: 1.8rem 0.6rem;
list-style: none;
min-width: 15rem;
display: flex;
flex-direction: column;
gap: 1.8rem 0.6rem;
padding: 1.2rem 1.8rem;
padding: 1.2rem 1.8rem;
border-radius: 6px;
background-color: var(--color-white);
box-shadow: 0 2px 3px rgba(var(--color-grey-10-rgb), 0.2);
color: var(--color-background);
border-radius: 6px;
background-color: var(--color-white);
box-shadow: 0 2px 3px rgba(var(--color-grey-10-rgb), 0.2);
color: var(--color-background);
z-index: 10;
z-index: 10;
& .user {
font-size: 1.6rem;
& .user {
font-size: 1.6rem;
&__name {
&__name {
}
&__roles {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
& .role__chip {
padding: 0.4rem 0.6rem;
border-radius: 4px;
background-color: var(--color-grey-90);
color: var(--color-grey-10);
}
}
}
&__roles {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
& .role__chip {
padding: 0.4rem 0.6rem;
border-radius: 4px;
background-color: var(--color-grey-90);
color: var(--color-grey-10);
}
}
}
}

View File

@@ -2,88 +2,92 @@ import { AuthOptions, Role, User } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export const authOptions: AuthOptions = {
session: {
strategy: "jwt",
},
pages: {
signIn: "/auth/log-in",
signOut: "/",
},
providers: [
CredentialsProvider({
name: "Sign in",
credentials: {
username: { label: "Username", type: "text", placeholder: "yourself" },
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
const response = await fetch(
process.env.NEXT_PUBLIC_AUTH_URL + "/auth/login",
{
method: "POST",
headers: {
"Content-Type": "application/json",
session: {
strategy: "jwt",
},
pages: {
signIn: "/auth/log-in",
signOut: "/",
},
providers: [
CredentialsProvider({
name: "Sign in",
credentials: {
username: {
label: "Username",
type: "text",
placeholder: "yourself",
},
password: { label: "Password", type: "password" },
},
body: JSON.stringify({
username: credentials?.username,
password: credentials?.password,
}),
},
);
async authorize(credentials, req) {
const response = await fetch(
process.env.NEXT_PUBLIC_AUTH_URL + "/auth/login",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: credentials?.username,
password: credentials?.password,
}),
},
);
type LoginResponse = {
access_token: string;
};
type LoginResponse = {
access_token: string;
};
if (response.status < 200 || response.status > 399) return null;
if (response.status < 200 || response.status > 399) return null;
const response_body = (await response.json()) as LoginResponse;
const response_body = (await response.json()) as LoginResponse;
type TokenPayload = {
sub: string;
username: string;
roles: Role[];
picture: string;
iat: number;
exp: number;
};
type TokenPayload = {
sub: string;
username: string;
roles: Role[];
picture: string;
iat: number;
exp: number;
};
const token_payload = JSON.parse(
atob(response_body.access_token.split(".")[1]),
) as TokenPayload;
const token_payload = JSON.parse(
atob(response_body.access_token.split(".")[1]),
) as TokenPayload;
const user: User = {
id: token_payload.username,
roles: token_payload.roles,
image: token_payload.picture,
name: token_payload.username,
apiSession: {
exp: token_payload.exp,
accessToken: response_body.access_token,
},
};
const user: User = {
id: token_payload.username,
roles: token_payload.roles,
image: token_payload.picture,
name: token_payload.username,
apiSession: {
exp: token_payload.exp,
accessToken: response_body.access_token,
},
};
return user;
},
}),
],
callbacks: {
async jwt({ token, user }) {
// Persist the OAuth access_token to the token right after signin
if (user) {
const { apiSession, ...cleanedUser } = user;
token.user = cleanedUser;
token.apiSession = apiSession;
}
return token;
return user;
},
}),
],
callbacks: {
async jwt({ token, user }) {
// Persist the OAuth access_token to the token right after signin
if (user) {
const { apiSession, ...cleanedUser } = user;
token.user = cleanedUser;
token.apiSession = apiSession;
}
return token;
},
async session({ session, token }) {
// Send properties to the client, like an access_token from a provider.
if (token?.user && session) {
session.apiSession = token.apiSession;
session.user = token.user;
}
return session;
},
},
async session({ session, token }) {
// Send properties to the client, like an access_token from a provider.
if (token?.user && session) {
session.apiSession = token.apiSession;
session.user = token.user;
}
return session;
},
},
};

View File

@@ -4,5 +4,5 @@ import { SessionProvider } from "next-auth/react";
import { PropsWithChildren } from "react";
export const AuthProvider: React.FC<PropsWithChildren> = ({ children }) => {
return <SessionProvider>{children}</SessionProvider>;
return <SessionProvider>{children}</SessionProvider>;
};

View File

@@ -2,29 +2,29 @@ import NextAuth, { DefaultSession } from "next-auth";
import { JWT, DefaultJWT } from "next-auth/jwt";
declare module "next-auth" {
type Role = "user" | "manager" | "admin";
interface ApiSession {
exp?: number;
accessToken: string;
refreshToken?: string;
}
interface User {
id: string;
roles: Role[];
image?: string;
name?: string;
apiSession?: ApiSession;
}
type Role = "user" | "manager" | "admin";
interface ApiSession {
exp?: number;
accessToken: string;
refreshToken?: string;
}
interface User {
id: string;
roles: Role[];
image?: string;
name?: string;
apiSession?: ApiSession;
}
interface Session extends DefaultSession {
user?: Omit<User, "apiSession">;
apiSession?: ApiSession;
}
interface Session extends DefaultSession {
user?: Omit<User, "apiSession">;
apiSession?: ApiSession;
}
}
declare module "next-auth/jwt" {
interface JWT {
user?: Omit<User, "apiSession">;
apiSession?: ApiSession;
}
interface JWT {
user?: Omit<User, "apiSession">;
apiSession?: ApiSession;
}
}

View File

@@ -1,7 +1,7 @@
.container {
min-height: 100vh;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -5,11 +5,11 @@ import styles from "./log-in.module.scss";
type LogInWidgetsProps = {};
export const LogInView: React.FC<LogInWidgetsProps> = ({}) => {
return (
<Suspense>
<div className={styles.container}>
<LogInWidget />
</div>
</Suspense>
);
return (
<Suspense>
<div className={styles.container}>
<LogInWidget />
</div>
</Suspense>
);
};

View File

@@ -7,13 +7,13 @@ import { PropsWithChildren } from "react";
* Receives the user component as a children in order to fetch it from the server and allow quick session information fetch
* */
export const NavbarHeader = (props: PropsWithChildren) => {
return (
<header className={styles.header}>
<Link href={"/"} className={styles.logo}>
<img src="/main-logo.svg" alt="Main logo" />
<h1>Your brand</h1>
</Link>
<nav>{props.children}</nav>
</header>
);
return (
<header className={styles.header}>
<Link href={"/"} className={styles.logo}>
<img src="/main-logo.svg" alt="Main logo" />
<h1>Your brand</h1>
</Link>
<nav>{props.children}</nav>
</header>
);
};

View File

@@ -1,46 +1,46 @@
.header {
position: sticky;
top: 1.2rem;
left: 0;
position: sticky;
top: 1.2rem;
left: 0;
display: flex;
justify-content: space-between;
margin: 1.2rem;
box-sizing: border-box;
border-radius: 8px;
padding: 0.6rem 2rem;
background-color: var(--color-white);
box-shadow: 0 1px 3px rgba(var(--color-grey-10-rgb), 0.3);
color: var(--color-background);
--header-item-height: 4.5rem;
.logo {
display: flex;
flex-direction: row;
gap: 0.6rem;
align-items: center;
justify-content: space-between;
font-size: 2.6rem;
font-weight: bold;
margin: 1.2rem;
box-sizing: border-box;
border-radius: 8px;
height: var(--header-item-height);
width: auto;
padding: 0.6rem 2rem;
background-color: var(--color-white);
box-shadow: 0 1px 3px rgba(var(--color-grey-10-rgb), 0.3);
color: var(--color-background);
& > img {
height: 100%;
width: 100%;
object-fit: contain;
--header-item-height: 4.5rem;
.logo {
display: flex;
flex-direction: row;
gap: 0.6rem;
align-items: center;
font-size: 2.6rem;
font-weight: bold;
height: var(--header-item-height);
width: auto;
& > img {
height: 100%;
width: 100%;
object-fit: contain;
}
& > h1 {
font-size: 1.4rem;
}
}
& > h1 {
font-size: 1.4rem;
& > nav {
display: flex;
}
}
& > nav {
display: flex;
}
}

View File

@@ -2,19 +2,19 @@ import React from "react";
// TODO: make it consistent and link current data-theme with checked state fo the checkbox, maybe with a react state
export const ThemeSwitcher = () => {
const changeHandler = () => {
const isDarkTheme =
document.documentElement.getAttribute("data-theme") === "dark";
document.documentElement.setAttribute(
"data-theme",
isDarkTheme ? "light" : "dark",
);
};
const changeHandler = () => {
const isDarkTheme =
document.documentElement.getAttribute("data-theme") === "dark";
document.documentElement.setAttribute(
"data-theme",
isDarkTheme ? "light" : "dark",
);
};
return (
<label className="switch">
<input type="checkbox" onClick={changeHandler} />
<span className="slider round"></span>
</label>
);
return (
<label className="switch">
<input type="checkbox" onClick={changeHandler} />
<span className="slider round"></span>
</label>
);
};

View File

@@ -3,51 +3,51 @@ import { timedFetch } from "../../utils/timedFetch";
import { useSession } from "next-auth/react";
type QueryReturn<T> = {
data?: T;
status?: number;
isLoading: boolean;
isError: boolean;
data?: T;
status?: number;
isLoading: boolean;
isError: boolean;
};
type QueryInput = {
url: string;
options: RequestInit;
timeout?: number;
url: string;
options: RequestInit;
timeout?: number;
};
export function useQuery<DataType>({
url,
options,
timeout,
url,
options,
timeout,
}: QueryInput): QueryReturn<DataType> {
const [response, setResponse] = useState<{
data: DataType;
status: number;
}>();
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isError, setIsError] = useState<boolean>(false);
const [response, setResponse] = useState<{
data: DataType;
status: number;
}>();
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isError, setIsError] = useState<boolean>(false);
const session = useSession();
const token = session.data?.apiSession?.accessToken;
if (token) {
options.headers = { ...options.headers, Authorization: "Bearer " + token };
}
const session = useSession();
const token = session.data?.apiSession?.accessToken;
if (token) {
options.headers = { ...options.headers, Authorization: "Bearer " + token };
}
useEffect(() => {
setIsLoading(true);
setIsError(false);
(async () => {
if (session.status !== "loading") {
const _response = await timedFetch<DataType>(url, options, timeout);
if (_response) {
setResponse(_response);
} else {
setIsError(true);
}
setIsLoading(false);
}
})();
}, [url, options, timeout, session.status]);
useEffect(() => {
setIsLoading(true);
setIsError(false);
(async () => {
if (session.status !== "loading") {
const _response = await timedFetch<DataType>(url, options, timeout);
if (_response) {
setResponse(_response);
} else {
setIsError(true);
}
setIsLoading(false);
}
})();
}, [url, options, timeout, session.status]);
return { ...response, isLoading, isError };
return { ...response, isLoading, isError };
}

View File

@@ -5,12 +5,12 @@ import NavbarHeader from "../../components/navbar-header";
import UserDropdown from "@/modules/auth/components/user-dropdown";
export const HomeLayout: React.FC<PropsWithChildren> = ({ children }) => {
return (
<div className={styles.container}>
<NavbarHeader>
<UserDropdown />
</NavbarHeader>
{children}
</div>
);
return (
<div className={styles.container}>
<NavbarHeader>
<UserDropdown />
</NavbarHeader>
{children}
</div>
);
};

View File

@@ -1,6 +1,6 @@
.container {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: var(--color-background);
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: var(--color-background);
}

View File

@@ -3,21 +3,21 @@ import "@/styles/main.scss";
import { ApplicationProvider } from "../../providers/application.provider";
export const metadata: Metadata = {
title: "Full stack archetype",
description:
"This is a full stack archetype supported in NextJS, NestJS and mysql",
title: "Full stack archetype",
description:
"This is a full stack archetype supported in NextJS, NestJS and mysql",
};
export function RootLayout({
children,
children,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<ApplicationProvider>{children}</ApplicationProvider>
</body>
</html>
);
return (
<html lang="en">
<body>
<ApplicationProvider>{children}</ApplicationProvider>
</body>
</html>
);
}

View File

@@ -3,8 +3,6 @@
import { AuthProvider } from "@/modules/auth/providers/auth.provider";
import { PropsWithChildren } from "react";
export const ApplicationProvider: React.FC<PropsWithChildren> = ({
children,
}) => {
return <AuthProvider>{children}</AuthProvider>;
export const ApplicationProvider: React.FC<PropsWithChildren> = ({ children }) => {
return <AuthProvider>{children}</AuthProvider>;
};

View File

@@ -1,31 +1,31 @@
class HttpError extends Error {
constructor(public response: Response) {
super(`HTTP error ${response.status}`);
}
constructor(public response: Response) {
super(`HTTP error ${response.status}`);
}
}
async function timedFetch<ResponseType = any>(
url: string,
options: RequestInit = {},
timeout: number = 5000,
url: string,
options: RequestInit = {},
timeout: number = 5000,
) {
try {
const result = await fetch(url, {
signal: AbortSignal.timeout(timeout),
...options,
});
if (!result.ok) {
throw new HttpError(result);
try {
const result = await fetch(url, {
signal: AbortSignal.timeout(timeout),
...options,
});
if (!result.ok) {
throw new HttpError(result);
}
return {
data: (await result.json()) as ResponseType,
status: result.status,
};
} catch (error) {
if ((error as Error).name === "AbortError") {
console.log(`Fetch aborted by timeout (${timeout}ms)`);
}
}
return {
data: (await result.json()) as ResponseType,
status: result.status,
};
} catch (error) {
if ((error as Error).name === "AbortError") {
console.log(`Fetch aborted by timeout (${timeout}ms)`);
}
}
}
export { timedFetch };

View File

@@ -3,44 +3,44 @@ import styles from "./example-detail.module.scss";
import Image from "next/image";
type ExampleDetailProps = {
exampleId: number;
exampleId: number;
};
type ExampleDetailType = {
id: number;
name: string;
description: string;
image: string;
created_at: string;
id: number;
name: string;
description: string;
image: string;
created_at: string;
};
export const ExampleDetail: React.FC<ExampleDetailProps> = ({ exampleId }) => {
const result = useQuery<ExampleDetailType>({
url: `${process.env.NEXT_PUBLIC_BACKEND_URL}/example/${exampleId}`,
options: { headers: {} },
timeout: 4000,
});
const result = useQuery<ExampleDetailType>({
url: `${process.env.NEXT_PUBLIC_BACKEND_URL}/example/${exampleId}`,
options: { headers: {} },
timeout: 4000,
});
if (result.isLoading) {
return <>Loading...</>;
}
if (result.isLoading) {
return <>Loading...</>;
}
if (result.isError) {
return <>Error</>;
}
if (result.isError) {
return <>Error</>;
}
return (
<div data-testid="example-detail" className={styles.container}>
<div className={styles["example__card"]}>
<Image
src={result.data?.image ?? ""}
alt="picture"
width={130}
height={130}
/>
<h3>{result.data?.name}</h3>
<p>{result.data?.description}</p>
</div>
</div>
);
return (
<div data-testid="example-detail" className={styles.container}>
<div className={styles["example__card"]}>
<Image
src={result.data?.image ?? ""}
alt="picture"
width={130}
height={130}
/>
<h3>{result.data?.name}</h3>
<p>{result.data?.description}</p>
</div>
</div>
);
};

View File

@@ -1,35 +1,35 @@
.container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
& .example__card {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 2rem;
max-width: 250px;
& .example__card {
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
padding: 2.5rem;
max-width: 250px;
border-radius: 4px;
padding: 2.5rem;
background-color: var(--color-grey-90);
box-shadow: 0px 2px 5px rgba(var(--color-black-rgb), 0.2);
border-radius: 4px;
& img {
border-radius: 50%;
box-shadow: 0px 2px 5px rgba(var(--color-black-rgb), 0.2);
background-color: var(--color-grey-90);
box-shadow: 0px 2px 5px rgba(var(--color-black-rgb), 0.2);
& img {
border-radius: 50%;
box-shadow: 0px 2px 5px rgba(var(--color-black-rgb), 0.2);
}
& h3 {
font-size: 2rem;
}
& p {
font-size: 1.6rem;
}
}
& h3 {
font-size: 2rem;
}
& p {
font-size: 1.6rem;
}
}
}

View File

@@ -1,3 +1,3 @@
"use client"
"use client";
export { ExampleDetail } from "./example-detail.component";

View File

@@ -6,52 +6,52 @@ import Link from "next/link";
type ExampleListProps = {};
type ExampleListType = {
results: {
id: number;
name: string;
description: string;
image: string;
created_at: string;
}[];
count: number;
results: {
id: number;
name: string;
description: string;
image: string;
created_at: string;
}[];
count: number;
};
export const ExampleList: React.FC<ExampleListProps> = ({}) => {
const result = useQuery<ExampleListType>({
url: process.env.NEXT_PUBLIC_BACKEND_URL + "/example",
options: { headers: {} },
timeout: 4000,
});
const result = useQuery<ExampleListType>({
url: process.env.NEXT_PUBLIC_BACKEND_URL + "/example",
options: { headers: {} },
timeout: 4000,
});
if (result.isLoading) {
return <>Loading...</>;
}
if (result.isLoading) {
return <>Loading...</>;
}
if (result.isError) {
return <>Error</>;
}
if (result.isError) {
return <>Error</>;
}
return (
<div data-testid="example-list" className={styles.container}>
<ul>
{result.data?.results?.map((example) => {
return (
<Link href={`/examples/${example.id}`} key={example.id}>
<li key={example.id}>
<Image
src={example.image}
alt="picture"
width={30}
height={30}
/>
<span>{example.name}</span>
<span>{example.description}</span>
</li>
</Link>
);
})}
</ul>
<div>This could be the pagination...</div>
</div>
);
return (
<div data-testid="example-list" className={styles.container}>
<ul>
{result.data?.results?.map((example) => {
return (
<Link href={`/examples/${example.id}`} key={example.id}>
<li key={example.id}>
<Image
src={example.image}
alt="picture"
width={30}
height={30}
/>
<span>{example.name}</span>
<span>{example.description}</span>
</li>
</Link>
);
})}
</ul>
<div>This could be the pagination...</div>
</div>
);
};

View File

@@ -1,39 +1,39 @@
.container {
display: flex;
flex-direction: column;
flex: 1;
padding: 1.2rem;
padding-top: 0;
gap: 1rem;
& ul {
flex: 1;
list-style: none;
display: flex;
flex-direction: column;
gap: 10px;
padding: 0.8rem;
background-color: var(--color-grey-90);
border-radius: 0.8rem;
flex: 1;
padding: 1.2rem;
padding-top: 0;
gap: 1rem;
& a > li {
display: grid;
grid-auto-flow: column;
grid-template-columns: 1fr 1fr 1fr;
align-items: center;
padding: 0.3rem 1.4rem;
width: 100%;
background-color: var(--color-white);
border-radius: 0.8rem;
& ul {
flex: 1;
list-style: none;
display: flex;
flex-direction: column;
gap: 10px;
padding: 0.8rem;
background-color: var(--color-grey-90);
border-radius: 0.8rem;
&:hover {
background-color: var(--color-primary-light);
box-shadow: 0 2px 4px rgba(var(--color-black-rgb), 0.2);
}
& a > li {
display: grid;
grid-auto-flow: column;
grid-template-columns: 1fr 1fr 1fr;
align-items: center;
padding: 0.3rem 1.4rem;
width: 100%;
background-color: var(--color-white);
border-radius: 0.8rem;
& > img {
border-radius: 50%;
}
&:hover {
background-color: var(--color-primary-light);
box-shadow: 0 2px 4px rgba(var(--color-black-rgb), 0.2);
}
& > img {
border-radius: 50%;
}
}
}
}
}

View File

@@ -1,3 +1,3 @@
"use client"
"use client";
export { ExampleList } from "./example-list.component";

View File

@@ -1,4 +1,4 @@
.container {
display: flex;
flex: 1;
display: flex;
flex: 1;
}

View File

@@ -2,18 +2,16 @@ import { ExampleDetail } from "../../components/example-detail";
import styles from "./example-detail.module.scss";
type ExampleDetailViewProps = {
// query parameters
searchParams: { [key: string]: string | string[] | undefined };
// url parameters
params: { exampleId: string };
// query parameters
searchParams: { [key: string]: string | string[] | undefined };
// url parameters
params: { exampleId: string };
};
export const ExampleDetailView: React.FC<ExampleDetailViewProps> = ({
params,
}) => {
return (
<div data-testid="example-detail-view" className={styles.container}>
<ExampleDetail exampleId={parseInt(params.exampleId)} />
</div>
);
export const ExampleDetailView: React.FC<ExampleDetailViewProps> = ({ params }) => {
return (
<div data-testid="example-detail-view" className={styles.container}>
<ExampleDetail exampleId={parseInt(params.exampleId)} />
</div>
);
};

View File

@@ -1,5 +1,5 @@
.container {
display: flex;
flex-direction: column;
flex: 1;
display: flex;
flex-direction: column;
flex: 1;
}

View File

@@ -4,9 +4,9 @@ import { ExampleList } from "../../components/example-list";
type ExampleListProps = {};
export const ExampleListView: React.FC<ExampleListProps> = ({}) => {
return (
<div data-testid="{example-list}" className={styles.container}>
<ExampleList />
</div>
);
return (
<div data-testid="{example-list}" className={styles.container}>
<ExampleList />
</div>
);
};

View File

@@ -2,103 +2,103 @@
// For variable definition
:root {
--color-white: #fafafa;
--color-black: #101010;
--color-white: #fafafa;
--color-black: #101010;
--color-grey-10: #171717;
--color-grey-30: #242424;
--color-grey-60: #363636;
--color-grey-70: #bbbbbb;
--color-grey-90: #e9ecef;
--color-grey-10: #171717;
--color-grey-30: #242424;
--color-grey-60: #363636;
--color-grey-70: #bbbbbb;
--color-grey-90: #e9ecef;
--color-primary: #274c77;
--color-primary-light: #a3cef1;
--color-primary-dark: #052f5f;
--color-primary: #274c77;
--color-primary-light: #a3cef1;
--color-primary-dark: #052f5f;
--color-error: #e63946;
--color-error: #e63946;
// -------- RGB variants -------- //
--color-white-rgb: 250, 250, 250;
--color-black-rgb: 16, 16, 16;
// -------- RGB variants -------- //
--color-white-rgb: 250, 250, 250;
--color-black-rgb: 16, 16, 16;
--color-grey-10-rgb: 23, 23, 23;
--color-grey-30-rgb: 36, 36, 36;
--color-grey-60-rgb: 54, 54, 54;
--color-grey-70-rgb: 187, 187, 187;
--color-grey-90-rgb: 233, 236, 239;
--color-grey-10-rgb: 23, 23, 23;
--color-grey-30-rgb: 36, 36, 36;
--color-grey-60-rgb: 54, 54, 54;
--color-grey-70-rgb: 187, 187, 187;
--color-grey-90-rgb: 233, 236, 239;
--color-primary-rgb: 39, 76, 119;
--color-primary-light-rgb: 163, 206, 241;
--color-primary-dark-rgb: 5, 47, 95;
--color-primary-rgb: 39, 76, 119;
--color-primary-light-rgb: 163, 206, 241;
--color-primary-dark-rgb: 5, 47, 95;
}
[data-theme="dark"] {
--color-white: #101010;
--color-black: #fafafa;
--color-white: #101010;
--color-black: #fafafa;
--color-grey-10: #e9ecef;
--color-grey-30: #bbbbbb;
--color-grey-70: #242424;
--color-grey-60: #363636;
--color-grey-90: #171717;
--color-grey-10: #e9ecef;
--color-grey-30: #bbbbbb;
--color-grey-70: #242424;
--color-grey-60: #363636;
--color-grey-90: #171717;
--color-primary: #274c77;
--color-primary-light: #052f5f;
--color-primary-dark: #a3cef1;
--color-primary: #274c77;
--color-primary-light: #052f5f;
--color-primary-dark: #a3cef1;
// -------- RGB variants -------- //
--color-white-rgb: 16, 16, 16;
--color-black-rgb: 250, 250, 250;
// -------- RGB variants -------- //
--color-white-rgb: 16, 16, 16;
--color-black-rgb: 250, 250, 250;
--color-grey-10-rgb: 233, 236, 239;
--color-grey-30-rgb: 187, 187, 187;
--color-grey-60-rgb: 54, 54, 54;
--color-grey-70-rgb: 36, 36, 36;
--color-grey-90-rgb: 23, 23, 23;
--color-grey-10-rgb: 233, 236, 239;
--color-grey-30-rgb: 187, 187, 187;
--color-grey-60-rgb: 54, 54, 54;
--color-grey-70-rgb: 36, 36, 36;
--color-grey-90-rgb: 23, 23, 23;
--color-primary-rgb: 39, 76, 119;
--color-primary-light-rgb: 5, 47, 95;
--color-primary-dark-rgb: 163, 206, 241;
--color-primary-rgb: 39, 76, 119;
--color-primary-light-rgb: 5, 47, 95;
--color-primary-dark-rgb: 163, 206, 241;
}
*,
::after,
::before {
margin: 0;
padding: 0;
box-sizing: inherit;
margin: 0;
padding: 0;
box-sizing: inherit;
}
html {
font-family: "Roboto", sans-serif;
max-width: 100vw;
overflow-x: hidden;
font-family: "Roboto", sans-serif;
max-width: 100vw;
overflow-x: hidden;
font-size: 62.5%; // 1rem = 10px; 10px/16px = 62.5%
@media only screen and (min-width: 112.5em) {
font-size: 75%;
}
@media only screen and (max-width: 75em) {
font-size: 56.25%;
}
@media only screen and (max-width: 56.25em) {
font-size: 50%;
}
font-size: 62.5%; // 1rem = 10px; 10px/16px = 62.5%
@media only screen and (min-width: 112.5em) {
font-size: 75%;
}
@media only screen and (max-width: 75em) {
font-size: 56.25%;
}
@media only screen and (max-width: 56.25em) {
font-size: 50%;
}
}
body {
box-sizing: border-box;
box-sizing: border-box;
background-color: var(--color-white);
color: var(--color-black);
background-color: var(--color-white);
color: var(--color-black);
}
a {
color: inherit;
text-decoration: none;
color: inherit;
text-decoration: none;
}
::selection {
background-color: var(--color-primary);
color: var(--color-white);
background-color: var(--color-primary);
color: var(--color-white);
}

View File

@@ -1,33 +1,33 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
},
"typeRoots": ["./src/modules/auth/types"]
},
"typeRoots": ["./src/modules/auth/types"]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"src/modules/**/*.d.ts"
],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"src/modules/**/*.d.ts"
],
"exclude": ["node_modules"]
}