diff --git a/docker-compose.yaml b/docker-compose.yaml index a321395..5c9be67 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,7 @@ version: '3.8' services: client-backend: - image: client:0.0.1 + image: gitea.rmevs.vip/rm/torgi2-backend:latest build: context: . dockerfile: Dockerfile @@ -44,6 +44,19 @@ services: - "5050:80" volumes: - pgadmin-data:/var/lib/pgadmin + profiles: + - tools + + frontend: + image: gitea.rmevs.vip/rm/torgi2-frontend:latest + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - "80:80" + depends_on: + - client-backend + restart: always volumes: db-data: diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..f32186d --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Торги — события + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..413422c --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,22 @@ +{ + "name": "torgi-frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^5.5.3", + "vite": "^5.4.8" + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..35f0ec5 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,10 @@ +import EventsPage from './pages/EventsPage'; + +export default function App() { + return ( +
+

Торги — события

+ +
+ ); +} diff --git a/frontend/src/api.ts b/frontend/src/api.ts new file mode 100644 index 0000000..9e3af0b --- /dev/null +++ b/frontend/src/api.ts @@ -0,0 +1,24 @@ +import type { Page, TorgiEvent } from './types'; + +// В dev-режиме запросы идут через vite-proxy (см. vite.config.ts), +// поэтому BASE по умолчанию пустой. Для продакшна задать VITE_API_URL, +// например https://torgi.example.com +const BASE = import.meta.env.VITE_API_URL ?? ''; + +export interface EventFilter { + cadastral?: string; + status?: string; + page: number; + size: number; +} + +export async function fetchEvents(filter: EventFilter): Promise> { + const q = new URLSearchParams(); + if (filter.cadastral) q.set('cadastral', filter.cadastral); + if (filter.status) q.set('status', filter.status); + q.set('page', String(filter.page)); + q.set('size', String(filter.size)); + const res = await fetch(`${BASE}/api/events?${q.toString()}`); + if (!res.ok) throw new Error(`API error: ${res.status}`); + return (await res.json()) as Page; +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..7cd328f --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,54 @@ +body { + font-family: system-ui, sans-serif; + margin: 0; + background: #f6f6f6; +} + +.container { + max-width: 1100px; + margin: 0 auto; + padding: 16px; +} + +.filters { + display: flex; + gap: 8px; + margin-bottom: 12px; +} + +input, button { + padding: 6px 10px; + font-size: 14px; +} + +table { + width: 100%; + border-collapse: collapse; + background: #fff; +} + +th, td { + border: 1px solid #ddd; + padding: 6px 8px; + text-align: left; + font-size: 14px; +} + +th { + background: #eee; +} + +.pagination { + display: flex; + gap: 12px; + align-items: center; + margin-top: 12px; +} + +.total { + color: #555; +} + +.error { + color: #b00020; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..0582243 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; +import './index.css'; + +const root = document.getElementById('root'); +if (!root) throw new Error('Root element not found'); + +createRoot(root).render( + + + , +); diff --git a/frontend/src/pages/EventsPage.tsx b/frontend/src/pages/EventsPage.tsx new file mode 100644 index 0000000..0acbb36 --- /dev/null +++ b/frontend/src/pages/EventsPage.tsx @@ -0,0 +1,116 @@ +import { useCallback, useEffect, useState } from 'react'; +import { fetchEvents } from '../api'; +import type { Page, TorgiEvent } from '../types'; + +const PAGE_SIZE = 20; + +function cell(value: string | number | null | undefined): string { + if (value === null || value === undefined || value === '') return '—'; + return String(value); +} + +export default function EventsPage() { + const [cadastral, setCadastral] = useState(''); + const [status, setStatus] = useState(''); + const [applied, setApplied] = useState({ cadastral: '', status: '' }); + const [page, setPage] = useState(0); + const [data, setData] = useState | null>(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const load = useCallback(async () => { + setLoading(true); + setError(null); + try { + const result = await fetchEvents({ ...applied, page, size: PAGE_SIZE }); + setData(result); + } catch (e) { + setError(e instanceof Error ? e.message : 'Unknown error'); + } finally { + setLoading(false); + } + }, [applied, page]); + + useEffect(() => { + void load(); + }, [load]); + + const onSearch = () => { + setPage(0); + setApplied({ cadastral: cadastral.trim(), status: status.trim() }); + }; + + return ( +
+
+ setCadastral(e.target.value)} + /> + setStatus(e.target.value)} + /> + +
+ + {loading &&

Загрузка…

} + {error &&

Ошибка: {error}

} + + {data && ( + <> +

Всего: {data.totalElements}

+ + + + + + + + + + + + + + {data.content.map((ev) => ( + + + + + + + + + + ))} + {data.content.length === 0 && ( + + + + )} + +
ИзвещениеЛотКадастрСтатусНач. ценаИтог. ценаДата торгов
{cell(ev.notice)}{cell(ev.lotNumber)}{cell(ev.cadastral)}{cell(ev.status)}{cell(ev.startPrice)}{cell(ev.finalPrice)}{cell(ev.tradesDate)}
Ничего не найдено
+
+ + + Стр. {data.number + 1} из {Math.max(data.totalPages, 1)} + + +
+ + )} +
+ ); +} diff --git a/frontend/src/types.ts b/frontend/src/types.ts new file mode 100644 index 0000000..3005d1f --- /dev/null +++ b/frontend/src/types.ts @@ -0,0 +1,26 @@ +export interface TorgiEvent { + id: number; + key: string | null; + status: string | null; + bidStart: string | null; + bidEnd: string | null; + tradesDate: string | null; + startPrice: string | null; + step: string | null; + initMoney: string | null; + finalPrice: string | null; + notice: string | null; + lotNumber: number | null; + cadastral: string | null; + type: string | null; + fullInfo: string | null; + createdAt: string | null; +} + +export interface Page { + content: T[]; + totalElements: number; + totalPages: number; + number: number; + size: number; +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..a4c834a --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..1492f7c --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// Dev-proxy на бэкенд, чтобы не зависеть от CORS при локальной разработке. +// VITE_API_URL имеет приоритет, если задан (например, для продакшн-сборки). +export default defineConfig({ + plugins: [react()], + server: { + host: '127.0.0.1', + port: 5173, + proxy: { + '/api': { + target: process.env.VITE_API_URL ?? 'http://localhost:8080', + changeOrigin: true, + }, + }, + }, +}); diff --git a/pom.xml b/pom.xml index e5b130b..f975e64 100644 --- a/pom.xml +++ b/pom.xml @@ -83,6 +83,11 @@ org.springframework.retry spring-retry + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.17 + com.fasterxml.jackson.core jackson-databind