diff --git a/tradein-mvp/backend/data/sql/084_brand_praktika_fill.sql b/tradein-mvp/backend/data/sql/084_brand_praktika_fill.sql new file mode 100644 index 00000000..976286c5 --- /dev/null +++ b/tradein-mvp/backend/data/sql/084_brand_praktika_fill.sql @@ -0,0 +1,25 @@ +-- 084_brand_praktika_fill.sql +-- #657 White-label для клиента «Практика» (gk-praktika.ru). +-- +-- ПРОБЛЕМА: seed бренда 'praktika' в 002_core_tables.sql задан заглушкой — +-- primary_color='#e87722' (orange, НЕ из брендбука клиента), logo_url/accent NULL. +-- Реальные фирменные цвета ГК «Практика» (с gk-praktika.ru): +-- primary #235F49 — тёмно-зелёный (основной), +-- accent #1FCECB — циан (акцент). +-- Логотип хотлинкается с сайта клиента (magenta-метка logo-pink.svg). +-- +-- Идемпотентно: UPDATE по PK slug='praktika'. Безопасно ре-применять. +-- +-- NUMBERING: highest на main = 082; PR #656 занимает 083 → используем 084, +-- чтобы избежать коллизии нумерации (см. PR body). + +BEGIN; + +UPDATE brands +SET primary_color = '#235F49', -- тёмно-зелёный (бренд) + accent_color = '#1FCECB', -- циан (акцент) + logo_url = 'https://gk-praktika.ru/images/logo/logo-pink.svg', -- hotlink с сайта клиента + footer_text = 'ГК «Практика» · Екатеринбург' +WHERE slug = 'praktika'; + +COMMIT; diff --git a/tradein-mvp/frontend/src/app/page.tsx b/tradein-mvp/frontend/src/app/page.tsx index 58f4c47c..8580b179 100644 --- a/tradein-mvp/frontend/src/app/page.tsx +++ b/tradein-mvp/frontend/src/app/page.tsx @@ -5,7 +5,7 @@ * С basePath=/trade-in user видит её на gendsgn.ru/trade-in/. * Использует CSS из /components/trade-in/trade-in.css. */ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { useQueryClient } from "@tanstack/react-query"; @@ -29,6 +29,7 @@ import { OfferCard } from "@/components/trade-in/OfferCard"; import { TestPresets } from "@/components/trade-in/TestPresets"; import { useEstimateImvBenchmark, useEstimateHouses } from "@/lib/trade-in-api"; import { useQuota } from "@/lib/useQuota"; +import { useBrand, getActiveBrandSlug } from "@/lib/useBrand"; function useEstimateId() { if (typeof window === "undefined") return null; @@ -41,6 +42,27 @@ export default function TradeInPage() { const queryClient = useQueryClient(); const quota = useQuota(); + // #657 white-label: «отдельная страничка под Практику» = re-skin того же + // UI при ?brand=praktika. Бренд резолвится из ?brand= slug (см. useBrand.ts). + const { data: brand } = useBrand(); + const activeBrandSlug = getActiveBrandSlug(); + + // Переопределяем CSS-переменные палитры в рантайме + вешаем .brand-active + // (переключает --font-sans на Manrope, см. trade-in.css). Cleanup снимает + // overrides → без ?brand= дефолтный UI полностью нетронут (no-op). + useEffect(() => { + const root = document.documentElement; + if (!brand) return; + root.style.setProperty("--accent", brand.primary_color); // зелёный (бренд) + root.style.setProperty("--accent-2", brand.accent_color); // циан (акцент) + root.classList.add("brand-active"); + return () => { + root.style.removeProperty("--accent"); + root.style.removeProperty("--accent-2"); + root.classList.remove("brand-active"); + }; + }, [brand]); + const blocked = quota.data ? !quota.data.unlimited && quota.data.remaining <= 0 : false; @@ -205,7 +227,7 @@ export default function TradeInPage() { - + ) : (
diff --git a/tradein-mvp/frontend/src/components/trade-in/OfferCard.tsx b/tradein-mvp/frontend/src/components/trade-in/OfferCard.tsx index 70052aae..0e8dfdaf 100644 --- a/tradein-mvp/frontend/src/components/trade-in/OfferCard.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/OfferCard.tsx @@ -9,13 +9,18 @@ import { API_BASE_URL } from "@/lib/api"; interface Props { estimate: AggregatedEstimate; + /** #657 white-label: активный ?brand= slug — прокидывается в PDF endpoint. */ + brandSlug?: string | null; } function fmtRub(v: number): string { return Math.round(v).toLocaleString("ru-RU"); } -export function OfferCard({ estimate }: Props) { +export function OfferCard({ estimate, brandSlug }: Props) { + // PDF endpoint (api/v1/trade_in.py) уже honor'ит ?brand= для white-label. + // Без бренда query пустой → стандартный PDF. + const pdfQuery = brandSlug ? `?brand=${encodeURIComponent(brandSlug)}` : ""; const basePrice = estimate.median_price_rub; const baseMln = basePrice / 1_000_000; // Расчёт издержек самостоятельной продажи (от рыночной цены) @@ -175,7 +180,7 @@ export function OfferCard({ estimate }: Props) {
diff --git a/tradein-mvp/frontend/src/components/trade-in/Topbar.tsx b/tradein-mvp/frontend/src/components/trade-in/Topbar.tsx index 63117a55..a2737747 100644 --- a/tradein-mvp/frontend/src/components/trade-in/Topbar.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/Topbar.tsx @@ -1,8 +1,11 @@ "use client"; +/* eslint-disable @next/next/no-img-element -- white-label логотип хотлинком с сайта клиента, next/image не нужен */ + import { UserMenu } from "@/components/auth/UserMenu"; import { API_BASE_URL, HTTPError } from "@/lib/api"; import { isPathAllowed } from "@/lib/isPathAllowed"; +import { useBrand } from "@/lib/useBrand"; import { useMe } from "@/lib/useMe"; type ActiveTab = @@ -72,6 +75,9 @@ const NAV_ITEMS: Array<{ export function Topbar({ active }: TopbarProps) { const { data, error } = useMe(); + // #657 white-label: при активном ?brand= с логотипом рендерим логотип + // клиента вместо «TI»-метки. Без бренда brand === null → стандартный wordmark. + const { data: brand } = useBrand(); // 401 (dev без Caddy basic_auth) — показываем все айтемы. // Если /me ещё грузится или дал ошибку без scope — fallback на все айтемы @@ -88,8 +94,17 @@ export function Topbar({ active }: TopbarProps) {
- TI - Trade-In + {brand?.logo_url ? ( + <> + {brand.name} + {brand.name} + + ) : ( + <> + TI + Trade-In + + )}