diff --git a/app/client/helpers.tsx b/app/client/helpers.tsx new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/client/helpers.tsx @@ -0,0 +1 @@ + diff --git a/app/client/main.tsx b/app/client/main.tsx index d8919d0..d2f6fb2 100644 --- a/app/client/main.tsx +++ b/app/client/main.tsx @@ -16,8 +16,11 @@ import { Session } from '@generated/types'; import { routeTree } from './routeTree.gen'; // Create a new router instance -const router = createRouter({ - routeTree +export const router = createRouter({ + routeTree, + context: { + isAuthenticated: false + } }); // Register the router instance for type safety @@ -28,13 +31,15 @@ declare module '@tanstack/react-router' { } // Environment -const MODE = import.meta.env.VITE_MODE || 'prod'; -const isProd = MODE === 'prod'; +const mode = import.meta.env.VITE_MODE || 'prod'; +const isProd = mode === 'prod'; -const HOST = import.meta.env.VITE_HOST || 'localhost'; -const PORT = import.meta.env.VITE_PORT || '3000'; -export const url = `https://${HOST}:${PORT}`; -const graphql_uri = `${url}/graphql`; +export const appName = import.meta.env.VITE_APP_NAME || 'project-ln'; + +const host = import.meta.env.VITE_HOST || 'localhost'; +const port = import.meta.env.VITE_PORT || '3000'; +export const url = `https://${host}:${port}`; +const graphqlUri = `${url}/graphql`; function App() { const [authHeaders, setAuthHeaders] = useState(null); @@ -60,7 +65,7 @@ function App() { ); const httpLink = new HttpLink({ - uri: graphql_uri, + uri: graphqlUri, credentials: 'include', headers: authHeaders != null ? { authorization: JSON.stringify(authHeaders) } : undefined }); @@ -70,7 +75,7 @@ function App() { graphQLErrors.map(async error => { if (error.message.includes('jwt expired')) { const session = await refreshToken(); - if (session != undefined) setAuthHeaders(session); + if (session !== undefined) setAuthHeaders(session); return forward(operation); } if (!isProd) @@ -88,7 +93,7 @@ function App() { } const client = new ApolloClient({ - uri: graphql_uri, + uri: graphqlUri, link: from([errorLink, httpLink]), cache: new InMemoryCache(), connectToDevTools: !isProd, @@ -100,7 +105,7 @@ function App() { { + setHeaders: (session: Session | null) => { setAuthHeaders(session); } }} @@ -108,7 +113,7 @@ function App() { - + diff --git a/app/client/pages/__root.tsx b/app/client/pages/__root.tsx index 25c2d04..c1e32de 100644 --- a/app/client/pages/__root.tsx +++ b/app/client/pages/__root.tsx @@ -1,7 +1,10 @@ -import { Button, Stack } from '@chakra-ui/react'; -import { createRootRoute, Link, Outlet } from '@tanstack/react-router'; +import { Icon, Stack, Text } from '@chakra-ui/react'; +import { createRootRouteWithContext, Link, Outlet } from '@tanstack/react-router'; import React from 'react'; +import { Button } from '../components/ui/button'; +import { Tooltip } from '../components/ui/tooltip'; import { useAuth } from '../hooks/auth'; +import { appName } from '../main'; const TanStackRouterDevtools = process.env.NODE_ENV === 'production' @@ -15,17 +18,30 @@ const TanStackRouterDevtools = })) ); -export const Route = createRootRoute({ +interface RouterContext { + isAuthenticated: boolean; +} + +export const Route = createRootRouteWithContext()({ component: () => { const { isAuthenticated, logout } = useAuth(); return ( - - - - + + + + + [{appName}] + + + + + + + + - + {!isAuthenticated ? ( <> @@ -36,7 +52,11 @@ export const Route = createRootRoute({ ) : ( - + + logout()}> + + + )} diff --git a/app/client/pages/about.tsx b/app/client/pages/about.tsx new file mode 100644 index 0000000..f14b877 --- /dev/null +++ b/app/client/pages/about.tsx @@ -0,0 +1,21 @@ +import { Text, Icon, Stack } from '@chakra-ui/react'; +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/about')({ + component: About +}); + +function About() { + return ( + + Hello from About! + And NerdFonts! + + + + + + + + ); +} diff --git a/app/client/pages/index.tsx b/app/client/pages/index.tsx index 30e9780..61ec4d1 100644 --- a/app/client/pages/index.tsx +++ b/app/client/pages/index.tsx @@ -6,10 +6,10 @@ import { gql, useQuery } from '@apollo/client'; import { BooksManyQuery } from '@generated/types'; export const Route = createFileRoute('/')({ - component: Index + component: Dashboard }); -function Index() { +function Dashboard() { const { data, refetch } = useQuery(getBooksQuery); return ( diff --git a/app/client/pages/login.tsx b/app/client/pages/login.tsx index c523ac9..faad333 100644 --- a/app/client/pages/login.tsx +++ b/app/client/pages/login.tsx @@ -2,14 +2,17 @@ import { Heading, Input, Stack } from '@chakra-ui/react'; import { Button } from '@client/components/ui/button'; import { Field } from '@client/components/ui/field'; import { valibotResolver } from '@hookform/resolvers/valibot'; -import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import { createFileRoute, redirect } from '@tanstack/react-router'; import { useForm } from 'react-hook-form'; import * as v from 'valibot'; import { PasswordInput } from '../components/ui/password-input'; import { useAuth } from '../hooks/auth'; export const Route = createFileRoute('/login')({ - component: LoginComponent + component: LoginComponent, + beforeLoad: ({ context }) => { + if (context.isAuthenticated) throw redirect({ to: '/' }); + } }); function LoginComponent() { @@ -27,8 +30,6 @@ interface FormValues { } function LoginForm() { - const navigate = useNavigate(); - const schema = v.pipe( v.object({ email: v.pipe(v.string(), v.nonEmpty('Email is required'), v.email('Invalid email')), @@ -51,7 +52,7 @@ function LoginForm() { email: values.email, password: values.password }); - if (session != null) navigate({ to: '/' }); + if (session != null) window.history.back(); })} > diff --git a/app/client/pages/register.tsx b/app/client/pages/register.tsx index b008f05..b137044 100644 --- a/app/client/pages/register.tsx +++ b/app/client/pages/register.tsx @@ -5,13 +5,16 @@ import { Field } from '@client/components/ui/field'; import { PasswordInput } from '@client/components/ui/password-input'; import { Session } from '@generated/types'; import { valibotResolver } from '@hookform/resolvers/valibot'; -import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router'; import { useForm } from 'react-hook-form'; import * as v from 'valibot'; import { useToastMutation } from '../hooks/useToastMutation'; export const Route = createFileRoute('/register')({ - component: RegisterComponent + component: RegisterComponent, + beforeLoad: ({ context }) => { + if (context.isAuthenticated) throw redirect({ to: '/' }); + } }); function RegisterComponent() { @@ -43,8 +46,11 @@ function SignUpForm() { username: v.pipe(v.string(), v.nonEmpty('Username is required')), password: v.pipe( v.string(), - v.nonEmpty('Password is required'), - v.minLength(8, 'Needs to be at least 8 characters') + v.minLength(8, 'Your password is too short.'), + v.maxLength(45, 'Your password is too long.'), + v.regex(/[a-z]/, 'Your password must contain a lowercase letter.'), + v.regex(/[A-Z]/, 'Your password must contain a uppercase letter.'), + v.regex(/[0-9]/, 'Your password must contain a number.') ), confirm_password: v.pipe(v.string(), v.nonEmpty('Confirm your password')) }), diff --git a/app/client/routeTree.gen.ts b/app/client/routeTree.gen.ts index 9c8b746..95d41eb 100644 --- a/app/client/routeTree.gen.ts +++ b/app/client/routeTree.gen.ts @@ -8,130 +8,149 @@ // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. -import { createFileRoute } from '@tanstack/react-router'; +import { createFileRoute } from '@tanstack/react-router' // Import Routes -import { Route as rootRoute } from './pages/__root'; -import { Route as RegisterImport } from './pages/register'; -import { Route as LoginImport } from './pages/login'; -import { Route as IndexImport } from './pages/index'; +import { Route as rootRoute } from './pages/__root' +import { Route as RegisterImport } from './pages/register' +import { Route as LoginImport } from './pages/login' +import { Route as AboutImport } from './pages/about' +import { Route as IndexImport } from './pages/index' // Create Virtual Routes -const BookLazyImport = createFileRoute('/book')(); +const BookLazyImport = createFileRoute('/book')() // Create/Update Routes const BookLazyRoute = BookLazyImport.update({ id: '/book', path: '/book', - getParentRoute: () => rootRoute -} as any).lazy(() => import('./pages/book.lazy').then(d => d.Route)); + getParentRoute: () => rootRoute, +} as any).lazy(() => import('./pages/book.lazy').then((d) => d.Route)) const RegisterRoute = RegisterImport.update({ id: '/register', path: '/register', - getParentRoute: () => rootRoute -} as any); + getParentRoute: () => rootRoute, +} as any) const LoginRoute = LoginImport.update({ id: '/login', path: '/login', - getParentRoute: () => rootRoute -} as any); + getParentRoute: () => rootRoute, +} as any) + +const AboutRoute = AboutImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRoute, +} as any) const IndexRoute = IndexImport.update({ id: '/', path: '/', - getParentRoute: () => rootRoute -} as any); + getParentRoute: () => rootRoute, +} as any) // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { interface FileRoutesByPath { '/': { - id: '/'; - path: '/'; - fullPath: '/'; - preLoaderRoute: typeof IndexImport; - parentRoute: typeof rootRoute; - }; + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutImport + parentRoute: typeof rootRoute + } '/login': { - id: '/login'; - path: '/login'; - fullPath: '/login'; - preLoaderRoute: typeof LoginImport; - parentRoute: typeof rootRoute; - }; + id: '/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LoginImport + parentRoute: typeof rootRoute + } '/register': { - id: '/register'; - path: '/register'; - fullPath: '/register'; - preLoaderRoute: typeof RegisterImport; - parentRoute: typeof rootRoute; - }; + id: '/register' + path: '/register' + fullPath: '/register' + preLoaderRoute: typeof RegisterImport + parentRoute: typeof rootRoute + } '/book': { - id: '/book'; - path: '/book'; - fullPath: '/book'; - preLoaderRoute: typeof BookLazyImport; - parentRoute: typeof rootRoute; - }; + id: '/book' + path: '/book' + fullPath: '/book' + preLoaderRoute: typeof BookLazyImport + parentRoute: typeof rootRoute + } } } // Create and export the route tree export interface FileRoutesByFullPath { - '/': typeof IndexRoute; - '/login': typeof LoginRoute; - '/register': typeof RegisterRoute; - '/book': typeof BookLazyRoute; + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/login': typeof LoginRoute + '/register': typeof RegisterRoute + '/book': typeof BookLazyRoute } export interface FileRoutesByTo { - '/': typeof IndexRoute; - '/login': typeof LoginRoute; - '/register': typeof RegisterRoute; - '/book': typeof BookLazyRoute; + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/login': typeof LoginRoute + '/register': typeof RegisterRoute + '/book': typeof BookLazyRoute } export interface FileRoutesById { - __root__: typeof rootRoute; - '/': typeof IndexRoute; - '/login': typeof LoginRoute; - '/register': typeof RegisterRoute; - '/book': typeof BookLazyRoute; + __root__: typeof rootRoute + '/': typeof IndexRoute + '/about': typeof AboutRoute + '/login': typeof LoginRoute + '/register': typeof RegisterRoute + '/book': typeof BookLazyRoute } export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath; - fullPaths: '/' | '/login' | '/register' | '/book'; - fileRoutesByTo: FileRoutesByTo; - to: '/' | '/login' | '/register' | '/book'; - id: '__root__' | '/' | '/login' | '/register' | '/book'; - fileRoutesById: FileRoutesById; + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/about' | '/login' | '/register' | '/book' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/about' | '/login' | '/register' | '/book' + id: '__root__' | '/' | '/about' | '/login' | '/register' | '/book' + fileRoutesById: FileRoutesById } export interface RootRouteChildren { - IndexRoute: typeof IndexRoute; - LoginRoute: typeof LoginRoute; - RegisterRoute: typeof RegisterRoute; - BookLazyRoute: typeof BookLazyRoute; + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute + LoginRoute: typeof LoginRoute + RegisterRoute: typeof RegisterRoute + BookLazyRoute: typeof BookLazyRoute } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + AboutRoute: AboutRoute, LoginRoute: LoginRoute, RegisterRoute: RegisterRoute, - BookLazyRoute: BookLazyRoute -}; + BookLazyRoute: BookLazyRoute, +} export const routeTree = rootRoute ._addFileChildren(rootRouteChildren) - ._addFileTypes(); + ._addFileTypes() /* ROUTE_MANIFEST_START { @@ -140,6 +159,7 @@ export const routeTree = rootRoute "filePath": "__root.tsx", "children": [ "/", + "/about", "/login", "/register", "/book" @@ -148,6 +168,9 @@ export const routeTree = rootRoute "/": { "filePath": "index.tsx" }, + "/about": { + "filePath": "about.tsx" + }, "/login": { "filePath": "login.tsx" }, diff --git a/tsconfig.json b/tsconfig.json index 5582c57..1dab02f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,5 +23,5 @@ "@root/*": ["./*"] } }, - "exclude": ["./app/client/components/ui", "./app/client/assets", "./__generated__/*", "./dist"] + "exclude": ["./app/client/assets", "./__generated__/*", "./dist"] }