fix(dashboard): show course cards in landing page settings preview (#697)
* fix(dashboard): show course cards in landing page settings preview Fetch public courses for the org and pass them to the theme preview on the landing page settings screen, matching the editor preview. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * refactor(dashboard): skip redundant public course fetches Add loadPublicCoursesIfNeeded so settings, editor, and live landing page reuse cached courses for the same org site instead of clearing and refetching on every mount. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(dashboard): always refetch courses on live org landing page Keep loadPublicCoursesIfNeeded for settings preview and editor only. The public-facing landing page should fetch fresh on each visit. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * feat(ui): add shared empty state for org landing page catalogs Introduce OrgLandingPageCoursesEmpty using the dashboard Empty pattern with landing-theme colors. Show it across all ten org landing templates when no courses are published, and wire translated labels through buildOrgLandingPageProps. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(dashboard): address Greptile review on public courses cache - Add coursesLoaded prop so empty catalog state waits for fetch - Deduplicate concurrent public course requests via in-flight promise - Refresh courses on settings mount; reuse cache in editor only - Expose invalidatePublicCourses for explicit cache clearing Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(dashboard): load landing preview courses when org store hydrates Replace onMount course fetch with a reactive effect keyed on the route slug and currentOrg.siteName so public courses load after app init. Mark the fetch as complete in finally so failed requests show the empty state instead of a permanently blank catalog section. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> ---------
This commit is contained in:
co-authored by
Rotimi Best
parent
8a22ee60bc
commit
bce38aa530
@@ -70,8 +70,11 @@ class OrgApi extends BaseApiWithErrors {
|
||||
audiencePagination = $state<OrganizationAudiencePagination | null>(null);
|
||||
publicCourses: OrgPublicCourses = $state([]);
|
||||
hasMorePublicCourses = $state(false);
|
||||
publicCoursesLoadedSiteName: string | null = $state(null);
|
||||
|
||||
isFetchingOrgPublicCourses = $state(false);
|
||||
private activePublicCoursesFetch: Promise<void> | null = null;
|
||||
private activePublicCoursesFetchSiteName: string | null = null;
|
||||
private activeAudienceRequestController: AbortController | null = null;
|
||||
|
||||
cancelAudienceRequest() {
|
||||
@@ -155,15 +158,52 @@ class OrgApi extends BaseApiWithErrors {
|
||||
return response;
|
||||
}
|
||||
|
||||
invalidatePublicCourses() {
|
||||
this.publicCoursesLoadedSiteName = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refetches public courses for a site, clearing any cached settings preview data.
|
||||
*/
|
||||
async refreshPublicCourses(siteName: string) {
|
||||
if (!siteName) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.invalidatePublicCourses();
|
||||
await this.fetchPublicCoursesBySiteName(siteName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads public courses for a site when they have not been fetched yet.
|
||||
* Skips the request when courses are already in memory for the same site.
|
||||
*/
|
||||
async loadPublicCoursesIfNeeded(siteName: string) {
|
||||
if (!siteName || this.publicCoursesLoadedSiteName === siteName) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.fetchPublicCoursesBySiteName(siteName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets public courses by organization siteName (for landing pages)
|
||||
* @param siteName Organization site name
|
||||
* @returns Published courses array
|
||||
*/
|
||||
async getPublicCoursesBySiteName(siteName: string) {
|
||||
this.invalidatePublicCourses();
|
||||
await this.fetchPublicCoursesBySiteName(siteName);
|
||||
}
|
||||
|
||||
private fetchPublicCoursesBySiteName(siteName: string): Promise<void> {
|
||||
if (this.activePublicCoursesFetch && this.activePublicCoursesFetchSiteName === siteName) {
|
||||
return this.activePublicCoursesFetch;
|
||||
}
|
||||
|
||||
this.isFetchingOrgPublicCourses = true;
|
||||
|
||||
await this.execute<GetOrgPublicCoursesRequest>({
|
||||
const fetchPromise = this.execute<GetOrgPublicCoursesRequest>({
|
||||
requestFn: () =>
|
||||
classroomio.organization.courses.public.$get({
|
||||
query: { siteName }
|
||||
@@ -172,10 +212,29 @@ class OrgApi extends BaseApiWithErrors {
|
||||
onSuccess: (response) => {
|
||||
this.publicCourses = response.data.courses;
|
||||
this.hasMorePublicCourses = response.data.hasMoreCourses;
|
||||
this.publicCoursesLoadedSiteName = siteName;
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(() => undefined)
|
||||
.finally(() => {
|
||||
this.isFetchingOrgPublicCourses = false;
|
||||
|
||||
this.isFetchingOrgPublicCourses = false;
|
||||
if (this.publicCoursesLoadedSiteName !== siteName) {
|
||||
this.publicCourses = [];
|
||||
this.hasMorePublicCourses = false;
|
||||
this.publicCoursesLoadedSiteName = siteName;
|
||||
}
|
||||
|
||||
if (this.activePublicCoursesFetchSiteName === siteName) {
|
||||
this.activePublicCoursesFetch = null;
|
||||
this.activePublicCoursesFetchSiteName = null;
|
||||
}
|
||||
});
|
||||
|
||||
this.activePublicCoursesFetch = fetchPromise;
|
||||
this.activePublicCoursesFetchSiteName = siteName;
|
||||
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,7 +49,8 @@
|
||||
normalizeLandingPageSettings(org.landingpage),
|
||||
orgApi.publicCourses,
|
||||
orgApi.hasMorePublicCourses,
|
||||
authAction
|
||||
authAction,
|
||||
{ coursesLoaded: hasLoadedCourses }
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
labelMatchesSocialPlatform,
|
||||
resolveFooterSocialPlatform
|
||||
} from '@cio/ui/custom/org-landing-page/footer-social-platform';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
export const landingPageThemes = [
|
||||
'minimal',
|
||||
@@ -689,12 +690,20 @@ export function mapPublicCoursesToLandingPageCourses(courses: OrgPublicCourses):
|
||||
});
|
||||
}
|
||||
|
||||
export function buildOrgLandingPageLabels(): OrgLandingPageProps['labels'] {
|
||||
return {
|
||||
catalogEmptyTitle: t.get('public_courses.catalog_empty.title'),
|
||||
catalogEmptyDescription: t.get('public_courses.catalog_empty.description')
|
||||
};
|
||||
}
|
||||
|
||||
export function buildOrgLandingPageProps(
|
||||
org: AccountOrg,
|
||||
landingpage: unknown,
|
||||
courses: OrgPublicCourses,
|
||||
hasMoreCourses = false,
|
||||
authAction?: OrgLandingPageProps['authAction']
|
||||
authAction?: OrgLandingPageProps['authAction'],
|
||||
options?: { coursesLoaded?: boolean }
|
||||
): OrgLandingPageProps {
|
||||
const normalizedLandingPage = normalizeLandingPageSettings(landingpage);
|
||||
|
||||
@@ -704,7 +713,9 @@ export function buildOrgLandingPageProps(
|
||||
authAction,
|
||||
...normalizedLandingPage,
|
||||
courses: mapPublicCoursesToLandingPageCourses(courses),
|
||||
hasMoreCourses
|
||||
hasMoreCourses,
|
||||
coursesLoaded: options?.coursesLoaded ?? true,
|
||||
labels: buildOrgLandingPageLabels()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import ArrowRightIcon from '@lucide/svelte/icons/arrow-right';
|
||||
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
|
||||
@@ -134,7 +135,21 @@
|
||||
: { label: t.get('navigation.login'), href: '/login' }
|
||||
);
|
||||
|
||||
const previewProps = $derived(buildOrgLandingPageProps($currentOrg, normalized, [], false, authAction));
|
||||
const previewSiteName = $derived($currentOrg.siteName || page.params.slug || '');
|
||||
|
||||
const coursesLoaded = $derived(!previewSiteName || orgApi.publicCoursesLoadedSiteName === previewSiteName);
|
||||
|
||||
const previewProps = $derived.by(() =>
|
||||
buildOrgLandingPageProps($currentOrg, normalized, orgApi.publicCourses, orgApi.hasMorePublicCourses, authAction, {
|
||||
coursesLoaded
|
||||
})
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (!previewSiteName) return;
|
||||
|
||||
void orgApi.refreshPublicCourses(previewSiteName);
|
||||
});
|
||||
|
||||
const ThemeComponent = $derived(landingPageThemeComponents[currentTheme] ?? landingPageThemeComponents.minimal);
|
||||
|
||||
|
||||
@@ -3739,6 +3739,10 @@
|
||||
"title": "Ingen kurser fundet",
|
||||
"description": "Prøv at rydde dine filtre, eller tjek igen senere."
|
||||
},
|
||||
"catalog_empty": {
|
||||
"title": "Ingen kurser udgivet endnu",
|
||||
"description": "Kom snart tilbage for nye programmer."
|
||||
},
|
||||
"filters": {
|
||||
"title": "Filtrer efter tags",
|
||||
"help": "Vælg et eller flere tags for at filtrere kurser",
|
||||
|
||||
@@ -3739,6 +3739,10 @@
|
||||
"title": "Keine Kurse gefunden",
|
||||
"description": "Versuchen Sie, Ihre Filter zu löschen, oder schauen Sie später noch einmal vorbei."
|
||||
},
|
||||
"catalog_empty": {
|
||||
"title": "Noch keine Kurse veröffentlicht",
|
||||
"description": "Schauen Sie bald wieder vorbei für neue Programme."
|
||||
},
|
||||
"filters": {
|
||||
"title": "Nach Tags filtern",
|
||||
"help": "Wählen Sie einen oder mehrere Tags aus, um Kurse zu filtern",
|
||||
|
||||
@@ -1231,6 +1231,10 @@
|
||||
"empty": {
|
||||
"title": "No courses found",
|
||||
"description": "Try clearing your filters or check back later."
|
||||
},
|
||||
"catalog_empty": {
|
||||
"title": "No courses published yet",
|
||||
"description": "Check back soon for new programs."
|
||||
}
|
||||
},
|
||||
"org_navigation": {
|
||||
|
||||
@@ -3739,6 +3739,10 @@
|
||||
"title": "No se encontraron cursos",
|
||||
"description": "Intente borrar sus filtros o vuelva a consultar más tarde."
|
||||
},
|
||||
"catalog_empty": {
|
||||
"title": "Aún no hay cursos publicados",
|
||||
"description": "Vuelva pronto para ver nuevos programas."
|
||||
},
|
||||
"filters": {
|
||||
"title": "Filtrar por etiquetas",
|
||||
"help": "Seleccione una o más etiquetas para filtrar cursos",
|
||||
|
||||
@@ -3739,6 +3739,10 @@
|
||||
"title": "Aucun cours trouvé",
|
||||
"description": "Essayez de supprimer vos filtres ou revenez plus tard."
|
||||
},
|
||||
"catalog_empty": {
|
||||
"title": "Aucun cours publié pour le moment",
|
||||
"description": "Revenez bientôt pour découvrir de nouveaux programmes."
|
||||
},
|
||||
"filters": {
|
||||
"title": "Filtrer par balises",
|
||||
"help": "Sélectionnez une ou plusieurs balises pour filtrer les cours",
|
||||
|
||||
@@ -3739,6 +3739,10 @@
|
||||
"title": "कोई पाठ्यक्रम नहीं मिला",
|
||||
"description": "अपने फ़िल्टर साफ़ करने का प्रयास करें या बाद में दोबारा जाँचें।"
|
||||
},
|
||||
"catalog_empty": {
|
||||
"title": "अभी तक कोई पाठ्यक्रम प्रकाशित नहीं हुआ",
|
||||
"description": "नए कार्यक्रमों के लिए जल्द ही वापस आएँ।"
|
||||
},
|
||||
"filters": {
|
||||
"title": "टैग द्वारा फ़िल्टर करें",
|
||||
"help": "पाठ्यक्रमों को फ़िल्टर करने के लिए एक या अधिक टैग चुनें",
|
||||
|
||||
@@ -3739,6 +3739,10 @@
|
||||
"title": "Nie znaleziono kursów",
|
||||
"description": "Spróbuj wyczyścić filtry lub sprawdź później."
|
||||
},
|
||||
"catalog_empty": {
|
||||
"title": "Brak opublikowanych kursów",
|
||||
"description": "Wróć wkrótce po nowe programy."
|
||||
},
|
||||
"filters": {
|
||||
"title": "Filtruj według tagów",
|
||||
"help": "Wybierz jeden lub więcej tagów, aby filtrować kursy",
|
||||
|
||||
@@ -3739,6 +3739,10 @@
|
||||
"title": "Nenhum curso encontrado",
|
||||
"description": "Tente limpar os filtros ou volte mais tarde."
|
||||
},
|
||||
"catalog_empty": {
|
||||
"title": "Nenhum curso publicado ainda",
|
||||
"description": "Volte em breve para novos programas."
|
||||
},
|
||||
"filters": {
|
||||
"title": "Filtrar por tags",
|
||||
"help": "Selecione uma ou mais tags para filtrar cursos",
|
||||
|
||||
@@ -3739,6 +3739,10 @@
|
||||
"title": "Курсы не найдены",
|
||||
"description": "Попробуйте очистить фильтры или зайдите позже."
|
||||
},
|
||||
"catalog_empty": {
|
||||
"title": "Пока нет опубликованных курсов",
|
||||
"description": "Загляните позже — скоро появятся новые программы."
|
||||
},
|
||||
"filters": {
|
||||
"title": "Фильтровать по тегам",
|
||||
"help": "Выберите один или несколько тегов, чтобы отфильтровать курсы.",
|
||||
|
||||
@@ -3739,6 +3739,10 @@
|
||||
"title": "Không tìm thấy khóa học nào",
|
||||
"description": "Hãy thử xóa bộ lọc của bạn hoặc kiểm tra lại sau."
|
||||
},
|
||||
"catalog_empty": {
|
||||
"title": "Chưa có khóa học nào được xuất bản",
|
||||
"description": "Hãy quay lại sớm để xem các chương trình mới."
|
||||
},
|
||||
"filters": {
|
||||
"title": "Lọc theo thẻ",
|
||||
"help": "Chọn một hoặc nhiều thẻ để lọc các khóa học",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition';
|
||||
import { onMount, type Component } from 'svelte';
|
||||
import type { Component } from 'svelte';
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -90,24 +90,27 @@
|
||||
}
|
||||
);
|
||||
|
||||
const previewProps = $derived(
|
||||
const previewSiteName = $derived($currentOrg.siteName || page.params.slug || '');
|
||||
|
||||
const coursesLoaded = $derived(!previewSiteName || orgApi.publicCoursesLoadedSiteName === previewSiteName);
|
||||
|
||||
const previewProps = $derived.by(() =>
|
||||
buildOrgLandingPageProps(
|
||||
$currentOrg,
|
||||
previewLandingPageSettings,
|
||||
orgApi.publicCourses,
|
||||
orgApi.hasMorePublicCourses,
|
||||
authAction
|
||||
authAction,
|
||||
{ coursesLoaded }
|
||||
)
|
||||
);
|
||||
|
||||
const ThemeComponent = $derived(landingPageThemeComponents[previewTheme] ?? landingPageThemeComponents.minimal);
|
||||
|
||||
onMount(() => {
|
||||
if ($currentOrg.siteName) {
|
||||
orgApi.publicCourses = [];
|
||||
orgApi.hasMorePublicCourses = false;
|
||||
void orgApi.getPublicCoursesBySiteName($currentOrg.siteName);
|
||||
}
|
||||
$effect(() => {
|
||||
if (!previewSiteName) return;
|
||||
|
||||
void orgApi.loadPublicCoursesIfNeeded(previewSiteName);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
|
||||
Reference in New Issue
Block a user