implemented identity, updated session, added 'workings' list

added paginated querying, added 'common'
This commit is contained in:
lnn0q 2025-05-17 00:29:55 +03:00
parent 027deba422
commit 6ada519e06
36 changed files with 1422 additions and 279 deletions

View file

@ -0,0 +1,159 @@
import { Card, Heading, SystemStyleObject } from '@chakra-ui/react';
import EditorJS, { EditorConfig } from '@editorjs/editorjs';
import Header from '@editorjs/header';
import List from '@editorjs/list';
import Quote from '@editorjs/quote';
import { useEffect, useRef } from 'react';
// @ts-ignore
import Link from '@coolbytes/editorjs-link';
export function Editor({ initialData }: { initialData?: EditorConfig['data'] }) {
const ejInstance = useRef<EditorJS | undefined | null>();
const initEditor = () => {
const editor = new EditorJS({
holder: 'editorjs',
onReady: () => {
ejInstance.current = editor;
},
autofocus: true,
data: initialData,
// onChange: async () => {
// const content = await editor.saver.save();
// },
tools: {
header: Header,
list: List,
link: Link,
quote: Quote
}
});
};
useEffect(() => {
if (ejInstance.current === null) {
initEditor();
}
return () => {
ejInstance?.current?.destroy();
ejInstance.current = null;
};
}, []);
return (
<>
<Card.Root
variant={'subtle'}
width="100%"
maxWidth="840px"
minHeight="sm"
justifyContent="center"
alignItems="center"
>
<Card.Header>
<Heading textAlign="center">Editor</Heading>
</Card.Header>
<Card.Body
width="90%"
css={{
'& *': {
fontSize: 'revert',
fontWeight: 'revert'
},
...editorJsStyles
}}
>
<div className="editorjs" id="editorjs"></div>
</Card.Body>
</Card.Root>
</>
);
}
const editorJsStyles:
| SystemStyleObject
| Omit<Array<SystemStyleObject | undefined>, keyof any[]>
| undefined = {
'& .ce-block--selected .ce-block__content, \
& .ce-inline-toolbar, \
& .codex-editor--narrow .ce-toolbox, \
& .ce-conversion-toolbar, \
& .ce-settings, \
& .ce-settings__button, \
& .ce-toolbar__settings-btn, \
& .cdx-button, \
& .ce-popover': {
background: 'colorPalette.500',
border: 'none'
},
'& .ce-inline-tool, \
& .ce-conversion-toolbar__label, \
& .ce-toolbox__button, \
& .cdx-settings-button, \
& .ce-toolbar__plus': {
color: 'colorPalette.500'
},
'& ::selection': {
background: 'colorPalette.500'
},
'& .cdx-settings-button:hover, \
& .ce-settings__button:hover, \
& .ce-toolbox__button--active, \
& .ce-toolbox__button:hover, \
& .cdx-button:hover, \
& .ce-inline-toolbar__dropdown:hover, \
& .ce-inline-tool:hover, \
& .ce-popover__item:hover, \
& .ce-toolbar__settings-btn:hover': {
background: 'colorPalette.500',
color: 'fg'
},
'& .cdx-notify--error': {
background: 'red !important'
},
'& .cdx-notify__cross::after, & .cdx-notify__cross::before': {
background: 'white'
},
'& .ce-toolbar__plus:hover, \
& .ce-popover__container': {
backgroundColor: 'bg.emphasized',
color: 'colorPalette.500',
border: 'none'
},
'& .ce-popover-item:hover:not(.ce-popover-item--no-hover)': {
backgroundColor: 'colorPalette.500',
color: 'fg'
},
'& .cdx-search-field': {
background: 'bg.muted',
color: 'fg',
border: 'none'
},
'& .ce-popover-item__title, \
& .ce-popover-item__icon': {
color: 'fg'
},
'& .ce-popover-item-separator': {
color: 'bg.muted'
},
'& .ce-toolbar__settings-btn': {
color: 'white'
},
'& .ce-toolbar__settings-btn:hover': {
color: 'black'
}
};

View file

