From 4f21af5f800f30d7a534932de3fb80515850bb83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Zatorski?= Date: Mon, 28 Sep 2026 07:43:55 +0200 Subject: [PATCH] Grafika strony glownej z biblioteki mediow, naglowek w panelu GRAFIKA JAKO USTAWIENIE, NIE PLIK W REPOZYTORIUM Nowe `ACADEMY_HERO_IMAGE_MEDIA_ID` (typ MEDIA) wskazuje zdjecie obok naglowka strony glownej. Zdjecie jest materialem konkretnego klienta, zwykle z jego logo, a model wdrozenia zabrania danych klienta w obrazie Docker: wbudowane w kod pojawiloby sie na stronie kazdej kolejnej instancji. Ta sama regula co przy logo i ikonie karty. Plik idzie przez `/api/public/media/{id}`, jak obrazki w tresci strony. Pusta albo nieliczbowa wartosc daje domyslny panel z motywem wodnym zamiast adresu donikad: zepsuty obrazek w najbardziej widocznym miejscu strony wyglada gorzej niz motyw domyslny. Ramka dostaje proporcje zdjecia (16:9) zamiast sztywnej wysokosci: przy innym ksztalcie `object-cover` obcinal brzegi, a tam zwykle stoi logo. NAGLOWEK STRONY GLOWNEJ W PANELU `ACADEMY_HERO_EYEBROW`, `_TITLE` i `_TEXT` istnialy w backendzie od poczatku, ale nie bylo ich w panelu: tytul strony dalo sie zmienic wylacznie zapytaniem do bazy. Ustawienia -> Strona glowna ma teraz karte z tymi polami i grafika. ODMIANA LICZNIKOW Etykiety licznikow mialy na sztywno forme dla "wielu", wiec na zywej stronie widnialo "1 kursantow". Licznik jest pierwsza rzecza, ktora widzi gosc. Uzyta istniejaca funkcja `plural` z `lib/dates.ts`, teraz wyeksportowana, zamiast czwartej kopii tej samej funkcji. Co-Authored-By: Claude Opus 5.5 --- .../webinar/academy/PublicAcademyDtos.java | 2 ++ .../webinar/academy/PublicAcademyService.java | 18 ++++++++++ .../softspm/webinar/settings/SettingKey.java | 14 ++++++++ frontend/src/academy/AcademyHomePage.tsx | 35 ++++++++++++++++--- frontend/src/api/types.ts | 2 ++ frontend/src/lib/dates.ts | 2 +- .../pages/settings/HomePageSettingsPage.tsx | 30 ++++++++++++++++ 7 files changed, 97 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/com/softspm/webinar/academy/PublicAcademyDtos.java b/backend/src/main/java/com/softspm/webinar/academy/PublicAcademyDtos.java index e038b3a..e697929 100644 --- a/backend/src/main/java/com/softspm/webinar/academy/PublicAcademyDtos.java +++ b/backend/src/main/java/com/softspm/webinar/academy/PublicAcademyDtos.java @@ -215,6 +215,8 @@ public final class PublicAcademyDtos { String heroEyebrow, String heroTitle, String heroText, + /** Adres grafiki obok naglowka albo null, gdy strona ma domyslny motyw. */ + String heroImageUrl, String expertsTitle, String expertsText, String reviewsTitle, diff --git a/backend/src/main/java/com/softspm/webinar/academy/PublicAcademyService.java b/backend/src/main/java/com/softspm/webinar/academy/PublicAcademyService.java index 0d99adc..4d7a89f 100644 --- a/backend/src/main/java/com/softspm/webinar/academy/PublicAcademyService.java +++ b/backend/src/main/java/com/softspm/webinar/academy/PublicAcademyService.java @@ -122,6 +122,7 @@ public class PublicAcademyService { settings.get(SettingKey.ACADEMY_HERO_EYEBROW), settings.get(SettingKey.ACADEMY_HERO_TITLE), settings.get(SettingKey.ACADEMY_HERO_TEXT), + heroImageUrl(), settings.get(SettingKey.ACADEMY_EXPERTS_TITLE), settings.get(SettingKey.ACADEMY_EXPERTS_TEXT), settings.get(SettingKey.ACADEMY_REVIEWS_TITLE), @@ -137,6 +138,23 @@ public class PublicAcademyService { sections.getOrDefault(PageSectionType.FAQ, List.of())); } + /** + * Adres grafiki obok naglowka. + * + * Plik idzie przez `/api/public/media/{id}`, jak obrazki w tresci strony: + * klucz obiektu w MinIO nie opuszcza serwera, a typ pochodzi z bazy. + * Wartosc, ktora nie jest liczba, daje null zamiast adresu donikad: + * zepsuty obrazek w najbardziej widocznym miejscu strony wyglada gorzej + * niz domyslny motyw. + */ + private String heroImageUrl() { + String id = settings.get(SettingKey.ACADEMY_HERO_IMAGE_MEDIA_ID); + if (id == null || !id.trim().matches("\\d+")) { + return null; + } + return "/api/public/media/" + id.trim(); + } + /** * Liczby w naglowku. * diff --git a/backend/src/main/java/com/softspm/webinar/settings/SettingKey.java b/backend/src/main/java/com/softspm/webinar/settings/SettingKey.java index 8f419cc..f2d0498 100644 --- a/backend/src/main/java/com/softspm/webinar/settings/SettingKey.java +++ b/backend/src/main/java/com/softspm/webinar/settings/SettingKey.java @@ -413,6 +413,20 @@ public enum SettingKey { "Kursy online i otwarte webinary z ekspertami. Jedno konto, certyfikaty, " + "dostęp z dowolnego urządzenia.", "Zdanie pod nagłówkiem strony głównej Akademii"), + /** + * Grafika obok naglowka strony glownej Akademii. + * + * Identyfikator pliku z biblioteki mediow, nie adres i nie plik w repozytorium. + * Zdjecie jest zwykle materialem konkretnego klienta (z jego logo), a model + * wdrozenia zabrania danych klienta w obrazie Docker: wbudowane w kod + * pojawiloby sie na stronie kazdej kolejnej instancji. Ta sama regula co przy + * logo i ikonie karty. + * + * Puste oznacza domyslny panel z motywem wodnym, co jest poprawnym stanem. + */ + ACADEMY_HERO_IMAGE_MEDIA_ID(SettingType.MEDIA, "", + "Grafika obok nagłówka strony głównej Akademii. Puste pokazuje domyślny motyw.", + Requirement.OPTIONAL_EMPTY), ACADEMY_EXPERTS_TITLE(SettingType.STRING, "Uczysz się od praktyków", "Nagłówek sekcji z prowadzącymi. Same osoby dodaje się w zakładce Strona główna."), ACADEMY_EXPERTS_TEXT(SettingType.STRING, "", diff --git a/frontend/src/academy/AcademyHomePage.tsx b/frontend/src/academy/AcademyHomePage.tsx index 634b805..01d0b5e 100644 --- a/frontend/src/academy/AcademyHomePage.tsx +++ b/frontend/src/academy/AcademyHomePage.tsx @@ -5,6 +5,7 @@ import { publicClient } from '../api/publicClient' import type { AcademyHome, SectionItem } from '../api/types' import { Icon } from '../components/Icon' import { Button, ErrorState, Spinner } from '../components/ui' +import { plural } from '../lib/dates' import { useDocumentMeta } from '../lib/useDocumentMeta' import { AcademyLayout } from './AcademyLayout' import { CourseCard, formatPrice, mediaUrl } from './AcademyBits' @@ -90,12 +91,18 @@ export function AcademyHomePage() { * przypadkowa, a przy zerze zdanych bylaby zniechecajaca. Wroci, gdy bedzie * co uśredniać. */ + /* + * Etykiety odmieniane po liczbie. Stala forma "kursantow" dawala na zywej + * stronie "1 kursantow": licznik jest pierwsza rzecza, ktora widzi gosc, + * i blad gramatyczny w tym miejscu podwaza wiarygodnosc calej strony. + */ + const s = data.stats const visibleStats = [ - { value: data.stats.hours, label: 'godzin materiału' }, - { value: data.stats.students, label: 'kursantów' }, - { value: data.stats.completed, label: 'ukończonych szkoleń' }, - { value: data.stats.courses, label: 'kursów online' }, - ].filter((s) => s.value > 0) + { value: s.hours, label: plural(s.hours, 'godzina materiału', 'godziny materiału', 'godzin materiału') }, + { value: s.students, label: plural(s.students, 'kursant', 'kursantów', 'kursantów') }, + { value: s.completed, label: plural(s.completed, 'ukończone szkolenie', 'ukończone szkolenia', 'ukończonych szkoleń') }, + { value: s.courses, label: plural(s.courses, 'kurs online', 'kursy online', 'kursów online') }, + ].filter((stat) => stat.value > 0) /** Kursy platne do sekcji cennika. Brak platnych ukrywa cala sekcje. */ const paidCourses = data.courses.filter((c) => !c.free) @@ -159,6 +166,23 @@ export function AcademyHomePage() { jedna pod druga, a panel schodzi pod tresc: `order` zostawiamy domyslny, bo tresc jest wazniejsza od ozdoby. */} + {data.heroImageUrl ? ( + /* + * Grafika z ustawien (biblioteka mediow), a nie z repozytorium: + * to material konkretnego klienta. Proporcje brane z samego + * zdjecia (16:9), a nie sztywna wysokosc, bo `object-cover` przy + * innym ksztalcie ramki obcinal brzegi, gdzie zwykle stoi logo. + * Opis pusty: zdjecie jest ozdoba, tresc niesie naglowek obok. + */ +
+ +
+ ) : ( + )} diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index d3ea97c..240a092 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -1014,6 +1014,8 @@ export type AcademyHome = { heroEyebrow: string heroTitle: string heroText: string + /** Adres grafiki obok naglowka; null oznacza domyslny motyw wodny. */ + heroImageUrl: string | null expertsTitle: string expertsText: string reviewsTitle: string diff --git a/frontend/src/lib/dates.ts b/frontend/src/lib/dates.ts index 63f3dbd..dfe6470 100644 --- a/frontend/src/lib/dates.ts +++ b/frontend/src/lib/dates.ts @@ -171,7 +171,7 @@ export function formatDuration(startIso: string, endIso: string): string { } /** Odmiana rzeczownika po liczbie, wedlug regul polskich. */ -function plural(count: number, one: string, few: string, many: string): string { +export function plural(count: number, one: string, few: string, many: string): string { if (count === 1) return one const lastTwo = count % 100 const last = count % 10 diff --git a/frontend/src/pages/settings/HomePageSettingsPage.tsx b/frontend/src/pages/settings/HomePageSettingsPage.tsx index 39008eb..b588a91 100644 --- a/frontend/src/pages/settings/HomePageSettingsPage.tsx +++ b/frontend/src/pages/settings/HomePageSettingsPage.tsx @@ -19,6 +19,8 @@ import { } from '../../components/ui' import { useConfirm } from '../../components/Confirm' import { useToasts } from '../../components/Toasts' +import { SettingsPane } from './SettingsPane' +import { SettingsSection } from './SettingsSection' /** * Tresc sekcji statycznych strony glownej Akademii. @@ -69,12 +71,40 @@ const SECTIONS: { }, ] +/* + * Naglowek strony glownej: pojedyncze wartosci z `app_settings`. + * + * Istnialy w backendzie od poczatku, ale nie bylo ich w panelu, wiec tytul + * i zdanie pod nim dalo sie zmienic wylacznie zapytaniem do bazy. Grafika to + * identyfikator z biblioteki mediow, bo zdjecie jest materialem klienta i nie + * moze trafic do repozytorium. + */ +const HERO_KEYS = [ + 'ACADEMY_HERO_EYEBROW', + 'ACADEMY_HERO_TITLE', + 'ACADEMY_HERO_TEXT', + 'ACADEMY_HERO_IMAGE_MEDIA_ID', +] + export function HomePageSettingsPage() { const [active, setActive] = useState('EXPERT') const section = SECTIONS.find((s) => s.id === active)! return (
+ + {({ settings, onSave, saving }) => ( + + )} + +