diff --git a/dashboard_js/app/components/form.jsx b/dashboard_js/app/components/form.jsx
new file mode 100644
index 0000000..16ea419
--- /dev/null
+++ b/dashboard_js/app/components/form.jsx
@@ -0,0 +1,145 @@
+import React, { forwardRef, useContext, useId } from 'react'
+import { ExclamationCircleIcon } from '@heroicons/react/24/outline'
+import { omit } from 'ramda'
+import Loader from './loader'
+import clsx from 'clsx'
+
+const ControlContext = React.createContext({})
+
+function ControlProvider({ children, ...props }) {
+ return (
+ {children}
+ )
+}
+
+function useControl(props) {
+ const field = useContext(ControlContext)
+ return { ...field, ...props }
+}
+
+export const Control = forwardRef(function Control(
+ { as = 'div', children, ...props },
+ ref,
+) {
+ const id = useId()
+ const props_ = omit(['id'], props)
+
+ return (
+
+ {React.createElement(as, { ref, ...props }, children)}
+
+ )
+})
+
+export function Button({
+ children,
+ as = 'button',
+ className,
+ isLoading = false,
+ ...props
+}) {
+ if (isLoading) {
+ props['disabled'] = isLoading
+ }
+
+ return React.createElement(
+ as,
+ {
+ className: clsx(
+ 'font-medium text-green-primary rounded-lg bg-green-secondary hover:bg-green-support',
+ 'h-12 px-4 relative',
+ 'disabled:bg-green-secondary/50 disabled:pointer-events-none',
+ 'transition',
+ className,
+ ),
+ ...props,
+ },
+ <>
+ {isLoading && (
+
+
+
+ )}
+
+ {children}
+ >,
+ )
+}
+
+export function Label({ children, className, ...props }) {
+ const { id, htmlFor, className: _, ...field } = useControl(props)
+
+ return (
+
+ )
+}
+
+export const Input = forwardRef(function Input(
+ { as = 'input', size = 'base', className, children, ...props },
+ ref,
+) {
+ const { className: _, ...field } = useControl(props)
+ const sizes = { base: 'h-12' }
+
+ return React.createElement(
+ as,
+ {
+ className: clsx(
+ 'bg-white outline-none px-4 rounded-lg transition',
+ 'border border-green-light dark:border-gray-700 dark:bg-gray-800',
+ 'focus:ring-1 focus:border-green-secondary focus:ring-green-secondary focus:placeholder:text-transparent',
+ // Tailwind's won't inherit focus behavior; you must define it explicitly for both modes.
+ 'dark:focus:border-green-secondary',
+ 'aria-[invalid=true]:border-red-400 aria-[invalid=true]:ring-red-400',
+ 'dark:aria-[invalid=true]:border-red-500 dark:aria-[invalid=true]:ring-red-500',
+ 'disabled:text-gray-400 disabled:border-gray-300 disabled:bg-gray-200',
+ 'dark:disabled:text-gray-500 dark:disabled:bg-gray-700',
+ sizes?.[size],
+ className,
+ ),
+ ref,
+ ...field,
+ },
+ children,
+ )
+})
+
+export const Checkbox = forwardRef(function Checkbox(
+ { className, ...props },
+ ref,
+) {
+ const { className: _, ...field } = useControl(props)
+
+ return (
+
+ )
+})
+
+export function Error({ children }) {
+ return (
+
+ )
+}
diff --git a/dashboard_js/app/components/loader.jsx b/dashboard_js/app/components/loader.jsx
new file mode 100644
index 0000000..2942d94
--- /dev/null
+++ b/dashboard_js/app/components/loader.jsx
@@ -0,0 +1,27 @@
+import clsx from 'clsx'
+
+export default function Loader({ className, ...props }) {
+ return (
+
+ )
+}
diff --git a/dashboard_js/app/hooks/use-auth.tsx b/dashboard_js/app/hooks/use-auth.tsx
index dbe391a..ed40550 100644
--- a/dashboard_js/app/hooks/use-auth.tsx
+++ b/dashboard_js/app/hooks/use-auth.tsx
@@ -16,7 +16,7 @@ interface AuthContextType {
username: string
password: string
}) => Promise
- currentUser: () => Promise
+ signOut: () => Promise
}
const AuthContext = createContext(null)
@@ -29,17 +29,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const [authUser, setAuthUser] =
useState(null)
- const currentUser = useCallback(async () => {
- try {
- const currentUser = await Auth.fetchUserAttributes()
-
- setAuthUser(currentUser)
- return currentUser
- } catch {
- setAuthUser(null)
- }
- }, [])
-
const signIn = useCallback(
async ({
username,
@@ -65,16 +54,22 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
[],
)
- const ctxValue = useMemo(
+ const signOut = useCallback(async (): Promise => {
+ try {
+ return await Auth.signOut()
+ } catch {}
+ }, [])
+
+ const authContext = useMemo(
() => ({
authUser,
signIn,
- currentUser,
+ signOut,
}),
- [authUser, signIn, currentUser],
+ [authUser, signIn, signOut],
)
return (
- {children}
+ {children}
)
}
diff --git a/dashboard_js/app/layouts/app.tsx b/dashboard_js/app/layouts/app.tsx
index 5ebd05e..cde43f3 100644
--- a/dashboard_js/app/layouts/app.tsx
+++ b/dashboard_js/app/layouts/app.tsx
@@ -1,20 +1,47 @@
-import { useNavigation } from 'react-router'
+import { useNavigation, redirect, useNavigate } from 'react-router'
+import { fetchAuthSession } from 'aws-amplify/auth'
import { Link } from 'react-router'
import { Outlet } from 'react-router'
+import { useAuth } from '~/hooks/use-auth'
+
+export async function clientLoader() {
+ const session = await fetchAuthSession()
+
+ if (!session?.tokens?.idToken) {
+ throw redirect('/auth')
+ }
+}
export default function Layout() {
const navigation = useNavigation()
+ const navigate = useNavigate()
+ const { signOut } = useAuth()
const isNavigating = Boolean(navigation.location)
return (
<>
-
- -
- Pagamentos
- Usuários
- Matrículas
-
-
+
{isNavigating ? <>Loading...> : }
>
diff --git a/dashboard_js/app/layouts/auth.tsx b/dashboard_js/app/layouts/auth.tsx
index 337ed2f..ba91e1c 100644
--- a/dashboard_js/app/layouts/auth.tsx
+++ b/dashboard_js/app/layouts/auth.tsx
@@ -49,3 +49,17 @@ export default function Auth() {
>
)
}
+
+export function Card({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+export function Container({ children }: { children: React.ReactNode }) {
+ return {children}
+}
diff --git a/dashboard_js/app/root.tsx b/dashboard_js/app/root.tsx
index cd894f2..b2b236b 100644
--- a/dashboard_js/app/root.tsx
+++ b/dashboard_js/app/root.tsx
@@ -10,6 +10,7 @@ import {
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { Amplify } from 'aws-amplify'
import { AuthProvider } from '~/hooks/use-auth'
+import { Smallest as Logo } from '~/components/logo'
import amplifyconfig from './amplifyconfiguration.json'
Amplify.configure(amplifyconfig)
@@ -59,16 +60,7 @@ export default function App() {
export function HydrateFallback() {
return (
-
+
)
}
diff --git a/dashboard_js/app/routes/auth/signin/index.tsx b/dashboard_js/app/routes/auth/signin/index.tsx
index 73af6ef..d047f6e 100644
--- a/dashboard_js/app/routes/auth/signin/index.tsx
+++ b/dashboard_js/app/routes/auth/signin/index.tsx
@@ -1,8 +1,35 @@
+import { useForm } from 'react-hook-form'
+import { useAuth } from '~/hooks/use-auth'
+import { Card } from '~/layouts/auth'
+import { Control, Label, Input, Button } from '~/components/form'
+import { useNavigate } from 'react-router'
+
export default function Signin() {
+ const navigate = useNavigate()
+ const { register, handleSubmit, formState } = useForm()
+ const { signIn } = useAuth()
+
+ const onSubmit = async (payload) => {
+ const { nextStep } = await signIn(payload)
+ return navigate('/')
+ }
+
return (
-
+
+
+
)
}
diff --git a/dashboard_js/package-lock.json b/dashboard_js/package-lock.json
index 3914323..57836e2 100644
--- a/dashboard_js/package-lock.json
+++ b/dashboard_js/package-lock.json
@@ -6,15 +6,21 @@
"": {
"name": "dashboard_js",
"dependencies": {
+ "@heroicons/react": "^2.2.0",
+ "@hookform/error-message": "^2.0.1",
+ "@hookform/resolvers": "^4.1.0",
"@react-router/fs-routes": "^7.2.0",
"@react-router/node": "^7.2.0",
"@react-router/serve": "^7.2.0",
"@tanstack/react-query": "^5.66.9",
"aws-amplify": "^6.13.1",
"axios": "^1.7.9",
+ "clsx": "^2.1.1",
"isbot": "^5.1.17",
+ "ramda": "^0.30.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "react-hook-form": "^7.54.2",
"react-router": "^7.2.0"
},
"devDependencies": {
@@ -2450,6 +2456,38 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@heroicons/react": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
+ "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">= 16 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/@hookform/error-message": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@hookform/error-message/-/error-message-2.0.1.tgz",
+ "integrity": "sha512-U410sAr92xgxT1idlu9WWOVjndxLdgPUHEB8Schr27C9eh7/xUnITWpCMF93s+lGiG++D4JnbSnrb5A21AdSNg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0",
+ "react-hook-form": "^7.0.0"
+ }
+ },
+ "node_modules/@hookform/resolvers": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.0.tgz",
+ "integrity": "sha512-fX/uHKb+OOCpACLc6enuTQsf0ZpRrKbeBBPETg5PCPLCIYV6osP2Bw6ezuclM61lH+wBF9eXcuC0+BFh9XOEnQ==",
+ "license": "MIT",
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001698"
+ },
+ "peerDependencies": {
+ "react-hook-form": "^7.0.0"
+ }
+ },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -5209,7 +5247,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -7649,6 +7686,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/ramda": {
+ "version": "0.30.1",
+ "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz",
+ "integrity": "sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ramda"
+ }
+ },
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -7736,6 +7783,22 @@
"react": "^19.0.0"
}
},
+ "node_modules/react-hook-form": {
+ "version": "7.54.2",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz",
+ "integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18 || ^19"
+ }
+ },
"node_modules/react-hotkeys-hook": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.6.1.tgz",
diff --git a/dashboard_js/package.json b/dashboard_js/package.json
index 3f9d2b2..ce5736a 100644
--- a/dashboard_js/package.json
+++ b/dashboard_js/package.json
@@ -9,15 +9,21 @@
"typecheck": "react-router typegen && tsc"
},
"dependencies": {
+ "@heroicons/react": "^2.2.0",
+ "@hookform/error-message": "^2.0.1",
+ "@hookform/resolvers": "^4.1.0",
"@react-router/fs-routes": "^7.2.0",
"@react-router/node": "^7.2.0",
"@react-router/serve": "^7.2.0",
"@tanstack/react-query": "^5.66.9",
"aws-amplify": "^6.13.1",
"axios": "^1.7.9",
+ "clsx": "^2.1.1",
"isbot": "^5.1.17",
+ "ramda": "^0.30.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "react-hook-form": "^7.54.2",
"react-router": "^7.2.0"
},
"devDependencies": {