Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
66a25a5
refactor: migrate Button to function component
maiano Jul 27, 2025
d69996b
refactor: migrate Input to function component
maiano Jul 27, 2025
653d5e1
refactor: migrate Header and Footer to function component
maiano Jul 27, 2025
e034448
refactor: migrate LoadingOverlay to function component
maiano Jul 27, 2025
a8f1ca7
refactor: migrate Pagination to function component
maiano Jul 27, 2025
2e7edde
refactor: migrate FallBack to function component
maiano Jul 27, 2025
326ace8
refactor: migrate Card to function component
maiano Jul 27, 2025
2216928
refactor: migrate CardList to function component
maiano Jul 27, 2025
3e35c98
refactor: migrate SearchBar to function component
maiano Jul 27, 2025
2311b71
refactor: move error throwing logic to Footer
maiano Jul 27, 2025
fbb58f0
refactor: add MainLayout and stubs for AboutPage and NotFoundPage
maiano Jul 27, 2025
ccbca58
feat: add React Router with basic route configuration
maiano Jul 27, 2025
3e5dc94
test: change test for App component
maiano Jul 27, 2025
cfbad37
refactor: migrate MainPage to function component
maiano Jul 28, 2025
e603859
fix: handling of missing data errors and add an ErrorLayout
maiano Jul 31, 2025
2b46ca4
fix: 404 test to expect empty result
maiano Aug 1, 2025
4f61b94
feat: implement generic hook for typed localStorage
maiano Aug 1, 2025
a4d74f8
refactor: move the routes to a separate module
maiano Aug 1, 2025
999acb9
test: add integration test to App tests
maiano Aug 1, 2025
b3e20f9
feat: sync search and pagination with URL
maiano Aug 1, 2025
9e59271
feat: add navigation links to header
maiano Aug 1, 2025
a36826d
refactor: migrate data fetching to useCharactersQuery
maiano Aug 2, 2025
06843d0
feat: add fetch-character util
maiano Aug 2, 2025
48ddf6b
feat: add useCharacterQuery hook
maiano Aug 2, 2025
ba412fe
feat: implement the CharacterDetails
maiano Aug 3, 2025
bf1e2bc
fix: auto-close character panel if id is not in current results
maiano Aug 3, 2025
9f0d3d0
feat: add variant prop to support details layout
maiano Aug 3, 2025
9da071e
feat: modal layout for CharacterDetails on mobile
maiano Aug 3, 2025
d8dc56d
refactor: move navigation logic from CharacterDetails
maiano Aug 3, 2025
97b62c8
feat: add placeholder message for About page
maiano Aug 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
},
"dependencies": {
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.84.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router": "^7.7.1",
"tailwindcss": "^4.1.11"
},
"devDependencies": {
Expand Down
29 changes: 12 additions & 17 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { RouterProvider, createMemoryRouter } from 'react-router';
import { describe, it, expect } from 'vitest';
import App from './App';
import { ErrorBoundary } from '@/components/ErrorBoundary';
import { routes } from '@/app/routes';
import { UI_STRINGS } from '@/shared/constants/ui-strings';

describe('App component', () => {
it('renders components', () => {
render(<App />);
expect(screen.getByText(UI_STRINGS.title)).toBeInTheDocument();
});

it('throw the error with the button', () => {
render(
<ErrorBoundary fallback={<div>App down</div>}>
<App />
</ErrorBoundary>,
);
it('renders ErrorLayout with Header and NotFoundPage', async () => {
const testRouter = createMemoryRouter(routes, {
initialEntries: ['/error'],
});

const errorButton = screen.getByText(UI_STRINGS.errorButton);
render(<RouterProvider router={testRouter} />);

fireEvent.click(errorButton);
expect(await screen.findByAltText(UI_STRINGS.altLogo)).toBeInTheDocument();

expect(screen.getByText(/App down/i)).toBeInTheDocument();
expect(
screen.getByRole('heading', { name: UI_STRINGS.title }),
).toBeInTheDocument();
});
});
49 changes: 19 additions & 30 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,23 @@
import { Component, type ReactNode } from 'react';
import Footer from '@/components/Footer';
import Header from '@/components/Header';
import { HomePage } from '@/pages/HomePage';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createBrowserRouter, RouterProvider } from 'react-router';
import { routes } from '@/app/routes';

type AppState = {
wouldThrow: boolean;
};
const router = createBrowserRouter(routes);

class App extends Component {
state: AppState = {
wouldThrow: false,
};
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 2 * 60 * 1000,
refetchOnWindowFocus: false,
},
},
});

