Enhance audience management features with new components and API integrations
- Added `AssignCoursesModal`, `AudienceMemberRow`, `AudienceTable`, and related components for improved audience management UI. - Implemented audience invite actions including resend and revoke functionality. - Updated audience API to support importing members and assigning courses. - Refactored audience data handling to include status and invite management. - Improved overall user experience with pagination and selection features in the audience table.
This commit is contained in:
@@ -48,7 +48,7 @@
|
||||
|
||||
// Invite context: allow signup when invite_token present or redirect contains invite info
|
||||
const hasInviteContext = $derived(
|
||||
!!inviteToken || (!!redirectUrl && (redirectUrl.includes('/invite/t/') || redirectUrl.includes('invite_token')))
|
||||
!!inviteToken || (!!redirectUrl && (redirectUrl.includes('/invite/') || redirectUrl.includes('invite_token')))
|
||||
);
|
||||
|
||||
const inviteOnly = $derived(!!org?.settings?.signup?.inviteOnly);
|
||||
|
||||
@@ -86,7 +86,14 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// No need to require login for public routes
|
||||
// Skip /login redirect below when the route is already public.
|
||||
// [NO_REDIRECT] public + path !== '/' — e.g. /login, /signup, /courses, … (not the main-app home edge case)
|
||||
// [NO_REDIRECT] public + '/' + isOrgSite — org landing page on its own domain
|
||||
// [MAY_REDIRECT_TO_LOGIN] public + '/' + !isOrgSite — main app home is public so we can show loading/errors while
|
||||
// auth resolves; if there is still no session after that, we send to /login (this effect). Logged-in users
|
||||
// return earlier (session branch above); post-login navigation from '/' is in init.svelte.ts routeUserToNextPage.
|
||||
// [MAY_REDIRECT_TO_LOGIN] not public — private route; unauthenticated users go to /login
|
||||
|
||||
if (isPublicRoute(path) && (path !== '/' || data.isOrgSite)) {
|
||||
return;
|
||||
}
|
||||
|
||||
+42
-8
@@ -11,6 +11,7 @@
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { page } from '$app/state';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { ROLE } from '@cio/utils/constants';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
@@ -25,6 +26,9 @@
|
||||
)
|
||||
);
|
||||
const canJoinOrganization = $derived(inviteStatus === 'ACTIVE' && !isInviteEmailMismatch);
|
||||
const isAlreadyJoined = $derived(inviteStatus === 'ACCEPTED');
|
||||
|
||||
const orgSlug = $derived(data.currentOrg?.siteName ?? data.invite.organization.siteName ?? '');
|
||||
|
||||
function buildInviteAuthParams(pathname: string, email: string): string {
|
||||
const parts: string[] = [];
|
||||
@@ -35,6 +39,9 @@
|
||||
|
||||
const loginParams = $derived(buildInviteAuthParams(page.url?.pathname || '', data.invite?.invite?.email || ''));
|
||||
|
||||
const orgName = $derived(data.invite?.organization?.name ?? '');
|
||||
const inviteRoleId = $derived(data.invite?.invite?.roleId ?? 0);
|
||||
|
||||
function getBlockedInviteMessage(): string {
|
||||
if (isInviteEmailMismatch) {
|
||||
return t.get('invite.organization.messages.email_mismatch');
|
||||
@@ -96,6 +103,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleNavigateAfterJoined() {
|
||||
if (inviteRoleId === ROLE.STUDENT) {
|
||||
return goto(resolve('/lms', {}));
|
||||
}
|
||||
return goto(resolve(orgSlug ? `/org/${orgSlug}` : '/org', {}));
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (!data.currentOrg) return;
|
||||
|
||||
@@ -110,23 +124,43 @@
|
||||
|
||||
<AuthUI isLogin={false} {handleSubmit} isLoading={loading} showOnlyContent={true} showLogo={true}>
|
||||
<div class="mt-0 w-full">
|
||||
<h3 class="mt-0 mb-4 text-center text-lg font-medium dark:text-white">{data.invite.organization.name}</h3>
|
||||
<p class="text-center text-sm font-light dark:text-white">
|
||||
{$t('invite.organization.role_label', { role: data.invite.invite.roleLabel })}
|
||||
{#if inviteRoleId === ROLE.STUDENT}
|
||||
{$t('invite.organization.invitation_student', { orgName })}
|
||||
{:else if inviteRoleId === ROLE.TUTOR}
|
||||
{$t('invite.organization.invitation_teacher', { orgName })}
|
||||
{:else if inviteRoleId === ROLE.ADMIN}
|
||||
{$t('invite.organization.invitation_admin', { orgName })}
|
||||
{:else}
|
||||
{$t('invite.organization.invitation_fallback', {
|
||||
orgName,
|
||||
role: data.invite.invite.roleLabel
|
||||
})}
|
||||
{/if}
|
||||
</p>
|
||||
<p class="mt-1 text-center text-sm font-light dark:text-white">
|
||||
{$t('invite.organization.email_label', { email: data.invite.invite.email })}
|
||||
<p class="mt-3 text-center text-sm font-light dark:text-white">
|
||||
{$t('invite.organization.email_line', { email: data.invite.invite.email })}
|
||||
</p>
|
||||
|
||||
{#if !canJoinOrganization}
|
||||
{#if !canJoinOrganization && !isAlreadyJoined}
|
||||
<p class="mt-3 text-center text-sm text-red-500">{getBlockedInviteMessage()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="my-4 flex w-full flex-col items-center justify-center gap-3">
|
||||
<Button type="submit" disabled={!canJoinOrganization || loading} {loading}>
|
||||
{$t('invite.organization.join_button')}
|
||||
</Button>
|
||||
{#if isAlreadyJoined}
|
||||
<Button type="button" onclick={handleNavigateAfterJoined}>
|
||||
{#if inviteRoleId === ROLE.STUDENT}
|
||||
{$t('navigation.goto_lms')}
|
||||
{:else}
|
||||
{$t('invite.organization.go_to_dashboard')}
|
||||
{/if}
|
||||
</Button>
|
||||
{:else}
|
||||
<Button type="submit" disabled={!canJoinOrganization || loading} {loading}>
|
||||
{$t('invite.organization.accept_button')}
|
||||
</Button>
|
||||
{/if}
|
||||
<p class="ui:text-muted-foreground text-center text-sm">
|
||||
{$t('login.already_have_account')}
|
||||
<a class="ui:text-primary hover:underline" href={resolve(`/login?${loginParams}`, {})}> {$t('login.login')}</a>
|
||||
@@ -1,11 +1,15 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { CommunityQuestionPage } from '$features/community/pages';
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
const communityPath = $derived(page.url.pathname.replace(/\/[^/]+$/, ''));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Question - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<CommunityQuestionPage slug={data.slug} />
|
||||
<CommunityQuestionPage slug={data.slug} backHref={resolve(communityPath, {})} />
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { AskCommunityPage } from '$features/community/pages';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { BackButton } from '@cio/ui';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
|
||||
const communityPath = $derived(page.url.pathname.replace(/\/ask$/, ''));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -11,6 +16,7 @@
|
||||
<Page.Root class="w-full">
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<BackButton href={resolve(communityPath, {})} label={$t('community.ask.go_back')} />
|
||||
<Page.Title>{$t('community.ask.ask_the')}</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
</Page.Header>
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
import { CreateCourseButton } from '$features/course/components';
|
||||
import * as Avatar from '@cio/ui/base/avatar';
|
||||
import { shortenName } from '$lib/utils/functions/string';
|
||||
import { Progress } from '@cio/ui/base/progress';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { WelcomeModal } from '$features/onboarding/components';
|
||||
@@ -148,11 +147,12 @@
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Avatar.Root class="h-6 w-6">
|
||||
<Avatar.Image
|
||||
src={enrollment.avatarUrl ? enrollment.avatarUrl : '/logo-192.png'}
|
||||
alt={enrollment.name ? enrollment.name : 'User'}
|
||||
/>
|
||||
<Avatar.Fallback>{shortenName(enrollment.name) || 'U'}</Avatar.Fallback>
|
||||
{#if enrollment.avatarUrl}
|
||||
<Avatar.Image src={enrollment.avatarUrl} alt={enrollment.name ? enrollment.name : 'User'} />
|
||||
{/if}
|
||||
<Avatar.Fallback>
|
||||
<UserIcon class="ui:size-3 ui:text-muted-foreground" />
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
|
||||
<div class="min-h-[45px] space-y-1">
|
||||
|
||||
@@ -5,17 +5,26 @@ export const load = async ({ parent, cookies }) => {
|
||||
|
||||
if (!orgId) {
|
||||
return {
|
||||
audience: []
|
||||
audience: [],
|
||||
courses: []
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch organization audience
|
||||
const audienceResponse = await classroomio.organization.audience.$get({}, getApiHeaders(cookies, orgId));
|
||||
const headers = getApiHeaders(cookies, orgId);
|
||||
|
||||
const [audienceResponse, coursesResponse] = await Promise.all([
|
||||
classroomio.organization.audience.$get({}, headers),
|
||||
classroomio.organization.courses.$get({ query: { tags: undefined } }, headers)
|
||||
]);
|
||||
|
||||
const audienceData = await audienceResponse.json();
|
||||
const audience = audienceData.success ? audienceData.data : [];
|
||||
|
||||
const coursesData = await coursesResponse.json();
|
||||
const courses = coursesData.success ? coursesData.data : [];
|
||||
|
||||
return {
|
||||
audience
|
||||
audience,
|
||||
courses
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
import { currentOrgPlan, currentOrgMaxAudience } from '$lib/utils/store/org';
|
||||
import { PLAN } from '@cio/utils/plans';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
import { page } from '$app/state';
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
let { data } = $props();
|
||||
let isLoading = $state(false);
|
||||
@@ -35,14 +37,17 @@
|
||||
</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<Button onclick={exportAudience} disabled={isLoading} loading={isLoading}>
|
||||
<Button variant="outline" onclick={exportAudience} disabled={isLoading} loading={isLoading}>
|
||||
{$t('audience.export')}
|
||||
</Button>
|
||||
<Button variant="secondary" href={resolve(`${page.url.pathname}/import`, {})}>
|
||||
{$t('audience.import_users')}
|
||||
</Button>
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<AudiencePage audience={data.audience} />
|
||||
<AudiencePage audience={data.audience} courses={data.courses} />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
</Page.Root>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { classroomio, getApiHeaders } from '$lib/utils/services/api';
|
||||
|
||||
export const load = async ({ parent, cookies }) => {
|
||||
const { orgId } = await parent();
|
||||
|
||||
if (!orgId) {
|
||||
return {
|
||||
courses: []
|
||||
};
|
||||
}
|
||||
|
||||
const coursesResponse = await classroomio.organization.courses.$get({}, getApiHeaders(cookies, orgId));
|
||||
const coursesData = await coursesResponse.json();
|
||||
const courses = coursesData.success ? coursesData.data : [];
|
||||
|
||||
return {
|
||||
courses
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { ImportAudiencePage } from '$features/audience/pages';
|
||||
import { BackButton } from '@cio/ui';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
const audiencePath = $derived(page.url.pathname.replace(/\/import$/, ''));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t('audience.import.title')} - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<Page.Root class="mx-auto w-full max-w-3xl">
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<BackButton href={resolve(audiencePath, {})} label={$t('audience.import.back')} class="p-0!" />
|
||||
<Page.Title>{$t('audience.import.title')}</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<ImportAudiencePage courses={data.courses} />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
</Page.Root>
|
||||
@@ -1,11 +1,15 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { CommunityQuestionPage } from '$features/community/pages';
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
const communityPath = $derived(page.url.pathname.replace(/\/[^/]+$/, ''));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Question - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<CommunityQuestionPage slug={data.slug} question={data.question} />
|
||||
<CommunityQuestionPage slug={data.slug} question={data.question} backHref={resolve(communityPath, {})} />
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { AskCommunityPage } from '$features/community/pages';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { BackButton } from '@cio/ui';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
|
||||
const communityPath = $derived(page.url.pathname.replace(/\/ask$/, ''));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -11,6 +16,7 @@
|
||||
<Page.Root class="w-full">
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<BackButton href={resolve(communityPath, {})} label={$t('community.ask.go_back')} />
|
||||
<Page.Title>{$t('community.ask.ask_the')}</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
</Page.Header>
|
||||
|
||||
@@ -2,24 +2,33 @@
|
||||
import { CoursesPage } from '$features/course/pages';
|
||||
import { CreateCourseButton, CourseFilterPopover } from '$features/course/components';
|
||||
import { courseMetaDeta } from '$features/course/utils/store';
|
||||
import { COURSE_SORT_OPTIONS } from '$features/course/utils/constants';
|
||||
import {
|
||||
CourseSortBy,
|
||||
CourseSortOrder,
|
||||
DEFAULT_COURSE_SORT,
|
||||
DEFAULT_SORT_ORDER,
|
||||
parseCourseSortOrder,
|
||||
parseCourseSortValue
|
||||
} from '$features/course/utils/constants';
|
||||
import { browser } from '$app/environment';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { onMount } from 'svelte';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
import { coursesApi } from '$features/course/api';
|
||||
|
||||
let { data } = $props();
|
||||
const getInitialSelectedTags = () => data.activeTags ?? [];
|
||||
|
||||
let searchValue = $state('');
|
||||
let selectedId: string = $state('0');
|
||||
let selectedOrder = $state<'asc' | 'desc'>('desc');
|
||||
let selectedTags = $state<string[]>([]);
|
||||
let hasInitializedFilters = $state(false);
|
||||
let isFiltering = $state(false);
|
||||
let sortKey: CourseSortBy = $state(DEFAULT_COURSE_SORT);
|
||||
let selectedOrder = $state<CourseSortOrder>(DEFAULT_SORT_ORDER);
|
||||
|
||||
const validSortValues = new Set(COURSE_SORT_OPTIONS.map((option) => option.value));
|
||||
const validSortOrders = new Set(['asc', 'desc']);
|
||||
let selectedTags = $state<string[]>(getInitialSelectedTags());
|
||||
|
||||
let hasInitializedFilters = $state(false);
|
||||
let hasCompletedInitialUrlSync = false;
|
||||
let isFiltering = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (data.courses) {
|
||||
@@ -27,39 +36,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
selectedTags = data.activeTags ?? [];
|
||||
});
|
||||
|
||||
function normalizeSortValue(sortValue: string | null): string {
|
||||
if (sortValue && validSortValues.has(sortValue)) {
|
||||
return sortValue;
|
||||
}
|
||||
|
||||
return '0';
|
||||
}
|
||||
|
||||
function normalizeSortOrder(sortOrder: string | null): 'asc' | 'desc' {
|
||||
if (sortOrder && validSortOrders.has(sortOrder)) {
|
||||
return sortOrder as 'asc' | 'desc';
|
||||
}
|
||||
|
||||
return 'desc';
|
||||
}
|
||||
|
||||
function getLegacySortState(sortValue: string | null) {
|
||||
switch (sortValue) {
|
||||
case '3':
|
||||
return { selectedId: '0', selectedOrder: 'asc' as const };
|
||||
case '4':
|
||||
return { selectedId: '1', selectedOrder: 'asc' as const };
|
||||
case '5':
|
||||
return { selectedId: '2', selectedOrder: 'asc' as const };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateFiltersUrl() {
|
||||
if (!browser || !hasInitializedFilters) {
|
||||
return;
|
||||
@@ -73,13 +49,13 @@
|
||||
nextUrl.searchParams.delete('tags');
|
||||
}
|
||||
|
||||
if (selectedId !== '0') {
|
||||
nextUrl.searchParams.set('sort', selectedId);
|
||||
if (sortKey !== DEFAULT_COURSE_SORT) {
|
||||
nextUrl.searchParams.set('sort', sortKey);
|
||||
} else {
|
||||
nextUrl.searchParams.delete('sort');
|
||||
}
|
||||
|
||||
if (selectedOrder !== 'desc') {
|
||||
if (selectedOrder !== DEFAULT_SORT_ORDER) {
|
||||
nextUrl.searchParams.set('order', selectedOrder);
|
||||
} else {
|
||||
nextUrl.searchParams.delete('order');
|
||||
@@ -94,8 +70,14 @@
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem('classroomio_filter_course_sort_key', selectedId);
|
||||
localStorage.setItem('classroomio_filter_course_sort_key', sortKey);
|
||||
localStorage.setItem('classroomio_filter_course_order_key', selectedOrder);
|
||||
|
||||
if (!hasCompletedInitialUrlSync) {
|
||||
hasCompletedInitialUrlSync = true;
|
||||
return;
|
||||
}
|
||||
|
||||
updateFiltersUrl();
|
||||
});
|
||||
|
||||
@@ -111,7 +93,7 @@
|
||||
}
|
||||
|
||||
function toggleTag(tagSlug: string, checked: boolean) {
|
||||
const next = new Set(selectedTags);
|
||||
const next = new SvelteSet(selectedTags);
|
||||
|
||||
if (checked) {
|
||||
next.add(tagSlug);
|
||||
@@ -123,8 +105,8 @@
|
||||
}
|
||||
|
||||
async function clearFilters() {
|
||||
selectedId = '0';
|
||||
selectedOrder = 'desc';
|
||||
sortKey = DEFAULT_COURSE_SORT;
|
||||
selectedOrder = DEFAULT_SORT_ORDER;
|
||||
|
||||
if (selectedTags.length === 0) {
|
||||
return;
|
||||
@@ -144,21 +126,21 @@
|
||||
|
||||
const sortedCourses = [...filteredCourses];
|
||||
|
||||
if (selectedId === '0') {
|
||||
if (sortKey === CourseSortBy.DateCreated) {
|
||||
return sortedCourses.sort((a, b) =>
|
||||
selectedOrder === 'asc'
|
||||
? new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||
: new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
selectedOrder === CourseSortOrder.Asc
|
||||
? new Date(a.createdAt ?? '').getTime() - new Date(b.createdAt ?? '').getTime()
|
||||
: new Date(b.createdAt ?? '').getTime() - new Date(a.createdAt ?? '').getTime()
|
||||
);
|
||||
} else if (selectedId === '1') {
|
||||
} else if (sortKey === CourseSortBy.Published) {
|
||||
return sortedCourses.sort((a, b) =>
|
||||
selectedOrder === 'asc'
|
||||
selectedOrder === CourseSortOrder.Asc
|
||||
? Number(a.isPublished) - Number(b.isPublished)
|
||||
: Number(b.isPublished) - Number(a.isPublished)
|
||||
);
|
||||
} else if (selectedId === '2') {
|
||||
} else if (sortKey === CourseSortBy.Lessons) {
|
||||
return sortedCourses.sort((a, b) =>
|
||||
selectedOrder === 'asc' ? a.lessonCount - b.lessonCount : b.lessonCount - a.lessonCount
|
||||
selectedOrder === CourseSortOrder.Asc ? a.lessonCount - b.lessonCount : b.lessonCount - a.lessonCount
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,34 +154,22 @@
|
||||
$courseMetaDeta.view = courseView;
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const legacySortFromUrl = getLegacySortState(searchParams.get('sort'));
|
||||
const legacySortFromStorage = getLegacySortState(localStorage.getItem('classroomio_filter_course_key'));
|
||||
const oldSortFromStorage = normalizeSortValue(localStorage.getItem('classroomio_filter_course_key'));
|
||||
const sortFromUrl = normalizeSortValue(searchParams.get('sort'));
|
||||
const sortFromStorage = normalizeSortValue(localStorage.getItem('classroomio_filter_course_sort_key'));
|
||||
const orderFromUrl = normalizeSortOrder(searchParams.get('order'));
|
||||
const orderFromStorage = normalizeSortOrder(localStorage.getItem('classroomio_filter_course_order_key'));
|
||||
const sortFromStorage = parseCourseSortValue(localStorage.getItem('classroomio_filter_course_sort_key'));
|
||||
const orderFromStorage = parseCourseSortOrder(localStorage.getItem('classroomio_filter_course_order_key'));
|
||||
|
||||
if (legacySortFromUrl) {
|
||||
selectedId = legacySortFromUrl.selectedId;
|
||||
selectedOrder = legacySortFromUrl.selectedOrder;
|
||||
} else if (sortFromUrl !== '0' || orderFromUrl !== 'desc') {
|
||||
selectedId = sortFromUrl;
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const sortFromUrl = parseCourseSortValue(searchParams.get('sort'));
|
||||
const orderFromUrl = parseCourseSortOrder(searchParams.get('order'));
|
||||
|
||||
if (sortFromUrl !== DEFAULT_COURSE_SORT || orderFromUrl !== DEFAULT_SORT_ORDER) {
|
||||
sortKey = sortFromUrl;
|
||||
selectedOrder = orderFromUrl;
|
||||
} else if (legacySortFromStorage) {
|
||||
selectedId = legacySortFromStorage.selectedId;
|
||||
selectedOrder = legacySortFromStorage.selectedOrder;
|
||||
} else if (sortFromStorage !== '0' || orderFromStorage !== 'desc') {
|
||||
selectedId = sortFromStorage;
|
||||
} else if (sortFromStorage !== DEFAULT_COURSE_SORT || orderFromStorage !== DEFAULT_SORT_ORDER) {
|
||||
sortKey = sortFromStorage;
|
||||
selectedOrder = orderFromStorage;
|
||||
} else {
|
||||
selectedId = oldSortFromStorage;
|
||||
selectedOrder = 'desc';
|
||||
}
|
||||
|
||||
hasInitializedFilters = true;
|
||||
updateFiltersUrl();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -218,10 +188,10 @@
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<CoursesPage courses={filteredCourses} bind:searchValue bind:selectedId showSortSelect={false}>
|
||||
<CoursesPage courses={filteredCourses} bind:searchValue bind:sortKey showSortSelect={false}>
|
||||
{#snippet filterControls()}
|
||||
<CourseFilterPopover
|
||||
bind:selectedId
|
||||
bind:sortKey
|
||||
bind:selectedOrder
|
||||
{selectedTags}
|
||||
tagGroups={data.tagGroups}
|
||||
|
||||
Reference in New Issue
Block a user