tr]:last:border-b-0",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
+ return (
+
+ )
+}
+
+function TableHead({ className, ...props }: React.ComponentProps<"th">) {
+ return (
+ [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCell({ className, ...props }: React.ComponentProps<"td">) {
+ return (
+ | [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCaption({
+ className,
+ ...props
+}: React.ComponentProps<"caption">) {
+ return (
+
+ )
+}
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+}
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
new file mode 100644
index 0000000..4ee26b3
--- /dev/null
+++ b/src/components/ui/tooltip.tsx
@@ -0,0 +1,61 @@
+"use client"
+
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+
+import { cn } from "@/lib/utils"
+
+function TooltipProvider({
+ delayDuration = 0,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function Tooltip({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function TooltipTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/src/config/app.json b/src/config/app.json
new file mode 100644
index 0000000..64ba493
--- /dev/null
+++ b/src/config/app.json
@@ -0,0 +1,8 @@
+{
+ "state": "go",
+ "api": {
+ "url": "http://localhost:3000/",
+ "prefix": "api/v1/",
+ "content_type": "application/json"
+ }
+}
\ No newline at end of file
diff --git a/src/hooks/use-mobile.ts b/src/hooks/use-mobile.ts
new file mode 100644
index 0000000..2b0fe1d
--- /dev/null
+++ b/src/hooks/use-mobile.ts
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+const MOBILE_BREAKPOINT = 768
+
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(undefined)
+
+ React.useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ }
+ mql.addEventListener("change", onChange)
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ return () => mql.removeEventListener("change", onChange)
+ }, [])
+
+ return !!isMobile
+}
diff --git a/src/interfaces/IConfig.ts b/src/interfaces/IConfig.ts
new file mode 100644
index 0000000..8c046e4
--- /dev/null
+++ b/src/interfaces/IConfig.ts
@@ -0,0 +1,8 @@
+export default interface IConfig {
+ state: string,
+ api: {
+ url: string,
+ prefix: string,
+ content_type: string
+ },
+}
\ No newline at end of file
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 0000000..bd0c391
--- /dev/null
+++ b/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/src/middleware.ts b/src/middleware.ts
new file mode 100644
index 0000000..ece7ded
--- /dev/null
+++ b/src/middleware.ts
@@ -0,0 +1,63 @@
+import jwt from "jsonwebtoken";
+import {
+ NextResponse,
+ type MiddlewareConfig,
+ type NextRequest
+} from "next/server";
+
+const publicRoutes = [
+ { path: "/login", whenAuthenticated: "redirect" },
+ { path: "/register", whenAuthenticated: "redirect" },
+ { path: "/", whenAuthenticated: "next" },
+] as const;
+
+const ROOT_PATH = "/login"; // url raiz
+
+export function middleware(request: NextRequest) {
+ const path = request.nextUrl.pathname;
+ const publicRoute = publicRoutes.find((route) => route.path === path);
+ const authToken = request.cookies.get("access_token");
+
+ // 1. Não autenticado e rota pública → segue normal
+ if (!authToken && publicRoute) {
+ return NextResponse.next();
+ }
+
+ // 2. Não autenticado e rota privada → redireciona para raiz
+ if (!authToken && !publicRoute) {
+ return NextResponse.redirect(new URL(ROOT_PATH, request.url));
+ }
+
+ // 3. Autenticado em rota pública com flag "redirect" → vai para raiz
+ if (authToken && publicRoute && publicRoute.whenAuthenticated === "redirect") {
+ return NextResponse.redirect(new URL(ROOT_PATH, request.url));
+ }
+
+ // 4. Autenticado em rota privada → valida token
+ if (authToken && !publicRoute) {
+ const decoded: any = jwt.decode(authToken.value);
+
+ if (!decoded || !decoded.exp) {
+ console.log("Token inválido");
+ return NextResponse.redirect(new URL(ROOT_PATH, request.url));
+ }
+
+ const currentTime = Math.floor(Date.now() / 1000);
+
+ if (decoded.exp <= currentTime) {
+ console.log("Token expirado");
+ return NextResponse.redirect(new URL(ROOT_PATH, request.url));
+ }
+
+ return NextResponse.next();
+ }
+
+ return NextResponse.next();
+}
+
+export const config: MiddlewareConfig = {
+ matcher: [
+ // ignora APIs, arquivos estáticos e metadados
+ "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|images).*)",
+ ],
+};
diff --git a/src/services/api/Api.ts b/src/services/api/Api.ts
new file mode 100644
index 0000000..f409baf
--- /dev/null
+++ b/src/services/api/Api.ts
@@ -0,0 +1,76 @@
+import Json from '@/actions/json/Json';
+import ApiInterface from './interfaces/ApiInterface';
+import Response from '@/services/response/Response';
+
+import TokenGet from '@/actions/token/TokenGet';
+import ApiSchema from '@/services/api/schemas/ApiSchema';
+import IConfig from '@/interfaces/IConfig';
+
+export default class API {
+
+ private ApiSchema: ApiSchema;
+ private config: IConfig;
+
+ constructor() {
+
+ // Classe validadora das informações
+ this.ApiSchema = new ApiSchema();
+
+ // Obtem as configurações da aplicação
+ this.config = Json.execute('config/app.json');
+
+ }
+
+ public async send(data: ApiInterface) {
+
+ // Define os dados para envio
+ const _data = data;
+
+ try {
+
+ // Verifica se todos os dados estão corretos
+ this.ApiSchema.url = this.config.api.url;
+ this.ApiSchema.prefix = this.config.api.prefix;
+ this.ApiSchema.endpoint = _data.endpoint;
+ this.ApiSchema.contentType = this.config.api.content_type;
+ this.ApiSchema.token = await TokenGet();
+
+ // Verifica se existem erros
+ if (this.ApiSchema.errors.length > 0) {
+
+ throw new Error(`Erros no schema: ${this.ApiSchema.errors.join(", ")}`);
+
+ }
+
+ // Verifica se existe body para envio
+ const filteredBody = _data.body ? Object.fromEntries(Object.entries(_data.body).filter(([_, v]) => v != null && v !== "")) : null;
+
+ // Realiza a requisição
+ const response = await fetch(`${this.ApiSchema.url}${this.ApiSchema.prefix}/${this.ApiSchema.endpoint}`, {
+ method: _data.method,
+ headers: {
+ "Accept": `${this.ApiSchema.contentType}`,
+ "Content-Type": `${this.ApiSchema.contentType}`,
+ "Authorization": `Bearer ${this.ApiSchema.token}`
+ },
+ ...(filteredBody && { body: JSON.stringify(filteredBody) }),
+ });
+
+ // Converte a reposta para json
+ const responseData = await response.json();
+
+ // Classe para manipular a resposta
+ const ResponseHandler = new Response();
+
+ // Manipula as respostas
+ return ResponseHandler.handler(responseData);
+
+ } catch (error) {
+
+ console.error("Erro ao enviar requisição:", error);
+ throw error;
+
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/services/api/enums/ApiMethodEnum.ts b/src/services/api/enums/ApiMethodEnum.ts
new file mode 100644
index 0000000..cba272c
--- /dev/null
+++ b/src/services/api/enums/ApiMethodEnum.ts
@@ -0,0 +1,7 @@
+export enum Methods {
+ GET = 'GET',
+ POST = 'POST',
+ DELETE = 'DELETE',
+ PUT = 'PUT',
+ PATCH = 'PATCH'
+}
\ No newline at end of file
diff --git a/src/services/api/interfaces/ApiInterface.ts b/src/services/api/interfaces/ApiInterface.ts
new file mode 100644
index 0000000..2f76720
--- /dev/null
+++ b/src/services/api/interfaces/ApiInterface.ts
@@ -0,0 +1,7 @@
+import { Methods } from "../enums/ApiMethodEnum";
+
+export default interface ApiInterface {
+ method: Methods,
+ endpoint: string,
+ body?: object,
+}
\ No newline at end of file
diff --git a/src/services/api/schemas/ApiSchema.ts b/src/services/api/schemas/ApiSchema.ts
new file mode 100644
index 0000000..a5ea80c
--- /dev/null
+++ b/src/services/api/schemas/ApiSchema.ts
@@ -0,0 +1,75 @@
+import empty from "@/actions/validations/empty";
+import Schema from "@/abstracts/Schema";
+
+export default class ApiSchema extends Schema {
+
+ private _url?: string;
+ private _prefix?: string;
+ private _endpoint?: string;
+ private _contentType?: string;
+ private _token?: string;
+
+ get url(): string | undefined {
+ return this._url;
+ }
+
+ set url(value: string | undefined) {
+ this._url = value;
+
+ // Verifica se esta preenchido
+ if (empty(this._url)) {
+ this.errors = 'A informação "URL" deve esta preenchido';
+ }
+
+ }
+
+ get prefix(): string | undefined {
+ return this._prefix;
+ }
+
+ set prefix(value: string | undefined) {
+ this._prefix = value;
+
+ // Verifica se esta preenchido
+ if (empty(this._prefix)) {
+ this.errors = 'A informação "Prefixo" deve esta preenchido';
+ }
+
+ }
+
+ get endpoint(): string | undefined {
+ return this._endpoint;
+ }
+
+ set endpoint(value: string | undefined) {
+ this._endpoint = value;
+
+ // Verifica se esta preenchido
+ if (empty(this._endpoint)) {
+ this.errors = 'A informação "Endpoint" deve esta preenchido';
+ }
+
+ }
+
+ get contentType(): string | undefined {
+ return this._contentType;
+ }
+
+ set contentType(value: string | undefined) {
+ this._contentType = value;
+
+ // Verifica se esta preenchido
+ if (empty(this._contentType)) {
+ this.errors = 'A informação "Tipo de Conteúdo" deve esta preenchido';
+ }
+
+ }
+
+ get token(): string | undefined {
+ return this._token;
+ }
+
+ set token(value: string | undefined) {
+ this._token = value;
+ }
+}
diff --git a/src/services/response/Response.ts b/src/services/response/Response.ts
new file mode 100644
index 0000000..55550d1
--- /dev/null
+++ b/src/services/response/Response.ts
@@ -0,0 +1,22 @@
+export default class Response {
+
+ public handler(data: any) {
+
+ switch (Number(data.status)) {
+
+ case 400 | 404:
+ this.status400(data);
+
+ }
+
+ return data;
+
+ }
+
+ private status400(data: any) {
+
+ console.log(data.error);
+
+ }
+
+}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..c133409
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "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/*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
|