@ -0,0 +1,44 @@
import { ButtonGroup, Pagination as ChakraPagination, IconButton } from '@chakra-ui/react';
import { LuChevronLeft, LuChevronRight } from 'react-icons/lu';
export function Pagination({
total,
page,
setPage,
pageSize = 5
}: {
total: number;
page: number;
setPage: (value: number) => void;
pageSize?: number;
}) {
return (
<ChakraPagination.Root
count={total}
page={page}
pageSize={pageSize}
onPageChange={e => setPage(e.page)}
defaultPage={1}
>
<ButtonGroup variant="ghost" size="sm">
<ChakraPagination.PrevTrigger asChild>
<IconButton>
<LuChevronLeft />
</IconButton>
</ChakraPagination.PrevTrigger>
<ChakraPagination.Items
render={page => (
<IconButton variant={{ base: 'ghost', _selected: 'solid' }}>{page.value}</IconButton>
)}
/>
<ChakraPagination.NextTrigger asChild>
<IconButton>
<LuChevronRight />
</IconButton>
</ChakraPagination.NextTrigger>
</ButtonGroup>
</ChakraPagination.Root>
);
}

View file

@ -0,0 +1,14 @@
import { List } from '@chakra-ui/react';
import { WorkingCardProps, WorkingListCard } from './WorkingListCard';
export function WorkingList({ workingsList }: { workingsList: WorkingCardProps[] }) {
return (
<List.Root variant="plain" gap={4}>
{workingsList.map(workingData => (
<List.Item key={workingData.id}>
<WorkingListCard {...workingData} />
</List.Item>
))}
</List.Root>
);
}

View file

@ -0,0 +1,115 @@
import { Badge, BadgeProps, Box, Card, Heading, Icon, Link, Stack, Text } from '@chakra-ui/react';
import { WorkingProgressStatus } from '@generated/types';
import { ReactNode } from 'react';
import { WorkingProgressStatusBadge } from './WorkingProgressStatusBadge';
import { WorkingRatingBadge } from './WorkingRatingBadge';
export interface WorkingCardProps {
id: string;
title: string;
description: string;
author: { id: string; username: string };
progressStatus: WorkingProgressStatus;
updatedAt: string;
chaptersTotal: number;
wordsTotal: number;
ratingTotal: number;
reviewsTotal: number;
}
export function WorkingListCard({
title,
description,
author,
progressStatus,
updatedAt,
chaptersTotal,
wordsTotal,
ratingTotal,
reviewsTotal
}: WorkingCardProps) {
return (
<Card.Root width="100%" variant="elevated">
<Card.Body>
<Stack>
<Stack direction="row" justifyContent="space-between">
<Link>
<Heading size="lg">{title}</Heading>
</Link>
<Stack direction="row" justifyContent="center" alignItems="center">
<WorkingRatingBadge ratingTotal={ratingTotal} />
<IconBadge backgroundColor="orange.400" iconClass="nf nf-oct-comment">
<Text>{reviewsTotal}</Text>
</IconBadge>
<WorkingProgressStatusBadge progressStatus={progressStatus} />
</Stack>
</Stack>
<Stack>
<Stack direction="row">
<Text>by:</Text>
<Link>{author.username}</Link>
</Stack>
<Stack direction="row">
<Box>
<Badge size="md" variant="solid">
<Stack direction="row">
<Text>last updated:</Text>
<Text>{updatedAt}</Text>
</Stack>
</Badge>
</Box>
<Box>
<Badge size="md" variant="solid">
<Stack direction="row">
<Text>chapters:</Text>
<Text>{chaptersTotal}</Text>
</Stack>
</Badge>
</Box>
<Box>
<Badge size="md" variant="solid">
<Stack direction="row">
<Text>words:</Text>
<Text>{wordsTotal}</Text>
</Stack>
</Badge>
</Box>
</Stack>
</Stack>
<Stack>
<Text lineClamp={4}>{description}</Text>
</Stack>
</Stack>
</Card.Body>
</Card.Root>
);
}
export function IconBadge({
children,
iconClass,
backgroundColor,
size = 'lg'
}: {
children?: ReactNode;
iconClass: string;
backgroundColor?: BadgeProps['backgroundColor'];
size?: 'sm' | 'md' | 'lg' | 'xs';
}) {
return (
<Box flex={1} direction="row" justifyContent="center" alignItems="center">
<Badge variant="solid" size={size} backgroundColor={backgroundColor}>
<Stack direction="row">
<Icon fontSize="xl">
<i className={iconClass} />
</Icon>
{children}
</Stack>
</Badge>
</Box>
);
}

View file

