feat: establish frontend foundation
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export { App } from "./ui/App";
|
||||
@@ -0,0 +1,81 @@
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
AccountsPageEmpty,
|
||||
AccountsPageError,
|
||||
AccountsPageLoading,
|
||||
} from "../../pages/accounts";
|
||||
import { theme } from "../theme";
|
||||
import { AppRoutes } from "./AppRoutes";
|
||||
|
||||
describe("AppRoutes", () => {
|
||||
afterEach(cleanup);
|
||||
|
||||
it.each([
|
||||
["/", "Счета"],
|
||||
["/login", "Вход"],
|
||||
["/register", "Регистрация"],
|
||||
["/accounts", "Счета"],
|
||||
["/accounts/demo", "Счет"],
|
||||
["/settings", "Настройки"],
|
||||
["/unknown", "Страница не найдена"],
|
||||
])("renders %s with its page title", (path, title) => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<MantineProvider theme={theme}>
|
||||
<AppRoutes />
|
||||
</MantineProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole("heading", { level: 1, name: title }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders only the empty state for accounts by default", () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/accounts"]}>
|
||||
<MantineProvider theme={theme}>
|
||||
<AppRoutes />
|
||||
</MantineProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole("heading", { level: 2, name: "Пока ничего нет" }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText("Загрузка")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Что-то пошло не так")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["loading", AccountsPageLoading, "status"],
|
||||
["error", AccountsPageError, "alert"],
|
||||
["empty", AccountsPageEmpty, undefined],
|
||||
] as const)("renders only the Accounts %s state", (_name, State, role) => {
|
||||
render(
|
||||
<MantineProvider theme={theme}>
|
||||
<State />
|
||||
</MantineProvider>,
|
||||
);
|
||||
|
||||
if (role) {
|
||||
expect(screen.getByRole(role)).toBeInTheDocument();
|
||||
} else {
|
||||
expect(
|
||||
screen.getByRole("heading", { level: 2, name: "Пока ничего нет" }),
|
||||
).toBeInTheDocument();
|
||||
}
|
||||
|
||||
expect(screen.queryAllByRole("status")).toHaveLength(
|
||||
role === "status" ? 1 : 0,
|
||||
);
|
||||
expect(screen.queryAllByRole("alert")).toHaveLength(
|
||||
role === "alert" ? 1 : 0,
|
||||
);
|
||||
expect(screen.queryAllByText("Пока ничего нет")).toHaveLength(role ? 0 : 1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Navigate, Route, Routes } from "react-router";
|
||||
|
||||
import { AccountPage } from "../../pages/account";
|
||||
import { AccountsPage } from "../../pages/accounts";
|
||||
import { LoginPage } from "../../pages/login";
|
||||
import { NotFoundPage } from "../../pages/not-found";
|
||||
import { RegisterPage } from "../../pages/register";
|
||||
import { SettingsPage } from "../../pages/settings";
|
||||
import { AppShell } from "../../shared/ui/AppShell";
|
||||
|
||||
export function AppRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/accounts" replace />} />
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/register" element={<RegisterPage />} />
|
||||
<Route element={<AppShell />}>
|
||||
<Route path="/accounts" element={<AccountsPage />} />
|
||||
<Route path="/accounts/:accountId" element={<AccountPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
</Route>
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { AppRoutes } from "./AppRoutes";
|
||||
@@ -0,0 +1,55 @@
|
||||
:root {
|
||||
--ft-color-bg-page: #e8ecf1;
|
||||
--ft-color-bg-app: #f5f7fa;
|
||||
--ft-color-bg-surface: #ffffff;
|
||||
--ft-color-bg-muted: #f1f5f9;
|
||||
--ft-color-text-primary: #1e293b;
|
||||
--ft-color-text-secondary: #64748b;
|
||||
--ft-color-border: #e2e8f0;
|
||||
--ft-color-accent: #2563eb;
|
||||
--ft-color-danger: #ef4444;
|
||||
--ft-radius-card: 12px;
|
||||
--ft-shadow-card: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--ft-color-bg-page);
|
||||
color: var(--ft-color-text-primary);
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.app-header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
.bottom-nav {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 47.99375em) {
|
||||
.bottom-nav {
|
||||
background: var(--ft-color-bg-surface);
|
||||
border-top: 1px solid var(--ft-color-border);
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
left: 0;
|
||||
padding: 0.75rem;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
z-index: 200;
|
||||
}
|
||||
.bottom-nav .active {
|
||||
color: var(--ft-color-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.mantine-AppShell-main {
|
||||
padding-bottom: 5rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createTheme } from "@mantine/core";
|
||||
|
||||
export const theme = createTheme({
|
||||
primaryColor: "blue",
|
||||
defaultRadius: "md",
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
colors: {
|
||||
blue: [
|
||||
"#eff6ff",
|
||||
"#dbeafe",
|
||||
"#bfdbfe",
|
||||
"#93c5fd",
|
||||
"#60a5fa",
|
||||
"#3b82f6",
|
||||
"#2563eb",
|
||||
"#1d4ed8",
|
||||
"#1e40af",
|
||||
"#1e3a8a",
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { BrowserRouter } from "react-router";
|
||||
|
||||
import { AppRoutes } from "../routes";
|
||||
import { theme } from "../theme";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { refetchOnWindowFocus: false } },
|
||||
});
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<MantineProvider theme={theme} forceColorScheme="light">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<AppRoutes />
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
</MantineProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user