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. + */ +