@ -0,0 +1,40 @@
import { Button, Icon, Input, InputGroup, Stack } from '@chakra-ui/react';
export function WorkingListSearch({
value,
onChange,
onSubmit
}: {
value: string;
onChange: (value: string) => void;
onSubmit: (value: string) => void;
}) {
return (
<form
onSubmit={event => {
event.preventDefault();
onSubmit(value);
}}
>
<Stack direction="row">
<InputGroup
flex="1"
startElement={
<Icon color="colorPalette.500">
<i className="nf nf-oct-search" />
</Icon>
}
>
<Input
value={value}
onChange={event => onChange(event.currentTarget.value)}
placeholder="Search works..."
/>
</InputGroup>
<Button type="submit" border="none">
Refresh
</Button>
</Stack>
</form>
);
}

View file

@ -0,0 +1,37 @@
import { Badge, Box, Icon, Stack, Text } from '@chakra-ui/react';
import { WorkingProgressStatus } from '@generated/types';
import {
assertUnreachable,
toBgColorByProgressStatus,
toWorkingProgressStatusLabel
} from '../helpers';
export function WorkingProgressStatusBadge({
progressStatus
}: {
progressStatus: WorkingProgressStatus;
}) {
return (
<Box flex={1} justifyContent="center" alignItems="center">
<Badge
variant="solid"
backgroundColor={toBgColorByProgressStatus(progressStatus)}
size={'lg'}
>
<Stack direction="row">
<Icon fontSize="xl">
<i className={`${toWorkingProgressStatusIcon(progressStatus)}`} />
</Icon>
<Text>{toWorkingProgressStatusLabel(progressStatus)}</Text>
</Stack>
</Badge>
</Box>
);
}
function toWorkingProgressStatusIcon(value: WorkingProgressStatus): string {
if (value === WorkingProgressStatus.InProgress) return 'nf nf-fa-clock';
if (value === WorkingProgressStatus.Frozen) return 'nf nf-fa-snowflake';
if (value === WorkingProgressStatus.Finished) return 'nf nf-fa-circle_check';
assertUnreachable(value);
}

View file

@ -0,0 +1,28 @@
import { BadgeProps, Text } from '@chakra-ui/react';
import { IconBadge } from './WorkingListCard';
interface WorkingRatingParams {
rating: string;
color: BadgeProps['backgroundColor'];
iconClass: string;
}
export function WorkingRatingBadge({ ratingTotal }: { ratingTotal: number }) {
const ratingParams: WorkingRatingParams =
ratingTotal >= 0
? {
rating: ratingTotal.toString(),
color: 'green.500',
iconClass: 'nf nf-fa-angle_up'
}
: {
rating: ratingTotal.toString().slice(1),
color: 'red.500',
iconClass: 'nf nf-fa-angle_down'
};
return (
<IconBadge backgroundColor={ratingParams.color} iconClass={ratingParams.iconClass}>
<Text>{ratingParams.rating}</Text>
</IconBadge>
);
}

View file

