Grafika strony glownej z biblioteki mediow, naglowek w panelu
Deploy PROD / deploy (push) Successful in 26s
Deploy PROD / deploy (push) Successful in 26s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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, "",
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
<div className="relative hidden overflow-hidden rounded-xl shadow-lg lg:block">
|
||||
<img
|
||||
src={data.heroImageUrl}
|
||||
alt=""
|
||||
className="aspect-video h-auto w-full object-cover"
|
||||
loading="eager"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="relative hidden h-72 overflow-hidden rounded-xl bg-gradient-hydro shadow-lg lg:block lg:h-96"
|
||||
@@ -171,6 +195,7 @@ export function AcademyHomePage() {
|
||||
<Icon name="droplet" size={34} className="text-white" />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Band>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<PageSectionType>('EXPERT')
|
||||
const section = SECTIONS.find((s) => s.id === active)!
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPane>
|
||||
{({ settings, onSave, saving }) => (
|
||||
<SettingsSection
|
||||
title="Nagłówek strony głównej"
|
||||
description="Tytuł, zdanie pod nim i grafika obok. Pusta grafika pokazuje domyślny motyw."
|
||||
settings={settings}
|
||||
keys={HERO_KEYS}
|
||||
onSave={onSave}
|
||||
saving={saving}
|
||||
/>
|
||||
)}
|
||||
</SettingsPane>
|
||||
|
||||
<Card
|
||||
title="Strona główna Akademii"
|
||||
description="Treść sekcji widocznych dla kursantów. Zmiany są widoczne od razu, bez wdrożenia."
|
||||
|
||||
Reference in New Issue
Block a user