refactor(frontend): SectionLabel + EmptyState + adminStyles shared (fixup)
Per audit batch #127 P2 hygiene (issue #129). Previous commit лошил файлы из-за pre-commit (видимо). Fixup-коммит с actual diff: ## New shared modules (3) - frontend/src/components/ui/SectionLabel.tsx - frontend/src/components/ui/EmptyState.tsx - frontend/src/lib/adminStyles.ts ## Replacements - SectionLabel: 12 inline usages → import (Overview/Land/Market/Environment Tab) - EmptyState: 3 inline usages → import (Land/Market/Environment) - adminStyles: 5 admin pages import cardStyle/labelStyle/inputStyle/th/td - BulkGeoPanel: cardStyle only (preserves local labelStyle для span) - leads/page.tsx: cardStyle с marginTop extend; th/td/inputStyle local (divergent) ## Visual regression: ZERO All px/colors/CSS properties copied verbatim. Files с divergent values left local. ## Checks - ruff (no files) - prettier - tsc + lint clean (per agent run) Closes #129 Refs: #127
This commit is contained in:
parent
6186804eb8
commit
d33023d7d6
13 changed files with 117 additions and 300 deletions
|
|
@ -6,6 +6,9 @@ import { useMemo, useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
|
import { cardStyle as cardStyleBase } from "@/lib/adminStyles";
|
||||||
|
|
||||||
|
const cardStyle = { ...cardStyleBase, marginTop: 16 };
|
||||||
|
|
||||||
const ReactECharts = dynamic(() => import("echarts-for-react"), { ssr: false });
|
const ReactECharts = dynamic(() => import("echarts-for-react"), { ssr: false });
|
||||||
|
|
||||||
|
|
@ -717,13 +720,6 @@ function StatusBadge({ status }: { status: string | null }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cardStyle = {
|
|
||||||
background: "#fff",
|
|
||||||
border: "1px solid #e6e8ec",
|
|
||||||
borderRadius: 12,
|
|
||||||
padding: 20,
|
|
||||||
marginTop: 16,
|
|
||||||
};
|
|
||||||
const inputStyle = {
|
const inputStyle = {
|
||||||
padding: "8px 10px",
|
padding: "8px 10px",
|
||||||
border: "1px solid #d1d5db",
|
border: "1px solid #d1d5db",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
|
import { cardStyle, inputStyle, labelStyle, td, th } from "@/lib/adminStyles";
|
||||||
import { BulkGeoPanel } from "@/components/admin/BulkGeoPanel";
|
import { BulkGeoPanel } from "@/components/admin/BulkGeoPanel";
|
||||||
import { JobSettingsPanel } from "@/components/admin/JobSettingsPanel";
|
import { JobSettingsPanel } from "@/components/admin/JobSettingsPanel";
|
||||||
|
|
||||||
|
|
@ -498,33 +499,11 @@ function Stat({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cardStyle = {
|
|
||||||
background: "#fff",
|
|
||||||
border: "1px solid #e6e8ec",
|
|
||||||
borderRadius: 12,
|
|
||||||
padding: 20,
|
|
||||||
};
|
|
||||||
const sectionTitle = {
|
const sectionTitle = {
|
||||||
margin: "0 0 12px",
|
margin: "0 0 12px",
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: 600 as const,
|
fontWeight: 600 as const,
|
||||||
};
|
};
|
||||||
const labelStyle = {
|
|
||||||
display: "block",
|
|
||||||
fontSize: 12,
|
|
||||||
color: "#5b6066",
|
|
||||||
marginBottom: 4,
|
|
||||||
textTransform: "uppercase" as const,
|
|
||||||
letterSpacing: 0.4,
|
|
||||||
};
|
|
||||||
const inputStyle = {
|
|
||||||
padding: "8px 10px",
|
|
||||||
border: "1px solid #d1d5db",
|
|
||||||
borderRadius: 6,
|
|
||||||
fontSize: 14,
|
|
||||||
width: "100%",
|
|
||||||
boxSizing: "border-box" as const,
|
|
||||||
};
|
|
||||||
const filterTab = {
|
const filterTab = {
|
||||||
padding: "6px 14px",
|
padding: "6px 14px",
|
||||||
border: "none",
|
border: "none",
|
||||||
|
|
@ -547,10 +526,3 @@ const tableStyle = {
|
||||||
borderCollapse: "collapse" as const,
|
borderCollapse: "collapse" as const,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
};
|
};
|
||||||
const th = {
|
|
||||||
padding: "8px 10px",
|
|
||||||
textAlign: "left" as const,
|
|
||||||
fontWeight: 600 as const,
|
|
||||||
borderBottom: "1px solid #e6e8ec",
|
|
||||||
};
|
|
||||||
const td = { padding: "8px 10px" };
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import ScrapeLogsPanel from "@/components/admin/ScrapeLogsPanel";
|
import ScrapeLogsPanel from "@/components/admin/ScrapeLogsPanel";
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
|
import { cardStyle, inputStyle, labelStyle, td, th } from "@/lib/adminStyles";
|
||||||
|
|
||||||
interface GeoJob {
|
interface GeoJob {
|
||||||
job_id: number;
|
job_id: number;
|
||||||
|
|
@ -551,33 +552,11 @@ export default function GeoScrapeAdminPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cardStyle = {
|
|
||||||
background: "#fff",
|
|
||||||
border: "1px solid #e6e8ec",
|
|
||||||
borderRadius: 12,
|
|
||||||
padding: 20,
|
|
||||||
};
|
|
||||||
const sectionTitle = {
|
const sectionTitle = {
|
||||||
margin: "0 0 12px",
|
margin: "0 0 12px",
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: 600 as const,
|
fontWeight: 600 as const,
|
||||||
};
|
};
|
||||||
const labelStyle = {
|
|
||||||
display: "block",
|
|
||||||
fontSize: 12,
|
|
||||||
color: "#5b6066",
|
|
||||||
marginBottom: 4,
|
|
||||||
textTransform: "uppercase" as const,
|
|
||||||
letterSpacing: 0.4,
|
|
||||||
};
|
|
||||||
const inputStyle = {
|
|
||||||
padding: "8px 10px",
|
|
||||||
border: "1px solid #d1d5db",
|
|
||||||
borderRadius: 6,
|
|
||||||
fontSize: 14,
|
|
||||||
width: "100%",
|
|
||||||
boxSizing: "border-box" as const,
|
|
||||||
};
|
|
||||||
const primaryBtn = {
|
const primaryBtn = {
|
||||||
padding: "10px 18px",
|
padding: "10px 18px",
|
||||||
background: "#1d4ed8",
|
background: "#1d4ed8",
|
||||||
|
|
@ -602,10 +581,3 @@ const tableStyle = {
|
||||||
borderCollapse: "collapse" as const,
|
borderCollapse: "collapse" as const,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
};
|
};
|
||||||
const th = {
|
|
||||||
padding: "8px 10px",
|
|
||||||
textAlign: "left" as const,
|
|
||||||
fontWeight: 600 as const,
|
|
||||||
borderBottom: "1px solid #e6e8ec",
|
|
||||||
};
|
|
||||||
const td = { padding: "8px 10px" };
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import ScrapeLogsPanel from "@/components/admin/ScrapeLogsPanel";
|
import ScrapeLogsPanel from "@/components/admin/ScrapeLogsPanel";
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
|
import { cardStyle, inputStyle, labelStyle, td, th } from "@/lib/adminStyles";
|
||||||
|
|
||||||
interface ObjectiveRunRow {
|
interface ObjectiveRunRow {
|
||||||
run_id: number;
|
run_id: number;
|
||||||
|
|
@ -825,33 +826,11 @@ function Stat({ label, value }: { label: string; value: number }) {
|
||||||
|
|
||||||
// ── Styles ──────────────────────────────────────────────────────────────────
|
// ── Styles ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const cardStyle = {
|
|
||||||
background: "#fff",
|
|
||||||
border: "1px solid #e6e8ec",
|
|
||||||
borderRadius: 12,
|
|
||||||
padding: 20,
|
|
||||||
};
|
|
||||||
const sectionTitle = {
|
const sectionTitle = {
|
||||||
margin: "0 0 12px",
|
margin: "0 0 12px",
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: 600 as const,
|
fontWeight: 600 as const,
|
||||||
};
|
};
|
||||||
const labelStyle = {
|
|
||||||
display: "block",
|
|
||||||
fontSize: 12,
|
|
||||||
color: "#5b6066",
|
|
||||||
marginBottom: 4,
|
|
||||||
textTransform: "uppercase" as const,
|
|
||||||
letterSpacing: 0.4,
|
|
||||||
};
|
|
||||||
const inputStyle = {
|
|
||||||
padding: "8px 10px",
|
|
||||||
border: "1px solid #d1d5db",
|
|
||||||
borderRadius: 6,
|
|
||||||
fontSize: 14,
|
|
||||||
width: "100%",
|
|
||||||
boxSizing: "border-box" as const,
|
|
||||||
};
|
|
||||||
const primaryBtn = {
|
const primaryBtn = {
|
||||||
padding: "10px 18px",
|
padding: "10px 18px",
|
||||||
background: "#1d4ed8",
|
background: "#1d4ed8",
|
||||||
|
|
@ -871,10 +850,3 @@ const secondaryBtn = {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
};
|
};
|
||||||
const th = {
|
|
||||||
padding: "8px 10px",
|
|
||||||
textAlign: "left" as const,
|
|
||||||
fontWeight: 600 as const,
|
|
||||||
borderBottom: "1px solid #e6e8ec",
|
|
||||||
};
|
|
||||||
const td = { padding: "8px 10px" };
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useState } from "react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
|
import { cardStyle, inputStyle, labelStyle, td, th } from "@/lib/adminStyles";
|
||||||
|
|
||||||
interface RunRow {
|
interface RunRow {
|
||||||
run_id: number;
|
run_id: number;
|
||||||
|
|
@ -975,28 +976,6 @@ export default function ScrapeAdminPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cardStyle = {
|
|
||||||
background: "#fff",
|
|
||||||
border: "1px solid #e6e8ec",
|
|
||||||
borderRadius: 12,
|
|
||||||
padding: 20,
|
|
||||||
};
|
|
||||||
const labelStyle = {
|
|
||||||
display: "block",
|
|
||||||
fontSize: 12,
|
|
||||||
color: "#5b6066",
|
|
||||||
marginBottom: 4,
|
|
||||||
textTransform: "uppercase" as const,
|
|
||||||
letterSpacing: 0.4,
|
|
||||||
};
|
|
||||||
const inputStyle = {
|
|
||||||
padding: "8px 10px",
|
|
||||||
border: "1px solid #d1d5db",
|
|
||||||
borderRadius: 6,
|
|
||||||
fontSize: 14,
|
|
||||||
width: "100%",
|
|
||||||
boxSizing: "border-box" as const,
|
|
||||||
};
|
|
||||||
const primaryBtn = {
|
const primaryBtn = {
|
||||||
padding: "10px 18px",
|
padding: "10px 18px",
|
||||||
background: "#1d4ed8",
|
background: "#1d4ed8",
|
||||||
|
|
@ -1031,10 +1010,3 @@ const queueTable = {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
};
|
};
|
||||||
const th = {
|
|
||||||
padding: "8px 10px",
|
|
||||||
textAlign: "left" as const,
|
|
||||||
fontWeight: 600,
|
|
||||||
borderBottom: "1px solid #e6e8ec",
|
|
||||||
};
|
|
||||||
const td = { padding: "8px 10px" };
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useState } from "react";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
|
import { cardStyle } from "@/lib/adminStyles";
|
||||||
|
|
||||||
// ── Types ────────────────────────────────────────────────────────────────────
|
// ── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -261,13 +262,6 @@ export function BulkGeoPanel({ token }: { token: string }) {
|
||||||
|
|
||||||
// ── Styles ──────────────────────────────────────────────────────────────────
|
// ── Styles ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const cardStyle = {
|
|
||||||
background: "#fff",
|
|
||||||
border: "1px solid #e6e8ec",
|
|
||||||
borderRadius: 12,
|
|
||||||
padding: 20,
|
|
||||||
};
|
|
||||||
|
|
||||||
const sectionTitle = {
|
const sectionTitle = {
|
||||||
margin: "0 0 8px",
|
margin: "0 0 8px",
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import type {
|
||||||
ParcelAnalysisWind,
|
ParcelAnalysisWind,
|
||||||
ParcelAnalysisWeather,
|
ParcelAnalysisWeather,
|
||||||
} from "@/types/site-finder";
|
} from "@/types/site-finder";
|
||||||
|
import { SectionLabel } from "@/components/ui/SectionLabel";
|
||||||
|
import { EmptyState } from "@/components/ui/EmptyState";
|
||||||
import { SeasonalWeatherBlock } from "./SeasonalWeatherBlock";
|
import { SeasonalWeatherBlock } from "./SeasonalWeatherBlock";
|
||||||
import { HydrologyBlock } from "./HydrologyBlock";
|
import { HydrologyBlock } from "./HydrologyBlock";
|
||||||
|
|
||||||
|
|
@ -424,18 +426,9 @@ export function EnvironmentTab({ data }: Props) {
|
||||||
{/* Primary env grid */}
|
{/* Primary env grid */}
|
||||||
{hasEnv && (
|
{hasEnv && (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<SectionLabel style={{ marginBottom: 12 }}>
|
||||||
style={{
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "#6b7280",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: "0.05em",
|
|
||||||
marginBottom: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Внешние факторы
|
Внешние факторы
|
||||||
</div>
|
</SectionLabel>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
|
|
@ -480,18 +473,9 @@ export function EnvironmentTab({ data }: Props) {
|
||||||
{/* Seasonal weather */}
|
{/* Seasonal weather */}
|
||||||
{"seasonal_weather" in data && (
|
{"seasonal_weather" in data && (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<SectionLabel style={{ marginBottom: 12 }}>
|
||||||
style={{
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "#6b7280",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: "0.05em",
|
|
||||||
marginBottom: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Климат (нормали 30 лет)
|
Климат (нормали 30 лет)
|
||||||
</div>
|
</SectionLabel>
|
||||||
<SeasonalWeatherBlock seasonal={data.seasonal_weather} />
|
<SeasonalWeatherBlock seasonal={data.seasonal_weather} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -499,18 +483,7 @@ export function EnvironmentTab({ data }: Props) {
|
||||||
{/* Hydrology */}
|
{/* Hydrology */}
|
||||||
{data.hydrology !== undefined && (
|
{data.hydrology !== undefined && (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<SectionLabel style={{ marginBottom: 12 }}>Гидрология</SectionLabel>
|
||||||
style={{
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "#6b7280",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: "0.05em",
|
|
||||||
marginBottom: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Гидрология
|
|
||||||
</div>
|
|
||||||
<HydrologyBlock hydrology={data.hydrology} />
|
<HydrologyBlock hydrology={data.hydrology} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -518,16 +491,7 @@ export function EnvironmentTab({ data }: Props) {
|
||||||
{!hasEnv &&
|
{!hasEnv &&
|
||||||
!("seasonal_weather" in data) &&
|
!("seasonal_weather" in data) &&
|
||||||
data.hydrology === undefined && (
|
data.hydrology === undefined && (
|
||||||
<div
|
<EmptyState message="Данные об окружающей среде недоступны" />
|
||||||
style={{
|
|
||||||
fontSize: 13,
|
|
||||||
color: "#9ca3af",
|
|
||||||
padding: "24px 0",
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Данные об окружающей среде недоступны
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { ParcelAnalysis } from "@/types/site-finder";
|
import type { ParcelAnalysis } from "@/types/site-finder";
|
||||||
|
import { SectionLabel } from "@/components/ui/SectionLabel";
|
||||||
|
import { EmptyState } from "@/components/ui/EmptyState";
|
||||||
import { GeologyBlock } from "./GeologyBlock";
|
import { GeologyBlock } from "./GeologyBlock";
|
||||||
import { GeometrySuitabilityBlock } from "./GeometrySuitabilityBlock";
|
import { GeometrySuitabilityBlock } from "./GeometrySuitabilityBlock";
|
||||||
import { GeotechRiskBlock } from "./GeotechRiskBlock";
|
import { GeotechRiskBlock } from "./GeotechRiskBlock";
|
||||||
|
|
@ -38,18 +40,9 @@ export function LandTab({ data }: Props) {
|
||||||
padding: "14px 18px",
|
padding: "14px 18px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<SectionLabel style={{ marginBottom: 8 }}>
|
||||||
style={{
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "#6b7280",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: "0.05em",
|
|
||||||
marginBottom: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Зонирование (ПЗЗ)
|
Зонирование (ПЗЗ)
|
||||||
</div>
|
</SectionLabel>
|
||||||
<div style={{ fontSize: 13, color: "#6b7280" }}>
|
<div style={{ fontSize: 13, color: "#6b7280" }}>
|
||||||
Данные ПЗЗ доступны через{" "}
|
Данные ПЗЗ доступны через{" "}
|
||||||
<a
|
<a
|
||||||
|
|
@ -86,18 +79,9 @@ export function LandTab({ data }: Props) {
|
||||||
{/* Geotech risk */}
|
{/* Geotech risk */}
|
||||||
{data.geotech_risk !== undefined && (
|
{data.geotech_risk !== undefined && (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<SectionLabel style={{ marginBottom: 12 }}>
|
||||||
style={{
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "#6b7280",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: "0.05em",
|
|
||||||
marginBottom: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Геотехнический риск
|
Геотехнический риск
|
||||||
</div>
|
</SectionLabel>
|
||||||
<GeotechRiskBlock risk={data.geotech_risk} />
|
<GeotechRiskBlock risk={data.geotech_risk} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -105,34 +89,12 @@ export function LandTab({ data }: Props) {
|
||||||
{/* Geology */}
|
{/* Geology */}
|
||||||
{data.geology !== undefined && (
|
{data.geology !== undefined && (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<SectionLabel style={{ marginBottom: 12 }}>Геология</SectionLabel>
|
||||||
style={{
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "#6b7280",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: "0.05em",
|
|
||||||
marginBottom: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Геология
|
|
||||||
</div>
|
|
||||||
<GeologyBlock geology={data.geology} />
|
<GeologyBlock geology={data.geology} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!hasAny && (
|
{!hasAny && <EmptyState message="Данные о земле и геологии недоступны" />}
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: 13,
|
|
||||||
color: "#9ca3af",
|
|
||||||
padding: "24px 0",
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Данные о земле и геологии недоступны
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { ParcelAnalysis } from "@/types/site-finder";
|
import type { ParcelAnalysis } from "@/types/site-finder";
|
||||||
|
import { SectionLabel } from "@/components/ui/SectionLabel";
|
||||||
|
import { EmptyState } from "@/components/ui/EmptyState";
|
||||||
import { MarketTrendBlock } from "./MarketTrendBlock";
|
import { MarketTrendBlock } from "./MarketTrendBlock";
|
||||||
import { CompetitorTable } from "./CompetitorTable";
|
import { CompetitorTable } from "./CompetitorTable";
|
||||||
import { Pipeline24moBlock } from "./Pipeline24moBlock";
|
import { Pipeline24moBlock } from "./Pipeline24moBlock";
|
||||||
|
|
@ -39,18 +41,7 @@ export function MarketTab({ data }: Props) {
|
||||||
{/* Competitors */}
|
{/* Competitors */}
|
||||||
{data.competitors.length > 0 && (
|
{data.competitors.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<SectionLabel style={{ marginBottom: 12 }}>Конкуренты</SectionLabel>
|
||||||
style={{
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "#6b7280",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: "0.05em",
|
|
||||||
marginBottom: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Конкуренты
|
|
||||||
</div>
|
|
||||||
<CompetitorTable
|
<CompetitorTable
|
||||||
competitors={data.competitors}
|
competitors={data.competitors}
|
||||||
districtName={data.district?.district_name}
|
districtName={data.district?.district_name}
|
||||||
|
|
@ -61,36 +52,16 @@ export function MarketTab({ data }: Props) {
|
||||||
{/* Success recommendation */}
|
{/* Success recommendation */}
|
||||||
{hasRecommendation && (
|
{hasRecommendation && (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<SectionLabel style={{ marginBottom: 12 }}>
|
||||||
style={{
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "#6b7280",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: "0.05em",
|
|
||||||
marginBottom: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Что хорошо продаётся
|
Что хорошо продаётся
|
||||||
</div>
|
</SectionLabel>
|
||||||
<SuccessRecommendationBlock
|
<SuccessRecommendationBlock
|
||||||
recommendation={data.success_recommendation}
|
recommendation={data.success_recommendation}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!hasAny && (
|
{!hasAny && <EmptyState message="Рыночные данные недоступны" />}
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: 13,
|
|
||||||
color: "#9ca3af",
|
|
||||||
padding: "24px 0",
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Рыночные данные недоступны
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import type { FeatureCollection } from "geojson";
|
import type { FeatureCollection } from "geojson";
|
||||||
import type { ParcelAnalysis } from "@/types/site-finder";
|
import type { ParcelAnalysis } from "@/types/site-finder";
|
||||||
|
import { SectionLabel } from "@/components/ui/SectionLabel";
|
||||||
import { ConfidenceBadge } from "./ConfidenceBadge";
|
import { ConfidenceBadge } from "./ConfidenceBadge";
|
||||||
import { IsochronesPanel } from "./IsochronesPanel";
|
import { IsochronesPanel } from "./IsochronesPanel";
|
||||||
import { ScoreBreakdownPanel } from "./ScoreBreakdownPanel";
|
import { ScoreBreakdownPanel } from "./ScoreBreakdownPanel";
|
||||||
|
|
@ -62,17 +63,7 @@ export function OverviewTab({ data, onIsochronesResult }: Props) {
|
||||||
gap: 6,
|
gap: 6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<SectionLabel>Район</SectionLabel>
|
||||||
style={{
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "#6b7280",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: "0.05em",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Район
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 16, fontWeight: 700, color: "#111827" }}>
|
<div style={{ fontSize: 16, fontWeight: 700, color: "#111827" }}>
|
||||||
{data.district.district_name}
|
{data.district.district_name}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -138,18 +129,7 @@ export function OverviewTab({ data, onIsochronesResult }: Props) {
|
||||||
gap: 4,
|
gap: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<SectionLabel style={{ marginBottom: 6 }}>Балл</SectionLabel>
|
||||||
style={{
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "#6b7280",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: "0.05em",
|
|
||||||
marginBottom: 6,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Балл
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 13, color: "#6b7280" }}>
|
<div style={{ fontSize: 13, color: "#6b7280" }}>
|
||||||
{data.poi_count} POI ·{" "}
|
{data.poi_count} POI ·{" "}
|
||||||
{data.source === "cad_quarter" ? "квартал" : "участок"}
|
{data.source === "cad_quarter" ? "квартал" : "участок"}
|
||||||
|
|
@ -186,18 +166,9 @@ export function OverviewTab({ data, onIsochronesResult }: Props) {
|
||||||
background: "#fff",
|
background: "#fff",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<SectionLabel style={{ marginBottom: 12 }}>
|
||||||
style={{
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "#6b7280",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: "0.05em",
|
|
||||||
marginBottom: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
POI по категориям
|
POI по категориям
|
||||||
</div>
|
</SectionLabel>
|
||||||
{Object.entries(data.score_breakdown).length === 0 ? (
|
{Object.entries(data.score_breakdown).length === 0 ? (
|
||||||
<p style={{ fontSize: 13, color: "#9ca3af" }}>Нет данных</p>
|
<p style={{ fontSize: 13, color: "#9ca3af" }}>Нет данных</p>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -236,18 +207,9 @@ export function OverviewTab({ data, onIsochronesResult }: Props) {
|
||||||
background: "#f9fafb",
|
background: "#f9fafb",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<SectionLabel style={{ marginBottom: 6 }}>
|
||||||
style={{
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "#6b7280",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: "0.05em",
|
|
||||||
marginBottom: 6,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Изохроны доступности
|
Изохроны доступности
|
||||||
</div>
|
</SectionLabel>
|
||||||
<div style={{ fontSize: 12, color: "#9ca3af" }}>
|
<div style={{ fontSize: 12, color: "#9ca3af" }}>
|
||||||
Недоступны — нужен OpenRouteService API key (см.{" "}
|
Недоступны — нужен OpenRouteService API key (см.{" "}
|
||||||
<code style={{ fontFamily: "monospace" }}>
|
<code style={{ fontFamily: "monospace" }}>
|
||||||
|
|
|
||||||
22
frontend/src/components/ui/EmptyState.tsx
Normal file
22
frontend/src/components/ui/EmptyState.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import type { CSSProperties } from "react";
|
||||||
|
|
||||||
|
interface EmptyStateProps {
|
||||||
|
message: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmptyState({ message, style }: EmptyStateProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
color: "#9ca3af",
|
||||||
|
padding: "24px 0",
|
||||||
|
textAlign: "center",
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
frontend/src/components/ui/SectionLabel.tsx
Normal file
23
frontend/src/components/ui/SectionLabel.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import type { CSSProperties, ReactNode } from "react";
|
||||||
|
|
||||||
|
interface SectionLabelProps {
|
||||||
|
children: ReactNode;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SectionLabel({ children, style }: SectionLabelProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "#6b7280",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: "0.05em",
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
frontend/src/lib/adminStyles.ts
Normal file
35
frontend/src/lib/adminStyles.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import type { CSSProperties } from "react";
|
||||||
|
|
||||||
|
export const cardStyle: CSSProperties = {
|
||||||
|
background: "#fff",
|
||||||
|
border: "1px solid #e6e8ec",
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 20,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const labelStyle: CSSProperties = {
|
||||||
|
display: "block",
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#5b6066",
|
||||||
|
marginBottom: 4,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: 0.4,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const inputStyle: CSSProperties = {
|
||||||
|
padding: "8px 10px",
|
||||||
|
border: "1px solid #d1d5db",
|
||||||
|
borderRadius: 6,
|
||||||
|
fontSize: 14,
|
||||||
|
width: "100%",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const th: CSSProperties = {
|
||||||
|
padding: "8px 10px",
|
||||||
|
textAlign: "left",
|
||||||
|
fontWeight: 600,
|
||||||
|
borderBottom: "1px solid #e6e8ec",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const td: CSSProperties = { padding: "8px 10px" };
|
||||||
Loading…
Add table
Reference in a new issue