@ -15,7 +15,6 @@ export function ColorModeProvider(props: ColorModeProviderProps) {
export function useColorMode() {
const { resolvedTheme, setTheme } = useTheme();
console.log(resolvedTheme);
const toggleColorMode = () => {
setTheme(resolvedTheme === 'light' ? 'dark' : 'light');
};

View file

@ -1,11 +1,16 @@
'use client';
import { ChakraProvider, defaultSystem, Theme } from '@chakra-ui/react';
import { ChakraProvider, createSystem, defaultConfig, Theme } from '@chakra-ui/react';
import { ColorModeProvider, type ColorModeProviderProps } from './color-mode';
export function Provider(props: ColorModeProviderProps) {
const system = createSystem(defaultConfig, {
theme: {
tokens: {}
}
});
return (
<ChakraProvider value={defaultSystem}>
<ChakraProvider value={system}>
<Theme colorPalette="cyan">
<ColorModeProvider defaultTheme="dark">{props.children}</ColorModeProvider>
</Theme>

View file

@ -2,6 +2,7 @@ import { gql } from '@apollo/client';
export const sessionFragment = gql`
fragment Session on Session {
userId
accessToken
}
`;

View file

@ -1 +1,26 @@
import { WorkingProgressStatus } from '@generated/types';
export function assertUnreachable(value: never): never {
throw new Error(`Unhandled scenario: ${value}`);
}
export function toBgColorByProgressStatus(value: WorkingProgressStatus): string {
if (value === WorkingProgressStatus.InProgress) return 'yellow.500';
if (value === WorkingProgressStatus.Frozen) return 'blue.500';
if (value === WorkingProgressStatus.Finished) return 'green.500';
assertUnreachable(value);
}
export function toFgColorByProgressStatus(value: WorkingProgressStatus): string {
if (value === WorkingProgressStatus.InProgress) return 'fg.warning';
if (value === WorkingProgressStatus.Frozen) return 'fg.info';
if (value === WorkingProgressStatus.Finished) return 'fg.success';
assertUnreachable(value);
}
export function toWorkingProgressStatusLabel(value: WorkingProgressStatus): string {
if (value === WorkingProgressStatus.InProgress) return 'In progress';
if (value === WorkingProgressStatus.Frozen) return 'Frozen';
if (value === WorkingProgressStatus.Finished) return 'Finished';
assertUnreachable(value);
}

View file

@ -31,7 +31,7 @@ export function useAuth(): UseAuthOutput {
successMsg: 'Logged out successfully.'
});
if (!authContext) throw new Error('useAuth must be used within an AuthProvider');
if (authContext == null) throw new Error('useAuth must be used within an AuthProvider');
return {
...authContext,
@ -45,7 +45,6 @@ export function useAuth(): UseAuthOutput {
}
}
});
if (data != null) {
authContext.setHeaders(data.login);
return data.login;

View file

@ -1,3 +1,52 @@
// interface UseIdentityProps {}
import { ApolloError, ApolloQueryResult, gql, OperationVariables, useQuery } from '@apollo/client';
import { Identity, IdentityRootQuery } from '@generated/types';
import { createContext, useContext } from 'react';
// export function useIdentity(props: UseIdentityProps) {}
export interface IdentityContext {
data: Identity;
refetch: () => void;
}
export const IdentityContext = createContext<IdentityContext | null>(null);
export const IdentityProvider = IdentityContext.Provider;
export function useIdentity(): IdentityContext | null {
const identityContext = useContext(IdentityContext);
if (identityContext == null) return null;
return identityContext;
}
export function useIdentityQuery({ skip }: { skip?: boolean }): {
data: IdentityRootQuery | undefined;
loading: boolean;
error: ApolloError | undefined;
refetch: (
variables?: Partial<OperationVariables> | undefined
) => Promise<ApolloQueryResult<IdentityRootQuery>>;
} {
const { data, loading, error, refetch } = useQuery<IdentityRootQuery>(query, {
skip
});
return {
data,
loading,
error,
refetch
};
}
const query = gql`
query IdentityRoot {
identity {
id
email
isDarkTheme
user {
id
username
createdAt
updatedAt
}
}
}
`;

View file

@ -0,0 +1,40 @@
import {
ApolloError,
ApolloQueryResult,
MaybeMasked,
OperationVariables,
useQuery
} from '@apollo/client';
import { DocumentNode } from 'graphql';
interface UsePaginatedQueryProps {
query: DocumentNode;
input: {
filter: {
terms?: string | null;
offset?: number | null;
size?: number | null;
};
};
}
interface UsePaginatedQueryReturn<T> {
data?: MaybeMasked<T> | undefined;
loading: boolean;
error: ApolloError | undefined;
refetch: (
variables?: Partial<OperationVariables> | undefined
) => Promise<ApolloQueryResult<MaybeMasked<T>>>;
}
export function usePaginatedQuery<T>(props: UsePaginatedQueryProps): UsePaginatedQueryReturn<T> {
const { data, loading, error, refetch } = useQuery<T>(props.query, {
variables: { input: props.input }
});
return {
data,
loading,
error,
refetch
};
}

View file

@ -0,0 +1,33 @@
import { ApolloError, ApolloQueryResult, gql, OperationVariables, useQuery } from '@apollo/client';
import { UserOneQuery } from '@generated/types';
export function useUserOneQuery({ id, skip }: { id?: string; skip?: boolean }): {
data: UserOneQuery | undefined;
loading: boolean;
error: ApolloError | undefined;
refetch: (
variables?: Partial<OperationVariables> | undefined
) => Promise<ApolloQueryResult<UserOneQuery>>;
} {
const { data, loading, error, refetch } = useQuery<UserOneQuery>(query, {
variables: { id },
skip
});
return {
data,
loading,
error,
refetch
};
}
const query = gql`
query UserOne($id: ID!) {
userOne(id: $id) {
id
username
createdAt
updatedAt
}
}
`;

View file

@ -1,7 +1,7 @@
import { ApolloClient, ApolloProvider, HttpLink, InMemoryCache, from } from '@apollo/client';
import { loadDevMessages, loadErrorMessages } from '@apollo/client/dev';
import { onError } from '@apollo/client/link/error';
import { Skeleton } from '@chakra-ui/react';
import { Box, Center, Spinner } from '@chakra-ui/react';
import '@client/assets/styles/index.css';
import '@client/assets/styles/nerd-fonts-icons.css';
import { Provider as ChakraProvider } from '@client/components/ui/provider';
@ -10,6 +10,7 @@ import { RouterProvider, createRouter } from '@tanstack/react-router';
import { StrictMode, useEffect, useState } from 'react';
import ReactDOM from 'react-dom/client';
import { AuthProvider, useRestAuth } from './hooks/auth';
import { IdentityProvider, useIdentityQuery } from './hooks/identity';
// TanStackRouter - Import the generated route tree
import { Session } from '@generated/types';
@ -59,7 +60,11 @@ function App() {
return (
<StrictMode>
<ChakraProvider>
<Skeleton />
<Box pos="absolute" inset="0">
<Center h="full">
<Spinner color="colorPalette.500" />
</Center>
</Box>
</ChakraProvider>
</StrictMode>
);
@ -113,7 +118,7 @@ function App() {
<ApolloProvider client={client}>
<ChakraProvider>
<Toaster />
<RouterProvider router={router} context={{ isAuthenticated: authHeaders != null }} />
<RouterProviderWrapper authHeaders={authHeaders} />
</ChakraProvider>
</ApolloProvider>
</AuthProvider>
@ -121,6 +126,28 @@ function App() {
);
}
function RouterProviderWrapper({ authHeaders }: { authHeaders: Session | null }) {
const userId = authHeaders?.userId;
const { data, loading, refetch } = useIdentityQuery({ skip: userId == null });
if (loading)
return (
<Box pos="absolute" inset="0">
<Center h="full">
<Spinner color="colorPalette.500" />
</Center>
</Box>
);
return (
<IdentityProvider
value={userId != null && data != null ? { data: data.identity, refetch } : null}
>
<RouterProvider router={router} context={{ isAuthenticated: authHeaders != null }} />
</IdentityProvider>
);
}
const rootElement = document.getElementById('root')!;
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);

View file

@ -1,12 +1,13 @@
import { Separator, Icon, Menu, Stack, Text } from '@chakra-ui/react';
import { Icon, Input, InputGroup, Kbd, Menu, Separator, Stack, Text } from '@chakra-ui/react';
import { createRootRouteWithContext, Link, Outlet } from '@tanstack/react-router';
import React from 'react';
import { Avatar } from '../components/ui/avatar';
import { Button } from '../components/ui/button';
import { ColorModeButton } from '../components/ui/color-mode';
import { Tooltip } from '../components/ui/tooltip';
import { useAuth } from '../hooks/auth';
import { useIdentity } from '../hooks/identity';
import { appName } from '../main';
import { ColorModeButton } from '../components/ui/color-mode';
const TanStackRouterDevtools =
process.env.NODE_ENV === 'production'
@ -26,48 +27,82 @@ interface RouterContext {
export const Route = createRootRouteWithContext<RouterContext>()({
component: () => {
const { isAuthenticated, logout } = useAuth();
const { logout } = useAuth();
const identity = useIdentity();
return (
<Stack padding={3}>
<Stack flexDirection="row" justifyContent="space-between" alignItems="center">
<Stack flexDirection="row" alignItems="center" gap={4}>
<Stack flexDirection="row" justifyContent="center" alignItems="center" gap={4}>
<Link to="/">
<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>
<Stack direction="row" justifyContent="center" alignItems="center" gap={1}>
<Icon fontSize={24} color="colorPalette.500">
<i className="nf nf-md-bird" />
</Icon>
<Text fontSize="lg" fontWeight="bold">
{appName}
</Text>
</Stack>
</Link>
{/* <Stack hideBelow="md"> */}
{/* <Link to="/about"> */}
{/* <Icon color="colorPalette.500" fontSize="md"> */}
{/* <i className="nf nf-oct-question" /> */}
{/* </Icon> */}
{/* </Link> */}
{/* </Stack> */}
</Stack>
<Stack flexDirection="row" alignItems="center" gap={4}>
<InputGroup
hideBelow="md"
flex={1}
alignItems="center"
startElement={
<Icon color="colorPalette.500">
<i className="nf nf-oct-search" />
</Icon>
}
endElement={<Kbd>⌘K</Kbd>}
>
<Input height={8} placeholder="Search works..." />
</InputGroup>
<ColorModeButton />
{isAuthenticated ? (
{identity != null ? (
<Menu.Root>
<Menu.Trigger>
<Menu.Trigger css={{ '&:hover': { cursor: 'pointer' } }}>
<Avatar
// src={'https://avatars.githubusercontent.com/u/40203084?v=4'}
fallback={'A'}
size="sm"
// src="https://avatars.githubusercontent.com/u/40203084?v=4"
/>
</Menu.Trigger>
<Menu.Positioner mt={2}>
<Menu.Content>
<>
<Menu.Item value="logout" onClick={async () => logout()}>
<Tooltip content="Log out">
<Menu.Content backgroundColor={'bg.subtle'}>
<Link to={`/profile/${identity.data.user.username}`}>
<Menu.Item value="/">
<Tooltip content="My profile">
<Stack direction="row">
<Icon color="colorPalette.500" fontSize="xl">
<i className="nf nf-fa-sign_out" />
<i className="nf nf-fa-user" />
</Icon>
Log out
My profile
</Stack>
</Tooltip>
</Menu.Item>
</>
</Link>
<Menu.Item value="logout" onClick={async () => logout()}>
<Tooltip content="Log out">
<Stack direction="row">
<Icon color="colorPalette.500" fontSize="xl">
<i className="nf nf-fa-sign_out" />
</Icon>
Log out
</Stack>
</Tooltip>
</Menu.Item>
</Menu.Content>
</Menu.Positioner>
</Menu.Root>
@ -75,30 +110,13 @@ export const Route = createRootRouteWithContext<RouterContext>()({
<>
<Separator orientation="vertical" height={5} />
<Link to="/login">
<Button>Sign in</Button>
<Button height={8}>Sign in</Button>
</Link>
<Link to="/register">
<Button>Sign up</Button>
<Button height={8}>Sign up</Button>
</Link>
</>
)}
{/* {!isAuthenticated ? (
<>
<Link to="/login">
<Button>Sign in</Button>
</Link>
<Link to="/register">
<Button>Sign up</Button>
</Link>
</>
) : (
<Tooltip content="Log out">
<Icon color="colorPalette.500" fontSize="xl" onClick={async () => logout()}>
<i className="nf nf-fa-sign_out" />
</Icon>
</Tooltip>
)} */}
</Stack>
</Stack>
<Separator style={{ padding: '4px' }} />

View file

@ -0,0 +1,15 @@
import { Stack } from '@chakra-ui/react';
import { createFileRoute } from '@tanstack/react-router';
import { Editor } from '../components/Editor';
export const Route = createFileRoute('/editor')({
component: RouteComponent
});
function RouteComponent() {
return (
<Stack justifyContent="center" alignItems="center">
<Editor />
</Stack>
);
}

View file

@ -0,0 +1,46 @@
import { Box, Button, Stack, Text } from '@chakra-ui/react'
import { createFileRoute } from '@tanstack/react-router'
import { gql, useQuery } from '@apollo/client'
import { BooksManyQuery } from '@generated/types'
export const Route = createFileRoute('/example')({
component: Dashboard,
})
function Dashboard() {
const { data, refetch } = useQuery<BooksManyQuery>(getBooksQuery)
return (
<Stack>
<Text>Hello world!</Text>
<Text>This is my template!</Text>
<Stack>
{data && (
<>
{data.booksMany.map((book: any) => (
<Stack key={book.id}>
<Text>{book.title}</Text>
<Text>{book.author}</Text>
</Stack>
))}
</>
)}
<Box>
<Button onClick={async () => refetch()}>Refetch</Button>
</Box>
</Stack>
</Stack>
)
}
const getBooksQuery = gql`
query BooksMany {
booksMany {
id
author
title
}
}
`

View file

@ -1,46 +1,139 @@
import { Box, Button, Stack, Text } from '@chakra-ui/react';
import { gql } from '@apollo/client';
import { Skeleton, Stack } from '@chakra-ui/react';
import { WorkingListQuery } from '@generated/types';
import { createFileRoute } from '@tanstack/react-router';
import { gql, useQuery } from '@apollo/client';
import { BooksManyQuery } from '@generated/types';
import { useState } from 'react';
import { Pagination } from '../components/Pagination';
import { WorkingList } from '../components/WorkingList';
import { WorkingListSearch } from '../components/WorkingListSearch';
import { usePaginatedQuery } from '../hooks/usePaginatedQuery';
export const Route = createFileRoute('/')({
component: Dashboard
component: RouteComponent
});
function Dashboard() {
const { data, refetch } = useQuery<BooksManyQuery>(getBooksQuery);
function RouteComponent() {
const [workingSearchValue, setWorkingSearchValue] = useState<string>('');
const [workingsListPage, setWorkingsListPage] = useState<number>(1);
const [workingsListPageSize, setWorkingsListPageSize] = useState<number>(2);
const startIndex = (workingsListPage - 1) * workingsListPageSize;
const { data } = usePaginatedQuery<WorkingListQuery>({
query,
input: {
filter: {
terms: workingSearchValue,
offset: startIndex,
size: workingsListPageSize
}
}
});
return (
<Stack>
<Text>Hello world!</Text>
<Text>This is my template!</Text>
<Stack>
{data && (
<Stack justifyContent="center" alignItems="center">
<Stack width="90%">
<WorkingListSearch
value={workingSearchValue}
onSubmit={value => {
alert(value);
}}
onChange={value => setWorkingSearchValue(value)}
/>
{data != null ? (
<>
{data.booksMany.map((book: any) => (
<Stack key={book.id}>
<Text>{book.title}</Text>
<Text>{book.author}</Text>
</Stack>
))}
<WorkingList
workingsList={data.workingMany.items.map(item => ({
...item,
chaptersTotal: item.chapters.total,
reviewsTotal: item.reviews.total
}))}
/>
<Pagination
total={data.workingMany.total}
page={workingsListPage}
setPage={setWorkingsListPage}
pageSize={workingsListPageSize}
/>
</>
) : (
<Skeleton />
)}
<Box>
<Button onClick={async () => refetch()}>Refetch</Button>
</Box>
</Stack>
</Stack>
);
}
const getBooksQuery = gql`
query BooksMany {
booksMany {
id
author
title
const query = gql`
query WorkingList($input: WorkingManyInput!) {
workingMany(input: $input) {
items {
id
title
description
author {
id
username
}
progressStatus
updatedAt
chapters {
total
}
wordsTotal
ratingTotal
reviews {
total
}
}
total
}
}
`;
// const workingData1: WorkingCardProps = {
// id: '1',
// title: 'Worth the Candle',
// description: `A teenager struggling after the death of his best friend finds himself in a fantasy
// world - one which seems to be an amalgamation of every Dungeons and Dragons campaign
// they ever played together. Now he's stuck trying to find the answers to why he's there
// and what this world is trying to say. The most terrifying answer might be that this
// world is an expression of the person he was back on Earth.`,
// author: {
// id: '1',
// createdAt: '12.8.2025',
// updatedAt: '4.9.2025',
// username: 'AlexanderWhales'
// },
// progressStatus: WorkingProgressStatus.Finished,
// updatedAt: '20.10.2025',
// chaptersTotal: 10,
// wordsTotal: 34000,
// ratingTotal: 10,
// reviewsTotal: 3
// };
//
// const workingData2: WorkingCardProps = {
// id: '2',
// title: 'Test',
// description: `Description.`,
// author: {
// id: '1',
// createdAt: '12.8.2025',
// updatedAt: '4.9.2025',
// username: 'JohnSmith'
// },
// progressStatus: WorkingProgressStatus.InProgress,
// updatedAt: '20.10.2025',
// chaptersTotal: 10,
// wordsTotal: 1200,
// ratingTotal: -10,
// reviewsTotal: 0
// };
//
// const mockWorkingData = {
// total: 4,
// items: [workingData1, workingData2, { ...workingData1, id: '3' }, { ...workingData2, id: '4' }]
// };
//

View file

@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/profile/$username')({
component: RouteComponent
});
function RouteComponent() {
const { username } = Route.useParams();
return <div>{`Hello, "${username}"!`}</div>;
}

View file

@ -15,8 +15,11 @@ import { createFileRoute } from '@tanstack/react-router'
import { Route as rootRoute } from './pages/__root'
import { Route as RegisterImport } from './pages/register'
import { Route as LoginImport } from './pages/login'
import { Route as ExampleImport } from './pages/example'
import { Route as EditorImport } from './pages/editor'
import { Route as AboutImport } from './pages/about'
import { Route as IndexImport } from './pages/index'
import { Route as ProfileUsernameImport } from './pages/profile/$username'
// Create Virtual Routes
@ -42,6 +45,18 @@ const LoginRoute = LoginImport.update({
getParentRoute: () => rootRoute,
} as any)
const ExampleRoute = ExampleImport.update({
id: '/example',
path: '/example',
getParentRoute: () => rootRoute,
} as any)
const EditorRoute = EditorImport.update({
id: '/editor',
path: '/editor',
getParentRoute: () => rootRoute,
} as any)
const AboutRoute = AboutImport.update({
id: '/about',
path: '/about',
@ -54,6 +69,12 @@ const IndexRoute = IndexImport.update({
getParentRoute: () => rootRoute,
} as any)
const ProfileUsernameRoute = ProfileUsernameImport.update({
id: '/profile/$username',
path: '/profile/$username',
getParentRoute: () => rootRoute,
} as any)
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
@ -72,6 +93,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AboutImport
parentRoute: typeof rootRoute
}
'/editor': {
id: '/editor'
path: '/editor'
fullPath: '/editor'
preLoaderRoute: typeof EditorImport
parentRoute: typeof rootRoute
}
'/example': {
id: '/example'
path: '/example'
fullPath: '/example'
preLoaderRoute: typeof ExampleImport
parentRoute: typeof rootRoute
}
'/login': {
id: '/login'
path: '/login'
@ -93,6 +128,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof BookLazyImport
parentRoute: typeof rootRoute
}
'/profile/$username': {
id: '/profile/$username'
path: '/profile/$username'
fullPath: '/profile/$username'
preLoaderRoute: typeof ProfileUsernameImport
parentRoute: typeof rootRoute
}
}
}
@ -101,51 +143,91 @@ declare module '@tanstack/react-router' {
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/about': typeof AboutRoute
'/editor': typeof EditorRoute
'/example': typeof ExampleRoute
'/login': typeof LoginRoute
'/register': typeof RegisterRoute
'/book': typeof BookLazyRoute
'/profile/$username': typeof ProfileUsernameRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/about': typeof AboutRoute
'/editor': typeof EditorRoute
'/example': typeof ExampleRoute
'/login': typeof LoginRoute
'/register': typeof RegisterRoute
'/book': typeof BookLazyRoute
'/profile/$username': typeof ProfileUsernameRoute
}
export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexRoute
'/about': typeof AboutRoute
'/editor': typeof EditorRoute
'/example': typeof ExampleRoute
'/login': typeof LoginRoute
'/register': typeof RegisterRoute
'/book': typeof BookLazyRoute
'/profile/$username': typeof ProfileUsernameRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/about' | '/login' | '/register' | '/book'
fullPaths:
| '/'
| '/about'
| '/editor'
| '/example'
| '/login'
| '/register'
| '/book'
| '/profile/$username'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/about' | '/login' | '/register' | '/book'
id: '__root__' | '/' | '/about' | '/login' | '/register' | '/book'
to:
| '/'
| '/about'
| '/editor'
| '/example'
| '/login'
| '/register'
| '/book'
| '/profile/$username'
id:
| '__root__'
| '/'
| '/about'
| '/editor'
| '/example'
| '/login'
| '/register'
| '/book'
| '/profile/$username'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AboutRoute: typeof AboutRoute
EditorRoute: typeof EditorRoute
ExampleRoute: typeof ExampleRoute
LoginRoute: typeof LoginRoute
RegisterRoute: typeof RegisterRoute
BookLazyRoute: typeof BookLazyRoute
ProfileUsernameRoute: typeof ProfileUsernameRoute
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AboutRoute: AboutRoute,
EditorRoute: EditorRoute,
ExampleRoute: ExampleRoute,
LoginRoute: LoginRoute,
RegisterRoute: RegisterRoute,
BookLazyRoute: BookLazyRoute,
ProfileUsernameRoute: ProfileUsernameRoute,
}
export const routeTree = rootRoute
@ -160,9 +242,12 @@ export const routeTree = rootRoute
"children": [
"/",
"/about",
"/editor",
"/example",
"/login",
"/register",
"/book"
"/book",
"/profile/$username"
]
},
"/": {
@ -171,6 +256,12 @@ export const routeTree = rootRoute
"/about": {
"filePath": "about.tsx"
},
"/editor": {
"filePath": "editor.tsx"
},
"/example": {
"filePath": "example.tsx"
},
"/login": {
"filePath": "login.tsx"
},
@ -179,6 +270,9 @@ export const routeTree = rootRoute
},
"/book": {
"filePath": "book.lazy.tsx"
},
"/profile/$username": {
"filePath": "profile/$username.tsx"
}
}
}