render(): ReactNode {
const { wouldThrow } = this.state;

if (wouldThrow) {
throw new Error('test error from button');
}

return (
<div className="flex min-h-screen bg-white">
<div className="w-full max-w-7xl flex flex-col bg-gray-100 mx-auto">
<Header />
<HomePage />
<Footer onThrowError={() => this.setState({ wouldThrow: true })} />
</div>
</div>
);
}
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
);
</QueryClientProvider>
);
}

export default App;
6 changes: 6 additions & 0 deletions src/app/paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const PATHS = {
HOME: '/',
ABOUT: '/about',
CHARACTER: '/character',
NOT_FOUND: '*',
} as const;
42 changes: 42 additions & 0 deletions src/app/routes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Navigate } from 'react-router';
import { PATHS } from '@/app/paths';
import { FallBack } from '@/components/FallBack';
import { ErrorLayout } from '@/layouts/ErrorLayout';
import { MainLayout } from '@/layouts/MainLayout';
import { AboutPage } from '@/pages/AboutPage';
import { HomePage } from '@/pages/HomePage';
import { NotFoundPage } from '@/pages/NotFoundPage';

export const routes = [
{
path: '/',
element: <Navigate to={PATHS.CHARACTER} replace />,
},
{
path: '/character',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

почему не используются пути из paths.ts?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

пропустил, когда рефакторил

element: <MainLayout />,
errorElement: <FallBack />,
children: [
{
index: true,
element: <HomePage />,
},
{
path: ':characterId',
element: <HomePage />,
},
],
},
{
path: PATHS.ABOUT,
element: <MainLayout />,
errorElement: <FallBack />,
children: [{ index: true, element: <AboutPage /> }],
},
{
path: PATHS.NOT_FOUND,
element: <ErrorLayout />,
errorElement: <FallBack />,
children: [{ path: PATHS.NOT_FOUND, element: <NotFoundPage /> }],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

зачем здесь дублирование пути?

},
];
56 changes: 27 additions & 29 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,35 @@
import { Component, type ButtonHTMLAttributes } from 'react';
import { type ButtonHTMLAttributes, type ReactNode } from 'react';

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
type ButtonProps = {
variant?: 'default' | 'secondary';
size?: 'sm' | 'default';
className?: string;
}
children?: ReactNode;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

тут children обязателен

} & ButtonHTMLAttributes<HTMLButtonElement>;

export class Button extends Component<ButtonProps> {
render() {
const {
variant = 'default',
size = 'default',
className = '',
...rest
} = this.props;
export const Button = ({
variant = 'default',
size = 'default',
className = '',
children,
...rest
}: ButtonProps) => {
let baseClasses =
'inline-flex items-center justify-center text-sm transition-colors focus:outline-none focus:ring focus:ring-gray-300 disabled:opacity-50 disabled:pointer-events-none';

let baseClasses =
'inline-flex items-center justify-center text-sm transition-colors focus:outline-none focus:ring focus:ring-gray-300 disabled:opacity-50 disabled:pointer-events-none';
let variantClasses =
variant === 'secondary'
? 'bg-gray-300 text-gray-800 hover:bg-gray-400'
: 'bg-gray-200 hover:bg-gray-400';

let variantClasses =
variant === 'secondary'
? 'bg-gray-300 text-gray-800 hover:bg-gray-400'
: 'bg-gray-200 hover:bg-gray-400';
let sizeClasses = size === 'sm' ? 'h-8 w-8 px-4' : 'h-9 px-4 min-sm:px-6';
Comment on lines +17 to +25
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

надо использовать classnames или clsx


let sizeClasses = size === 'sm' ? 'h-8 w-8 px-4' : 'h-9 px-4 min-sm:px-6';

return (
<button
className={`${baseClasses} ${variantClasses} ${sizeClasses} ${className}`}
{...rest}
>
{this.props.children}
</button>
);
}
}
return (
<button
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

надо указывать type кнопки или прокидывать через пропы. по умолчанию type="submit", может привести к неожиданному поведению

className={`${baseClasses} ${variantClasses} ${sizeClasses} ${className}`}
{...rest}
>
{children}
</button>
);
};
Loading