new version
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
VITE_API_URL=https://venus.acatlan.unam.mx/impresiones_test
|
||||
VITE_PORT=3001
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,13 @@
|
||||
# React + Vite
|
||||
|
||||
Este proyecto es una aplicación web construida con **React + Vite** y está pensada para ser desplegada en **producción usando Nginx** como servidor web.
|
||||
|
||||
construimos el build
|
||||
|
||||
**npm run build**
|
||||
|
||||
este genera una carpeta: dist/
|
||||
|
||||
colocamos el dist/ en el servidor y configuramos nuestro Nginx , para https se recomienda Let’s Encrypt + Nginx
|
||||
|
||||
y reiniciamos Nginx
|
||||
@@ -0,0 +1,2 @@
|
||||
VITE_API_URL
|
||||
VITE_PORT
|
||||
@@ -0,0 +1,29 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/printer.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#0a3d62" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Sistema académico de bitacora de impresiones desarrollado para Facultad de Estudios Superiores Acatlán, UNAM."
|
||||
/>
|
||||
<meta name="author" content="Lino" />
|
||||
<meta name="keywords" content="bitácora, UNAM, FES Acatlán, impresiones" />
|
||||
<title>Impresiones</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+3255
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "front-impresiones-vite",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"vite": "^7.2.4"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="-2.4 -2.4 28.80 28.80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0">
|
||||
|
||||
<rect x="-2.4" y="-2.4" width="28.80" height="28.80" rx="0" fill="#ffffff" strokewidth="0"/>
|
||||
|
||||
</g>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 519 KiB |
@@ -0,0 +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>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
+185
@@ -0,0 +1,185 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
width: 100vw;
|
||||
height: 100dvh;
|
||||
background-color: #f5f5f7;
|
||||
}
|
||||
|
||||
.App {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
scrollbar-width: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
max-width: 470px;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #cfcfcf;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.messageBox {
|
||||
position: relative;
|
||||
padding: 0.5rem 1.25rem;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
opacity: 1;
|
||||
z-index: 100;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.messageBox.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 1s ease-out;
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: #d1f7c4;
|
||||
color: #000000;
|
||||
border: 1px solid #a5d6a7;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: #ffcdd2;
|
||||
color: #000000;
|
||||
border: 1px solid #ef9a9a;
|
||||
}
|
||||
|
||||
.containerHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.containerButton {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button {
|
||||
min-height: 34px;
|
||||
padding: 0.5rem 1.25rem;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
color: #ffffff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.button-search {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.button-search:hover {
|
||||
background-color: #1d4ed8;
|
||||
}
|
||||
|
||||
.button-charge {
|
||||
background-color: #16a34a;
|
||||
}
|
||||
|
||||
.button-charge:hover {
|
||||
background-color: #15803d;
|
||||
}
|
||||
|
||||
.button-logout {
|
||||
background-color: #d32f2f;
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.button-logout:hover {
|
||||
background-color: #d12020;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.messageBox {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: 10px;
|
||||
margin-top: 10px;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.containerHeader {
|
||||
max-width: 91%;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.button {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 0 0 0;
|
||||
}
|
||||
|
||||
.button-logout {
|
||||
color: transparent;
|
||||
padding: 1.2rem;
|
||||
font-size: 0;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.button-logout::after {
|
||||
content: "";
|
||||
border: 3px solid white;
|
||||
border-radius: 2px;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.button-logout::before {
|
||||
content: "";
|
||||
border: 2px solid white;
|
||||
border-radius: 1px;
|
||||
height: 0px;
|
||||
width: 10px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
input {
|
||||
min-width: 0px;
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import "./App.css";
|
||||
import { Impressions } from "./components/Impressions";
|
||||
import { Header } from "./components/Header";
|
||||
import { Login } from "./components/Login";
|
||||
import { PrivateRoute } from "./components/routes/PrivateRoute";
|
||||
import { LoginRedirect } from "./components/routes/LoginRedirect";
|
||||
import NotFound from "./components/NotFound";
|
||||
|
||||
function App() {
|
||||
const session = localStorage.getItem("session");
|
||||
return (
|
||||
<section className="body">
|
||||
<Header />
|
||||
<div className="App">
|
||||
<div className="impressions-container">
|
||||
<header>
|
||||
<div className="containerHeader lineSeparator">
|
||||
<h3 className="impressions-title">IMPRESIONES</h3>
|
||||
<h3 className="impressions-title ">{session}</h3>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Impressions />
|
||||
</PrivateRoute>
|
||||
}
|
||||
></Route>
|
||||
|
||||
<Route
|
||||
path="/Login"
|
||||
element={
|
||||
<LoginRedirect>
|
||||
<Login />
|
||||
</LoginRedirect>
|
||||
}
|
||||
></Route>
|
||||
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<div className="img"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
//IO
|
||||
@@ -0,0 +1,8 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import header from "./header.module.css";
|
||||
|
||||
export const Header = () => {
|
||||
return (
|
||||
<header className={header.header}>
|
||||
<a
|
||||
href="https://www.unam.mx/"
|
||||
target="_blank"
|
||||
className={header.center}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
className={header.logo}
|
||||
src="/logo-blanco.png"
|
||||
alt="Logo FES"
|
||||
width={250}
|
||||
height={70}
|
||||
/>
|
||||
</a>
|
||||
<div className={header.yellowPart}></div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
//IO
|
||||
@@ -0,0 +1,242 @@
|
||||
.formContainer{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.impressions-container {
|
||||
position: relative;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
width: 90%;
|
||||
max-width: 1100px;
|
||||
min-height: 520px;
|
||||
background-color: white;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
.impressions-title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: bold;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.lineSeparator {
|
||||
border-bottom: 1px solid #cfcfcf;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.containerInformation {
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.inputPages {
|
||||
max-width: 50px;
|
||||
}
|
||||
|
||||
.selection {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin-top: 1rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.selectionButton {
|
||||
background-color: #e5e7eb;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.selectionButton:hover {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.toggle-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
background-color: #f3f4f6;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
flex: 1;
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s, color 0.3s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.toggle-button:hover {
|
||||
background-color: #e5e7eb;
|
||||
}
|
||||
|
||||
.toggle-button.active {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.information {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
background-color: #f3f4f6;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.impressions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 400px;
|
||||
height: 615px;
|
||||
background-image: url("/rock.jpg");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.img::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgb(255, 255, 255),
|
||||
rgba(128, 128, 128, 0)
|
||||
);
|
||||
}
|
||||
|
||||
.labelSession {
|
||||
position: absolute;
|
||||
font-weight: bold;
|
||||
font-size: 1.75rem;
|
||||
top: 15px;
|
||||
right: 25px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #ffffff;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s ease-in-out infinite;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.impressions-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
max-width: 85%;
|
||||
max-height: 600px;
|
||||
}
|
||||
|
||||
.impressions-title {
|
||||
font-size: 1.45rem;
|
||||
}
|
||||
|
||||
.lineSeparator {
|
||||
text-align: center;
|
||||
padding-bottom: 0;
|
||||
border-bottom: 1px solid #717171;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.labelSession {
|
||||
font-size: 1rem;
|
||||
top: 20px;
|
||||
right: 60px;
|
||||
}
|
||||
|
||||
.img {
|
||||
right: 50%;
|
||||
transform: translateX(50%);
|
||||
width: 600px;
|
||||
opacity: 1;
|
||||
height: 650px;
|
||||
}
|
||||
|
||||
.img::before,
|
||||
.img::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.img::before {
|
||||
left: 0;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgb(255, 255, 255),
|
||||
rgba(128, 128, 128, 0)
|
||||
);
|
||||
}
|
||||
|
||||
.img::after {
|
||||
right: 0;
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
rgb(255, 255, 255),
|
||||
rgba(128, 128, 128, 0)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 440px) {
|
||||
.selection {
|
||||
width: max-content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
import axios from "axios";
|
||||
import { useEffect, useState } from "react";
|
||||
import { envConfig } from "../envConfigurations/EnvConfigurations";
|
||||
import "./login.css";
|
||||
|
||||
export const Impressions = () => {
|
||||
const [numAccount, setNumAccount] = useState("");
|
||||
const [pages, setPages] = useState("");
|
||||
const [view, setView] = useState("impresiones");
|
||||
const [folio, setFolio] = useState("");
|
||||
const [amount, setAmount] = useState("");
|
||||
const [date, setDate] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [alert, setAlert] = useState("");
|
||||
const [errorStudent, setErrorStudent] = useState("");
|
||||
const [showErrorStudent, setShowErrorStudent] = useState(false);
|
||||
const [showError, setShowError] = useState(false);
|
||||
const [showAlert, setShowAlert] = useState(false);
|
||||
const [studentData, setStudentData] = useState(null);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingstudent, setLoadingStudent] = useState(false);
|
||||
|
||||
//restrict this month//
|
||||
const day = new Date();
|
||||
const year = day.getFullYear();
|
||||
const month = day.getMonth();
|
||||
const today = day.getDate();
|
||||
|
||||
const minFecha = new Date(year, month, 1).toISOString().split("T")[0];
|
||||
const maxFecha = new Date(year, month, today).toISOString().split("T")[0];
|
||||
//restrict this month//
|
||||
|
||||
const url = envConfig().apiUrl;
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
//fadeOut alert and error
|
||||
useEffect(() => {
|
||||
if (alert) {
|
||||
setShowAlert(true);
|
||||
const timeout = setTimeout(() => {
|
||||
setShowAlert(false);
|
||||
setTimeout(() => setAlert(""), 500);
|
||||
}, 6000);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [alert]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
setShowError(true);
|
||||
const timeout = setTimeout(() => {
|
||||
setShowError(false);
|
||||
setTimeout(() => setError(""), 500);
|
||||
}, 6000);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
useEffect(() => {
|
||||
if (errorStudent) {
|
||||
setShowErrorStudent(true);
|
||||
const timeout = setTimeout(() => {
|
||||
setShowErrorStudent(false);
|
||||
setTimeout(() => setErrorStudent(""), 500);
|
||||
}, 6000);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [errorStudent]);
|
||||
//fadeOut alert and error//
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("session");
|
||||
window.location.href = "/Login";
|
||||
};
|
||||
|
||||
const handleSearch = async () => {
|
||||
setErrorStudent("");
|
||||
setLoadingStudent(true);
|
||||
if (!numAccount) {
|
||||
setErrorStudent("Ingresa un numero de cuenta");
|
||||
setLoadingStudent(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await axios.get(`${url}/student/${numAccount}`);
|
||||
|
||||
setStudentData(response.data);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error.response?.data?.error || "No se encontró el estudiante";
|
||||
|
||||
if (errorMessage === "Token inválido") {
|
||||
handleLogout();
|
||||
}
|
||||
|
||||
setErrorStudent(errorMessage);
|
||||
setStudentData(null);
|
||||
} finally {
|
||||
setLoadingStudent(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePayment = async () => {
|
||||
setAlert("");
|
||||
setError("");
|
||||
setLoading(true);
|
||||
if (!studentData) {
|
||||
setError("Error busca denuevo al estudiante");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!pages) {
|
||||
setError("Ingresa el numero de hojas a imprimir");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await axios.post(
|
||||
`${url}/impressions`,
|
||||
{
|
||||
numAccount: numAccount,
|
||||
pages: parseInt(pages),
|
||||
cost: parseInt(pages),
|
||||
},
|
||||
{ headers }
|
||||
);
|
||||
handleSearch();
|
||||
setAlert("Cobro realizado correctamente");
|
||||
setPages("");
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error.response?.data?.error || "No se encontró el estudiante";
|
||||
|
||||
if (errorMessage === "Token inválido") {
|
||||
handleLogout();
|
||||
}
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveReceipt = async () => {
|
||||
setAlert("");
|
||||
setError("");
|
||||
setLoading(true);
|
||||
if (!studentData) {
|
||||
setError("Primero busca un estudiante");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!folio) {
|
||||
setError("Ingresa el numero de folio");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!amount) {
|
||||
setError("Ingresa el monto");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!date) {
|
||||
setError("Ingresa la fecha del ticket");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await axios.post(
|
||||
`${url}/receipt`,
|
||||
{
|
||||
fol: folio,
|
||||
amount: parseFloat(amount),
|
||||
date: date,
|
||||
numAccount: numAccount,
|
||||
},
|
||||
{ headers }
|
||||
);
|
||||
handleSearch();
|
||||
setAlert("Recibo guardado correctamente");
|
||||
setFolio("");
|
||||
setAmount("");
|
||||
setDate("");
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error.response?.data?.error || "No se encontró el estudiante";
|
||||
|
||||
if (errorMessage === "Token inválido") {
|
||||
handleLogout();
|
||||
}
|
||||
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="formContainer">
|
||||
<div className="containerInformation">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSearch();
|
||||
}}
|
||||
>
|
||||
<label className="input-label">No.Cuenta</label>
|
||||
<div className="input-group">
|
||||
<input
|
||||
type="text"
|
||||
value={numAccount}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (/^\d*$/.test(value) && value.length <= 9) {
|
||||
setNumAccount(value);
|
||||
}
|
||||
}}
|
||||
className="input-field"
|
||||
placeholder="Coloca un número de cuenta..."
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
/>
|
||||
<button className="button button-search" type="submit" disabled={loadingstudent}>
|
||||
{loadingstudent ? <div className="spinner"></div> : "Buscar"}
|
||||
</button>
|
||||
</div>
|
||||
{errorStudent && (
|
||||
<div
|
||||
className={`messageBox error ${
|
||||
!showErrorStudent ? "hidden" : ""
|
||||
}`}
|
||||
style={{ marginTop: "10px" }}
|
||||
>
|
||||
{errorStudent}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
|
||||
{studentData && (
|
||||
<div className="information">
|
||||
<label>
|
||||
<b>No.Cuenta: </b>
|
||||
{studentData[0]?.id_cuenta}
|
||||
</label>
|
||||
<label>
|
||||
<b>Nombre: </b>
|
||||
{studentData[0]?.nombre}
|
||||
</label>
|
||||
<label>
|
||||
<b>Carrera: </b>
|
||||
{studentData[0]?.carrera}
|
||||
</label>
|
||||
<label>
|
||||
<b>Crédito: </b>
|
||||
{studentData[0]?.credito}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{studentData && (
|
||||
<div className="selection">
|
||||
<div className="toggle-group">
|
||||
<button
|
||||
className={`toggle-button ${
|
||||
view === "impresiones" ? "active" : ""
|
||||
}`}
|
||||
onClick={() => setView("impresiones")}
|
||||
>
|
||||
Impresiones
|
||||
</button>
|
||||
<button
|
||||
className={`toggle-button ${view === "recibo" ? "active" : ""}`}
|
||||
onClick={() => setView("recibo")}
|
||||
>
|
||||
Recibo
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{view === "impresiones" && (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handlePayment();
|
||||
}}
|
||||
>
|
||||
<div className="impressions">
|
||||
<div className="groupLabel">
|
||||
<label className="input-label">Costo: $1.00</label>
|
||||
</div>
|
||||
|
||||
<div className="input-group">
|
||||
<label className="input-label">Hojas:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={pages}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (/^\d*$/.test(value)) {
|
||||
setPages(value);
|
||||
}
|
||||
}}
|
||||
className="input-field "
|
||||
placeholder="Numero de hojas a imprimir..."
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="groupLabel">
|
||||
<label className="input-label">
|
||||
Total: {pages && `$${pages}.00`}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="containerButton">
|
||||
<button
|
||||
className="button button-charge"
|
||||
type="submit"
|
||||
disabled={loading || loadingstudent}
|
||||
>
|
||||
{loading ? <div className="spinner"></div> : "Cobrar"}
|
||||
</button>
|
||||
|
||||
{error && (
|
||||
<div
|
||||
className={`messageBox error ${
|
||||
!showError ? "hidden" : ""
|
||||
}`}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{alert && (
|
||||
<div
|
||||
className={`messageBox success ${
|
||||
!showAlert ? "hidden" : ""
|
||||
}`}
|
||||
>
|
||||
{alert}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{view === "recibo" && (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSaveReceipt();
|
||||
}}
|
||||
>
|
||||
<div className="impressions">
|
||||
<div className="input-group">
|
||||
<label className="input-label">Ticket:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={folio}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (/^\d*$/.test(value)) {
|
||||
setFolio(value);
|
||||
}
|
||||
}}
|
||||
className="input-field"
|
||||
placeholder="Numero de folio..."
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
minLength={6}
|
||||
maxLength={10}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="input-group">
|
||||
<label className="input-label">Monto:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={amount}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
|
||||
if (/^\d*\.?\d*$/.test(value)) {
|
||||
const numericValue = parseFloat(value);
|
||||
|
||||
if (value === "" || numericValue <= 1000) {
|
||||
setAmount(value);
|
||||
} else {
|
||||
setAlert("");
|
||||
setError("El monto no puede superar $1000.00");
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="input-field"
|
||||
placeholder="Monto recibido..."
|
||||
inputMode="numeric"
|
||||
pattern="^\d*\.?\d+$"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="input-group">
|
||||
<label className="input-label">Fecha de Pago:</label>
|
||||
<input
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
className="input-field"
|
||||
min={minFecha}
|
||||
max={maxFecha}
|
||||
/>
|
||||
</div>
|
||||
<div className="containerButton">
|
||||
<button
|
||||
className="button button-search"
|
||||
type="submit"
|
||||
disabled={loading || loadingstudent}
|
||||
>
|
||||
{loading ? <div className="spinner"></div> : "Guardar"}
|
||||
</button>
|
||||
|
||||
{error && (
|
||||
<div
|
||||
className={`messageBox error ${
|
||||
!showError ? "hidden" : ""
|
||||
}`}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{alert && (
|
||||
<div
|
||||
className={`messageBox success ${
|
||||
!showAlert ? "hidden" : ""
|
||||
}`}
|
||||
>
|
||||
{alert}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<button onClick={handleLogout} className="button button-logout">
|
||||
Cerrar sesión
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
//IO
|
||||
@@ -0,0 +1,86 @@
|
||||
import axios from 'axios';
|
||||
import { useState } from 'react';
|
||||
import { envConfig } from '../envConfigurations/EnvConfigurations';
|
||||
import './Impressions.css';
|
||||
|
||||
export const Login = () => {
|
||||
const [user, setUser] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
const handleLogin = async () => {
|
||||
const url = envConfig().apiUrl;
|
||||
setMessage('')
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${url}/login`, {
|
||||
user,
|
||||
password,
|
||||
});
|
||||
|
||||
if (response.data?.token) {
|
||||
localStorage.setItem('token', response.data.token);
|
||||
localStorage.setItem('session', user);
|
||||
setMessage('Inicio de sesión exitoso');
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
setMessage('Credenciales incorrectas');
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.error || 'Error de conexión con la base de datos';
|
||||
|
||||
setMessage(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className='containerLogin'>
|
||||
|
||||
<form
|
||||
className='login'
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleLogin();
|
||||
}}
|
||||
>
|
||||
<h2 className='textHeader'>Inicio de sesión</h2>
|
||||
|
||||
<div className='containerInput'>
|
||||
<label className='label'>Usuario</label>
|
||||
<input
|
||||
type='text'
|
||||
value={user}
|
||||
onChange={(e) => setUser(e.target.value)}
|
||||
placeholder='Coloca tu usuario...'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='containerInput relative'>
|
||||
<label className='label'>Contraseña</label>
|
||||
<input
|
||||
type={'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder='Coloca tu contraseña...'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className='button button-search'
|
||||
style={{ maxWidth: '100%', width: '100%' }}
|
||||
type='submit'
|
||||
>Iniciar sesión
|
||||
</button>
|
||||
</form>
|
||||
{message &&
|
||||
<div
|
||||
className={`messageBox ${message.includes('exitoso') ? 'success' : 'error'}`}
|
||||
style={{ marginTop: '10px' }}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
//IO
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div style={{ padding: 40, textAlign: 'center' }}>
|
||||
<h1>404 - Página no encontrada</h1>
|
||||
<p>La ruta que buscas no existe.</p>
|
||||
<a href='/Login'>Login</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
//IO
|
||||
@@ -0,0 +1,93 @@
|
||||
.logo {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.cedetecContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 300px;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.cedetecContainer::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(to right, #f9f9f9, rgba(128, 128, 128, 0));
|
||||
}
|
||||
|
||||
.yellowPart {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: 0;
|
||||
left: -20px;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
background: linear-gradient(to right, #bd8c01 40%, #fff 100%);
|
||||
transform: skew(-45deg);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #fff;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.header::after {
|
||||
content: "";
|
||||
top: 0;
|
||||
left: -60px;
|
||||
position: absolute;
|
||||
background-color: #003e79;
|
||||
height: 100%;
|
||||
min-width: 400px;
|
||||
transform: skew(45deg);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.header::before {
|
||||
content: "";
|
||||
top: 0;
|
||||
left: -40px;
|
||||
position: absolute;
|
||||
background-color: rgb(1, 92, 184);
|
||||
height: 100%;
|
||||
width: 10%;
|
||||
min-width: 400px;
|
||||
transform: skew(45deg);
|
||||
z-index: 2;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.yellowPart {
|
||||
left: 0;
|
||||
background: #bd8c01;
|
||||
transform: skew(0deg);
|
||||
}
|
||||
.header::after {
|
||||
left: -100px;
|
||||
}
|
||||
.header::before {
|
||||
left: -80px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.header::after {
|
||||
left: -50px;
|
||||
min-width: 100%;
|
||||
transform: skew(20deg);
|
||||
}
|
||||
.header::before {
|
||||
left: -40px;
|
||||
min-width: 100%;
|
||||
transform: skew(20deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
.label {
|
||||
position: block;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.containerLogin {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
height: 400px;
|
||||
border-radius: 4px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.login {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.textHeader {
|
||||
font-weight: bold;
|
||||
font-size: 1.75rem;
|
||||
text-align: center;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.containerInput {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.eyeClose,
|
||||
.eyeOpen {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: -1px;
|
||||
transform: translateY(-50%);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-size: cover;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.eyeClose {
|
||||
background-image: url('../../img/eye-hidden.svg');
|
||||
}
|
||||
|
||||
.eyeOpen {
|
||||
background-image: url('../../img/eye-open.svg');
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useAuth } from '../../services/useAuth';
|
||||
|
||||
export const LoginRedirect = ({ children }) => {
|
||||
const { loading, authenticated } = useAuth();
|
||||
|
||||
if (loading) return <p>Cargando...</p>;
|
||||
|
||||
return authenticated ? <Navigate to='/' /> : children;
|
||||
};
|
||||
//IO
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useAuth } from '../../services/useAuth';
|
||||
|
||||
export const PrivateRoute = ({ children }) => {
|
||||
const { loading, authenticated } = useAuth();
|
||||
|
||||
if (loading) return <p>Cargando...</p>;
|
||||
|
||||
return authenticated ? children : window.location.href = '/Login';
|
||||
};
|
||||
//IO
|
||||
@@ -0,0 +1,4 @@
|
||||
export const envConfig = () => ({
|
||||
apiUrl: import.meta.env.VITE_API_URL
|
||||
});
|
||||
//IO
|
||||
@@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
import axios from 'axios';
|
||||
import { envConfig } from '../envConfigurations/EnvConfigurations';
|
||||
|
||||
const url = envConfig().apiUrl
|
||||
|
||||
export const searchUser = async (user, password) => {
|
||||
try {
|
||||
const response = await axios.post(url, {
|
||||
user,
|
||||
password,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
};
|
||||
//IO
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import { envConfig } from '../envConfigurations/EnvConfigurations';
|
||||
|
||||
export const useAuth = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [authenticated, setAuthenticated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
if (!token) {
|
||||
setAuthenticated(false);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axios.get(`${envConfig().apiUrl}/validate-token`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
const isValid = res.data.valid;
|
||||
|
||||
if (isValid) {
|
||||
setAuthenticated(true);
|
||||
} else {
|
||||
setAuthenticated(false);
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('session');
|
||||
}
|
||||
} catch (err) {
|
||||
setAuthenticated(false);
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('session');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
}, []);
|
||||
|
||||
return { loading, authenticated };
|
||||
};
|
||||
//IO
|
||||
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineConfig, loadEnv, } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
// https://vite.dev/config/
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), "");
|
||||
|
||||
const port = Number(env.VITE_PORT);
|
||||
|
||||
if (!port) {
|
||||
console.error("Not found VITE_PORT, add VITE_PORT in .env");
|
||||
throw new Error("Not found VITE_PORT, add VITE_PORT in .env");
|
||||
}
|
||||
|
||||
return {
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port,
|
||||
strictPort: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user