updated UI, minor fixes

This commit is contained in:
lnn0q 2025-04-20 18:10:34 +03:00
parent b5280b70a1
commit 755c56ee8a
9 changed files with 175 additions and 98 deletions

1
app/client/helpers.tsx Normal file
View file

@ -0,0 +1 @@

View file

@ -16,8 +16,11 @@ import { Session } from '@generated/types';
import { routeTree } from './routeTree.gen'; import { routeTree } from './routeTree.gen';
// Create a new router instance // Create a new router instance
const router = createRouter({ export const router = createRouter({
routeTree routeTree,
context: {
isAuthenticated: false
}
}); });
// Register the router instance for type safety // Register the router instance for type safety
@ -28,13 +31,15 @@ declare module '@tanstack/react-router' {
} }
// Environment // Environment
const MODE = import.meta.env.VITE_MODE || 'prod'; const mode = import.meta.env.VITE_MODE || 'prod';
const isProd = MODE === 'prod'; const isProd = mode === 'prod';
const HOST = import.meta.env.VITE_HOST || 'localhost'; export const appName = import.meta.env.VITE_APP_NAME || 'project-ln';
const PORT = import.meta.env.VITE_PORT || '3000';
export const url = `https://${HOST}:${PORT}`; const host = import.meta.env.VITE_HOST || 'localhost';
const graphql_uri = `${url}/graphql`; const port = import.meta.env.VITE_PORT || '3000';
export const url = `https://${host}:${port}`;
const graphqlUri = `${url}/graphql`;
function App() { function App() {
const [authHeaders, setAuthHeaders] = useState<Session | null>(null); const [authHeaders, setAuthHeaders] = useState<Session | null>(null);
@ -60,7 +65,7 @@ function App() {
); );
const httpLink = new HttpLink({ const httpLink = new HttpLink({
uri: graphql_uri, uri: graphqlUri,
credentials: 'include', credentials: 'include',
headers: authHeaders != null ? { authorization: JSON.stringify(authHeaders) } : undefined headers: authHeaders != null ? { authorization: JSON.stringify(authHeaders) } : undefined
}); });
@ -70,7 +75,7 @@ function App() {
graphQLErrors.map(async error => { graphQLErrors.map(async error => {
if (error.message.includes('jwt expired')) { if (error.message.includes('jwt expired')) {
const session = await refreshToken(); const session = await refreshToken();
if (session != undefined) setAuthHeaders(session); if (session !== undefined) setAuthHeaders(session);
return forward(operation); return forward(operation);
} }
if (!isProd) if (!isProd)
@ -88,7 +93,7 @@ function App() {
} }
const client = new ApolloClient({ const client = new ApolloClient({
uri: graphql_uri, uri: graphqlUri,
link: from([errorLink, httpLink]), link: from([errorLink, httpLink]),
cache: new InMemoryCache(), cache: new InMemoryCache(),
connectToDevTools: !isProd, connectToDevTools: !isProd,
@ -100,7 +105,7 @@ function App() {
<AuthProvider <AuthProvider
value={{ value={{
headers: authHeaders, headers: authHeaders,
setHeaders: (session: Session) => { setHeaders: (session: Session | null) => {
setAuthHeaders(session); setAuthHeaders(session);
} }
}} }}
@ -108,7 +113,7 @@ function App() {
<ApolloProvider client={client}> <ApolloProvider client={client}>
<ChakraProvider> <ChakraProvider>
<Toaster /> <Toaster />
<RouterProvider router={router} /> <RouterProvider router={router} context={{ isAuthenticated: authHeaders != null }} />
</ChakraProvider> </ChakraProvider>
</ApolloProvider> </ApolloProvider>
</AuthProvider> </AuthProvider>

View file

@ -1,7 +1,10 @@
import { Button, Stack } from '@chakra-ui/react'; import { Icon, Stack, Text } from '@chakra-ui/react';
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'; import { createRootRouteWithContext, Link, Outlet } from '@tanstack/react-router';
import React from 'react'; import React from 'react';
import { Button } from '../components/ui/button';
import { Tooltip } from '../components/ui/tooltip';
import { useAuth } from '../hooks/auth'; import { useAuth } from '../hooks/auth';
import { appName } from '../main';
const TanStackRouterDevtools = const TanStackRouterDevtools =
process.env.NODE_ENV === 'production' process.env.NODE_ENV === 'production'
@ -15,17 +18,30 @@ const TanStackRouterDevtools =
})) }))
); );
export const Route = createRootRoute({ interface RouterContext {
isAuthenticated: boolean;
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: () => { component: () => {
const { isAuthenticated, logout } = useAuth(); const { isAuthenticated, logout } = useAuth();
return ( return (
<Stack padding={3}> <Stack padding={3}>
<Stack flexDirection={'row'} justifyContent={'space-between'}> <Stack flexDirection="row" justifyContent="space-between" alignItems="center">
<Link to="/"> <Stack flexDirection="row" alignItems="center" gap={4}>
<Button>Home</Button> <Link to="/">
</Link> <Text color="colorPalette.500" fontSize="xl">
[{appName}]
</Text>
</Link>
<Link to="/about">
<Icon color="colorPalette.500" fontSize="md">
<i className="nf nf-oct-question" />
</Icon>
</Link>
</Stack>
<Stack flexDirection={'row'}> <Stack flexDirection="row" alignItems="center">
{!isAuthenticated ? ( {!isAuthenticated ? (
<> <>
<Link to="/login"> <Link to="/login">
@ -36,7 +52,11 @@ export const Route = createRootRoute({
</Link> </Link>
</> </>
) : ( ) : (
<Button onClick={async () => logout()}>Log out</Button> <Tooltip content="Log out">
<Icon color="colorPalette.500" fontSize="xl" onClick={async () => logout()}>
<i className="nf nf-fa-sign_out" />
</Icon>
</Tooltip>
)} )}
</Stack> </Stack>
</Stack> </Stack>

View file

@ -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 (
<Stack flexDirection={'row'} alignItems={'center'}>
<Text>Hello from About!</Text>
<Text>And NerdFonts!</Text>
<Icon>
<i className="nf nf-fa-heart" />
</Icon>
<Icon>
<i className="nf nf-custom-vim" />
</Icon>
</Stack>
);
}

View file

@ -6,10 +6,10 @@ import { gql, useQuery } from '@apollo/client';
import { BooksManyQuery } from '@generated/types'; import { BooksManyQuery } from '@generated/types';
export const Route = createFileRoute('/')({ export const Route = createFileRoute('/')({
component: Index component: Dashboard
}); });
function Index() { function Dashboard() {
const { data, refetch } = useQuery<BooksManyQuery>(getBooksQuery); const { data, refetch } = useQuery<BooksManyQuery>(getBooksQuery);
return ( return (

View file

@ -2,14 +2,17 @@ import { Heading, Input, Stack } from '@chakra-ui/react';
import { Button } from '@client/components/ui/button'; import { Button } from '@client/components/ui/button';
import { Field } from '@client/components/ui/field'; import { Field } from '@client/components/ui/field';
import { valibotResolver } from '@hookform/resolvers/valibot'; 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 { useForm } from 'react-hook-form';
import * as v from 'valibot'; import * as v from 'valibot';
import { PasswordInput } from '../components/ui/password-input'; import { PasswordInput } from '../components/ui/password-input';
import { useAuth } from '../hooks/auth'; import { useAuth } from '../hooks/auth';
export const Route = createFileRoute('/login')({ export const Route = createFileRoute('/login')({
component: LoginComponent component: LoginComponent,
beforeLoad: ({ context }) => {
if (context.isAuthenticated) throw redirect({ to: '/' });
}
}); });
function LoginComponent() { function LoginComponent() {
@ -27,8 +30,6 @@ interface FormValues {
} }
function LoginForm() { function LoginForm() {
const navigate = useNavigate();
const schema = v.pipe( const schema = v.pipe(
v.object({ v.object({
email: v.pipe(v.string(), v.nonEmpty('Email is required'), v.email('Invalid email')), email: v.pipe(v.string(), v.nonEmpty('Email is required'), v.email('Invalid email')),
@ -51,7 +52,7 @@ function LoginForm() {
email: values.email, email: values.email,
password: values.password password: values.password
}); });
if (session != null) navigate({ to: '/' }); if (session != null) window.history.back();
})} })}
> >
<Stack> <Stack>

View file

@ -5,13 +5,16 @@ import { Field } from '@client/components/ui/field';
import { PasswordInput } from '@client/components/ui/password-input'; import { PasswordInput } from '@client/components/ui/password-input';
import { Session } from '@generated/types'; import { Session } from '@generated/types';
import { valibotResolver } from '@hookform/resolvers/valibot'; 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 { useForm } from 'react-hook-form';
import * as v from 'valibot'; import * as v from 'valibot';
import { useToastMutation } from '../hooks/useToastMutation'; import { useToastMutation } from '../hooks/useToastMutation';
export const Route = createFileRoute('/register')({ export const Route = createFileRoute('/register')({
component: RegisterComponent component: RegisterComponent,
beforeLoad: ({ context }) => {
if (context.isAuthenticated) throw redirect({ to: '/' });
}
}); });
function RegisterComponent() { function RegisterComponent() {
@ -43,8 +46,11 @@ function SignUpForm() {
username: v.pipe(v.string(), v.nonEmpty('Username is required')), username: v.pipe(v.string(), v.nonEmpty('Username is required')),
password: v.pipe( password: v.pipe(
v.string(), v.string(),
v.nonEmpty('Password is required'), v.minLength(8, 'Your password is too short.'),
v.minLength(8, 'Needs to be at least 8 characters') 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')) confirm_password: v.pipe(v.string(), v.nonEmpty('Confirm your password'))
}), }),

View file

@ -8,130 +8,149 @@
// You should NOT make any changes in this file as it will be overwritten. // 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. // 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 Routes
import { Route as rootRoute } from './pages/__root'; import { Route as rootRoute } from './pages/__root'
import { Route as RegisterImport } from './pages/register'; import { Route as RegisterImport } from './pages/register'
import { Route as LoginImport } from './pages/login'; import { Route as LoginImport } from './pages/login'
import { Route as IndexImport } from './pages/index'; import { Route as AboutImport } from './pages/about'
import { Route as IndexImport } from './pages/index'
// Create Virtual Routes // Create Virtual Routes
const BookLazyImport = createFileRoute('/book')(); const BookLazyImport = createFileRoute('/book')()
// Create/Update Routes // Create/Update Routes
const BookLazyRoute = BookLazyImport.update({ const BookLazyRoute = BookLazyImport.update({
id: '/book', id: '/book',
path: '/book', path: '/book',
getParentRoute: () => rootRoute getParentRoute: () => rootRoute,
} as any).lazy(() => import('./pages/book.lazy').then(d => d.Route)); } as any).lazy(() => import('./pages/book.lazy').then((d) => d.Route))
const RegisterRoute = RegisterImport.update({ const RegisterRoute = RegisterImport.update({
id: '/register', id: '/register',
path: '/register', path: '/register',
getParentRoute: () => rootRoute getParentRoute: () => rootRoute,
} as any); } as any)
const LoginRoute = LoginImport.update({ const LoginRoute = LoginImport.update({
id: '/login', id: '/login',
path: '/login', path: '/login',
getParentRoute: () => rootRoute getParentRoute: () => rootRoute,
} as any); } as any)
const AboutRoute = AboutImport.update({
id: '/about',
path: '/about',
getParentRoute: () => rootRoute,
} as any)
const IndexRoute = IndexImport.update({ const IndexRoute = IndexImport.update({
id: '/', id: '/',
path: '/', path: '/',
getParentRoute: () => rootRoute getParentRoute: () => rootRoute,
} as any); } as any)
// Populate the FileRoutesByPath interface // Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
interface FileRoutesByPath { interface FileRoutesByPath {
'/': { '/': {
id: '/'; id: '/'
path: '/'; path: '/'
fullPath: '/'; fullPath: '/'
preLoaderRoute: typeof IndexImport; preLoaderRoute: typeof IndexImport
parentRoute: typeof rootRoute; parentRoute: typeof rootRoute
}; }
'/about': {
id: '/about'
path: '/about'
fullPath: '/about'
preLoaderRoute: typeof AboutImport
parentRoute: typeof rootRoute
}
'/login': { '/login': {
id: '/login'; id: '/login'
path: '/login'; path: '/login'
fullPath: '/login'; fullPath: '/login'
preLoaderRoute: typeof LoginImport; preLoaderRoute: typeof LoginImport
parentRoute: typeof rootRoute; parentRoute: typeof rootRoute
}; }
'/register': { '/register': {
id: '/register'; id: '/register'
path: '/register'; path: '/register'
fullPath: '/register'; fullPath: '/register'
preLoaderRoute: typeof RegisterImport; preLoaderRoute: typeof RegisterImport
parentRoute: typeof rootRoute; parentRoute: typeof rootRoute
}; }
'/book': { '/book': {
id: '/book'; id: '/book'
path: '/book'; path: '/book'
fullPath: '/book'; fullPath: '/book'
preLoaderRoute: typeof BookLazyImport; preLoaderRoute: typeof BookLazyImport
parentRoute: typeof rootRoute; parentRoute: typeof rootRoute
}; }
} }
} }
// Create and export the route tree // Create and export the route tree
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute; '/': typeof IndexRoute
'/login': typeof LoginRoute; '/about': typeof AboutRoute
'/register': typeof RegisterRoute; '/login': typeof LoginRoute
'/book': typeof BookLazyRoute; '/register': typeof RegisterRoute
'/book': typeof BookLazyRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute; '/': typeof IndexRoute
'/login': typeof LoginRoute; '/about': typeof AboutRoute
'/register': typeof RegisterRoute; '/login': typeof LoginRoute
'/book': typeof BookLazyRoute; '/register': typeof RegisterRoute
'/book': typeof BookLazyRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRoute; __root__: typeof rootRoute
'/': typeof IndexRoute; '/': typeof IndexRoute
'/login': typeof LoginRoute; '/about': typeof AboutRoute
'/register': typeof RegisterRoute; '/login': typeof LoginRoute
'/book': typeof BookLazyRoute; '/register': typeof RegisterRoute
'/book': typeof BookLazyRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath; fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/login' | '/register' | '/book'; fullPaths: '/' | '/about' | '/login' | '/register' | '/book'
fileRoutesByTo: FileRoutesByTo; fileRoutesByTo: FileRoutesByTo
to: '/' | '/login' | '/register' | '/book'; to: '/' | '/about' | '/login' | '/register' | '/book'
id: '__root__' | '/' | '/login' | '/register' | '/book'; id: '__root__' | '/' | '/about' | '/login' | '/register' | '/book'
fileRoutesById: FileRoutesById; fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute; IndexRoute: typeof IndexRoute
LoginRoute: typeof LoginRoute; AboutRoute: typeof AboutRoute
RegisterRoute: typeof RegisterRoute; LoginRoute: typeof LoginRoute
BookLazyRoute: typeof BookLazyRoute; RegisterRoute: typeof RegisterRoute
BookLazyRoute: typeof BookLazyRoute
} }
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
AboutRoute: AboutRoute,
LoginRoute: LoginRoute, LoginRoute: LoginRoute,
RegisterRoute: RegisterRoute, RegisterRoute: RegisterRoute,
BookLazyRoute: BookLazyRoute BookLazyRoute: BookLazyRoute,
}; }
export const routeTree = rootRoute export const routeTree = rootRoute
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>(); ._addFileTypes<FileRouteTypes>()
/* ROUTE_MANIFEST_START /* ROUTE_MANIFEST_START
{ {
@ -140,6 +159,7 @@ export const routeTree = rootRoute
"filePath": "__root.tsx", "filePath": "__root.tsx",
"children": [ "children": [
"/", "/",
"/about",
"/login", "/login",
"/register", "/register",
"/book" "/book"
@ -148,6 +168,9 @@ export const routeTree = rootRoute
"/": { "/": {
"filePath": "index.tsx" "filePath": "index.tsx"
}, },
"/about": {
"filePath": "about.tsx"
},
"/login": { "/login": {
"filePath": "login.tsx" "filePath": "login.tsx"
}, },

View file

@ -23,5 +23,5 @@
"@root/*": ["./*"] "@root/*": ["./*"]
} }
}, },
"exclude": ["./app/client/components/ui", "./app/client/assets", "./__generated__/*", "./dist"] "exclude": ["./app/client/assets", "./__generated__/*", "./dist"]
} }