feat: centralize baseURL and app experience for cloud and self hosted

This commit is contained in:
Rotimi Best
2026-03-11 11:21:41 +01:00
parent a98d6a35fa
commit 74c87fb72a
18 changed files with 81 additions and 100 deletions
@@ -5,7 +5,7 @@
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left'; import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
import { IconButton } from '@cio/ui/custom/icon-button'; import { IconButton } from '@cio/ui/custom/icon-button';
import { globalStore } from '$lib/utils/store/app'; import { isStudentExperience } from '$lib/utils/store/app';
import { currentOrgPath } from '$lib/utils/store/org'; import { currentOrgPath } from '$lib/utils/store/org';
import { t } from '$lib/utils/functions/translations'; import { t } from '$lib/utils/functions/translations';
@@ -18,7 +18,7 @@
let { title = $bindable(''), navClass = '', isCoursePage = false }: Props = $props(); let { title = $bindable(''), navClass = '', isCoursePage = false }: Props = $props();
let coursesPath = $derived( let coursesPath = $derived(
$globalStore.isStudent ? '/lms/mylearning' : isCoursePage ? `${$currentOrgPath}/courses` : $currentOrgPath $isStudentExperience ? '/lms/mylearning' : isCoursePage ? `${$currentOrgPath}/courses` : $currentOrgPath
); );
</script> </script>
@@ -1,9 +1,8 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import { ROLE } from '@cio/utils/constants';
import { user } from '$lib/utils/store/user'; import { user } from '$lib/utils/store/user';
import { currentOrg, currentOrgPath } from '$lib/utils/store/org'; import { basePath } from '$lib/utils/store/app';
import { isCoursePage } from '$lib/utils/functions/app'; import { isCoursePage } from '$lib/utils/functions/app';
import { t } from '$lib/utils/functions/translations'; import { t } from '$lib/utils/functions/translations';
import type { TCustomLinks } from './types'; import type { TCustomLinks } from './types';
@@ -38,11 +37,10 @@
let redirect = $derived(isCoursePage(page.url.pathname) ? `?redirect=${page.url.pathname}` : ''); let redirect = $derived(isCoursePage(page.url.pathname) ? `?redirect=${page.url.pathname}` : '');
let showLinks = $derived(customLinks && customLinks.show && customLinks.links && customLinks.links.length > 0); let showLinks = $derived(customLinks && customLinks.show && customLinks.links && customLinks.links.length > 0);
let isAdminOrTeacher = $derived($currentOrg.roleId === ROLE.ADMIN || $currentOrg.roleId === ROLE.TUTOR); let gotoHref = $derived(resolve($basePath !== '#' ? $basePath : '/lms', {}));
let gotoHref = $derived( let gotoLabel = $derived(
isAdminOrTeacher && $currentOrgPath !== '#' ? resolve($currentOrgPath, {}) : resolve('/lms', {}) $basePath === '/lms' || $basePath === '#' ? 'navigation.goto_lms' : 'navigation.goto_dashboard'
); );
let gotoLabel = $derived(isAdminOrTeacher ? 'navigation.goto_dashboard' : 'navigation.goto_lms');
function toggleMobileMenu() { function toggleMobileMenu() {
mobileMenuOpen = !mobileMenuOpen; mobileMenuOpen = !mobileMenuOpen;
@@ -122,35 +122,27 @@ class AppInitApi extends BaseApi {
} }
const isStudent = get(isOrgStudent); const isStudent = get(isOrgStudent);
const userHasOrganizations = this.data.organizations.length > 0; const userHasOrganizations = this.data.organizations.length > 0;
const isCloud = PUBLIC_IS_SELFHOSTED !== 'true'; const isCloud = PUBLIC_IS_SELFHOSTED !== 'true';
// CLOUD: when user has no orgs and isOrgSite is false, route to /onboarding // CLOUD: when user has no orgs and isOrgSite is false, route to /onboarding
// isOrgSite - means the user is on a multi tenant organization site, we don't want to redirect to /onboarding in this case // isOrgSite - means the user is on a multi tenant organization site, we don't want to redirect to /onboarding in this case
if (isCloud) { if (isCloud) {
const shouldRedirectToOnboarding = !userHasOrganizations && !isOrgSite; const shouldRedirectToOnboarding = !userHasOrganizations && !isOrgSite;
if (shouldRedirectToOnboarding) { if (shouldRedirectToOnboarding) {
return goto(resolve(`/onboarding`, {})); return goto(resolve(`/onboarding`, {}));
} }
if (!shouldRedirectOnAuth(page.url.pathname)) return;
return isOrgSite || isStudent ? this.goToLMS() : this.goToOrg();
} else { } else {
// Self-hosted: when user has no orgs, route to /onboarding // Self-hosted: when user has no orgs, route to /onboarding
// This should only happen once cause Self hosted instance is single tenant
//
if (!userHasOrganizations) { if (!userHasOrganizations) {
return goto(resolve(`/onboarding`, {})); return goto(resolve(`/onboarding`, {}));
} }
if (!shouldRedirectOnAuth(page.url.pathname)) return;
return isStudent ? this.goToLMS() : this.goToOrg();
} }
if (!shouldRedirectOnAuth(page.url.pathname)) return;
const shouldGoToLMS = isCloud ? isOrgSite || !!isStudent : !!isStudent;
return shouldGoToLMS ? this.goToLMS() : this.goToOrg();
} }
goToLMS() { goToLMS() {
@@ -104,10 +104,11 @@ function isURLCustomDomain(url: URL) {
} }
export function getSubdomain(url: URL) { export function getSubdomain(url: URL) {
const appHost = env.PRIVATE_APP_HOST;
if (!appHost) return null;
const host = url.hostname.replace('www.', ''); const host = url.hostname.replace('www.', '');
const parts = host.split('.'); const parts = host.split('.');
const appHost = env.PRIVATE_APP_HOST;
const appHostParts = appHost.split('.'); const appHostParts = appHost.split('.');
const isAppHost = parts.slice(-appHostParts.length).join('.') === appHost; const isAppHost = parts.slice(-appHostParts.length).join('.') === appHost;
@@ -9,14 +9,14 @@ import type {
UpvotePostRequest UpvotePostRequest
} from '../utils/types'; } from '../utils/types';
import { ZCommunityComment, ZCommunityQuestionUpdate, ZCreateCommunityQuestion } from '@cio/utils/validation/community'; import { ZCommunityComment, ZCommunityQuestionUpdate, ZCreateCommunityQuestion } from '@cio/utils/validation/community';
import { currentOrgPath, currentOrg as currentOrgStore } from '$lib/utils/store/org'; import { currentOrg as currentOrgStore } from '$lib/utils/store/org';
import type { TCreateCommunityQuestion } from '@cio/utils/validation/community'; import type { TCreateCommunityQuestion } from '@cio/utils/validation/community';
import { coursesApi } from '$features/course/api/courses.svelte'; import { coursesApi } from '$features/course/api/courses.svelte';
import { currentCommunityQuestion } from '../utils/store'; import { currentCommunityQuestion } from '../utils/store';
import generateSlug from '$lib/utils/functions/generateSlug'; import generateSlug from '$lib/utils/functions/generateSlug';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { globalStore } from '$lib/utils/store/app'; import { basePath, isStudentExperience } from '$lib/utils/store/app';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { mapZodErrorsToTranslations } from '$lib/utils/validation'; import { mapZodErrorsToTranslations } from '$lib/utils/validation';
import { profile as profileStore } from '$lib/utils/store/user'; import { profile as profileStore } from '$lib/utils/store/user';
@@ -70,9 +70,8 @@ class CommunityApi extends BaseApiWithErrors {
* Fetches community questions for an organization * Fetches community questions for an organization
* Handles API call, success/error handling, and navigation * Handles API call, success/error handling, and navigation
* @param orgId Organization ID * @param orgId Organization ID
* @param isLMS Whether this is in LMS context (affects navigation path)
*/ */
async fetchCommunityQuestions({ orgId, isLMS = false }: { orgId: string; isLMS?: boolean }) { async fetchCommunityQuestions({ orgId }: { orgId: string }) {
await this.execute<typeof classroomio.community.$get>({ await this.execute<typeof classroomio.community.$get>({
requestFn: () => classroomio.community.$get({ query: { orgId } }), requestFn: () => classroomio.community.$get({ query: { orgId } }),
logContext: 'fetching community questions', logContext: 'fetching community questions',
@@ -81,8 +80,7 @@ class CommunityApi extends BaseApiWithErrors {
}, },
onError: (result) => { onError: (result) => {
console.error('Error loading community questions:', result); console.error('Error loading community questions:', result);
const basePath = isLMS ? '/lms' : get(currentOrgPath); goto(resolve(get(basePath), {}));
goto(resolve(basePath, {}));
} }
}); });
} }
@@ -91,9 +89,8 @@ class CommunityApi extends BaseApiWithErrors {
* Fetches a single community question by slug * Fetches a single community question by slug
* Handles API call, success/error handling, navigation, and store updates * Handles API call, success/error handling, navigation, and store updates
* @param slug Question slug * @param slug Question slug
* @param isLMS Whether this is in LMS context (affects navigation path and store updates)
*/ */
async fetchCommunityQuestion({ slug, isLMS = false }: { slug: string; isLMS?: boolean }) { async fetchCommunityQuestion({ slug }: { slug: string }) {
await this.execute<(typeof classroomio.community)[':slug']['$get']>({ await this.execute<(typeof classroomio.community)[':slug']['$get']>({
requestFn: () => classroomio.community[':slug'].$get({ param: { slug } }), requestFn: () => classroomio.community[':slug'].$get({ param: { slug } }),
logContext: 'fetching community question', logContext: 'fetching community question',
@@ -105,9 +102,9 @@ class CommunityApi extends BaseApiWithErrors {
} }
}, },
onError: (result) => { onError: (result) => {
console.error(`[${isLMS ? 'LMS' : 'ORG'}] Error loading community`, result); const isStudent = get(isStudentExperience);
const basePath = isLMS ? '/lms' : get(currentOrgPath); console.error(`[${isStudent ? 'LMS' : 'ORG'}] Error loading community`, result);
goto(resolve(basePath, {})); goto(resolve(get(basePath), {}));
} }
}); });
} }
@@ -116,9 +113,8 @@ class CommunityApi extends BaseApiWithErrors {
* Creates a new community question * Creates a new community question
* Handles validation, API call, success/error handling, and navigation * Handles validation, API call, success/error handling, and navigation
* @param fields User-provided fields (title, body, courseId) * @param fields User-provided fields (title, body, courseId)
* @param isLMS Whether this is in LMS context (affects navigation path)
*/ */
async createQuestion(fields: TCreateCommunityQuestion, isLMS: boolean = false) { async createQuestion(fields: TCreateCommunityQuestion) {
const result = ZCreateCommunityQuestion.safeParse(fields); const result = ZCreateCommunityQuestion.safeParse(fields);
if (!result.success) { if (!result.success) {
this.errors = mapZodErrorsToTranslations(result.error, 'community'); this.errors = mapZodErrorsToTranslations(result.error, 'community');
@@ -156,8 +152,8 @@ class CommunityApi extends BaseApiWithErrors {
snackbar.success('snackbar.community.success.question_submitted'); snackbar.success('snackbar.community.success.question_submitted');
// Navigate to the question based on context // Navigate to the question based on context
const basePath = isLMS ? '/lms' : get(currentOrgPath); const path = get(basePath);
goto(resolve(`${basePath}/community/${questionSlug}`, {})); goto(resolve(`${path}/community/${questionSlug}`, {}));
// Mark as successful // Mark as successful
this.success = true; this.success = true;
@@ -178,10 +174,9 @@ class CommunityApi extends BaseApiWithErrors {
* Handles validation, API call, and success/error handling * Handles validation, API call, and success/error handling
* Uses this.comment state for the comment body * Uses this.comment state for the comment body
* @param questionId Question ID * @param questionId Question ID
* @param isLMS Whether this is in LMS context (affects store updates)
* @param onSuccess Optional callback to execute on successful comment creation * @param onSuccess Optional callback to execute on successful comment creation
*/ */
async createComment(questionId: number, isLMS: boolean = false, onSuccess?: () => void) { async createComment(questionId: number, onSuccess?: () => void) {
const profile = get(profileStore); const profile = get(profileStore);
if (!profile?.id) { if (!profile?.id) {
snackbar.error('snackbar.community.error.missing_context'); snackbar.error('snackbar.community.error.missing_context');
@@ -220,7 +215,7 @@ class CommunityApi extends BaseApiWithErrors {
// Refetch the question to get updated comments with proper structure // Refetch the question to get updated comments with proper structure
if (this.question && this.question.slug) { if (this.question && this.question.slug) {
this.fetchCommunityQuestion({ slug: this.question.slug, isLMS }); this.fetchCommunityQuestion({ slug: this.question.slug });
} }
// Call the onSuccess callback if provided // Call the onSuccess callback if provided
@@ -369,9 +364,8 @@ class CommunityApi extends BaseApiWithErrors {
* Deletes a question by its ID (including all comments) * Deletes a question by its ID (including all comments)
* Handles API call, success/error handling, and navigation * Handles API call, success/error handling, and navigation
* @param questionId Question ID * @param questionId Question ID
* @param isLMS Whether this is in LMS context (affects navigation path)
*/ */
async deleteQuestion(questionId: string, isLMS: boolean = false) { async deleteQuestion(questionId: string) {
await this.execute<(typeof classroomio.community)[':id']['$delete']>({ await this.execute<(typeof classroomio.community)[':id']['$delete']>({
requestFn: () => classroomio.community[':id'].$delete({ param: { id: questionId } }), requestFn: () => classroomio.community[':id'].$delete({ param: { id: questionId } }),
logContext: 'deleting community question', logContext: 'deleting community question',
@@ -380,8 +374,8 @@ class CommunityApi extends BaseApiWithErrors {
snackbar.success('snackbar.community.success.success_delete'); snackbar.success('snackbar.community.success.success_delete');
// Navigate back to community page based on context // Navigate back to community page based on context
const basePath = isLMS ? '/lms' : get(currentOrgPath); const path = get(basePath);
goto(resolve(`${basePath}/community`, {})); goto(resolve(`${path}/community`, {}));
// Mark as successful // Mark as successful
this.success = true; this.success = true;
@@ -438,9 +432,9 @@ class CommunityApi extends BaseApiWithErrors {
async fetchCoursesForOrg(userId: string | null, orgId: string) { async fetchCoursesForOrg(userId: string | null, orgId: string) {
if (this.courses.length || !userId || !orgId) return; if (this.courses.length || !userId || !orgId) return;
const isLMS = get(globalStore).isOrgSite; const isStudent = get(isStudentExperience);
if (isLMS) { if (isStudent) {
const response = await coursesApi.getEnrolledCourses(); const response = await coursesApi.getEnrolledCourses();
if (response?.success && response?.data) { if (response?.success && response?.data) {
@@ -1,15 +1,14 @@
<script lang="ts"> <script lang="ts">
import { t } from '$lib/utils/functions/translations'; import { t } from '$lib/utils/functions/translations';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { currentOrgPath } from '$lib/utils/store/org'; import { resolve } from '$app/paths';
import { basePath } from '$lib/utils/store/app';
import { Button } from '@cio/ui/base/button'; import { Button } from '@cio/ui/base/button';
import { isMobileStore } from '@cio/ui/hooks/is-mobile.svelte'; import { isMobileStore } from '@cio/ui/hooks/is-mobile.svelte';
import PlusIcon from '@lucide/svelte/icons/plus'; import PlusIcon from '@lucide/svelte/icons/plus';
let { isLMS = false }: { isLMS?: boolean } = $props();
function askCommunity() { function askCommunity() {
goto(`${isLMS ? '/lms/community/ask' : `${$currentOrgPath}/community/ask`}`); goto(resolve(`${$basePath}/community/ask`, {}));
} }
</script> </script>
@@ -9,12 +9,6 @@
import { currentOrg } from '$lib/utils/store/org'; import { currentOrg } from '$lib/utils/store/org';
import { communityApi } from '$features/community/api/community.svelte'; import { communityApi } from '$features/community/api/community.svelte';
interface Props {
isLMS?: boolean;
}
let { isLMS = false }: Props = $props();
let fields = $state({ let fields = $state({
title: '', title: '',
body: '', body: '',
@@ -45,10 +39,8 @@
</p> </p>
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content>
{#each communityApi.courses as course} {#each communityApi.courses as course (course.id)}
{#if course.id} <Select.Item value={course.id}>{course.title}</Select.Item>
<Select.Item value={course.id}>{course.title}</Select.Item>
{/if}
{/each} {/each}
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
@@ -68,11 +60,7 @@
</div> </div>
<div class="mt-4 flex justify-end"> <div class="mt-4 flex justify-end">
<Button <Button variant="default" onclick={() => communityApi.createQuestion(fields)} disabled={communityApi.isLoading}>
variant="default"
onclick={() => communityApi.createQuestion(fields, isLMS)}
disabled={communityApi.isLoading}
>
{$t('community.ask.publish')} {$t('community.ask.publish')}
</Button> </Button>
</div> </div>
@@ -26,11 +26,10 @@
interface Props { interface Props {
slug: string; slug: string;
isLMS?: boolean;
question?: CommunityQuestionSuccess['data'] | null; question?: CommunityQuestionSuccess['data'] | null;
} }
let { isLMS = false, question }: Props = $props(); let { question }: Props = $props();
let commentEditor: TiptapEditor | undefined = $state(); let commentEditor: TiptapEditor | undefined = $state();
let isValidAnswer = false; // V2 allow admin mark an answer as accepted let isValidAnswer = false; // V2 allow admin mark an answer as accepted
@@ -86,7 +85,7 @@
}} }}
onDelete={() => { onDelete={() => {
if (communityApi.question) { if (communityApi.question) {
communityApi.deleteQuestion(String(communityApi.question.id), isLMS); communityApi.deleteQuestion(String(communityApi.question.id));
} }
}} }}
/> />
@@ -119,10 +118,8 @@
</span> </span>
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content>
{#each communityApi.courses as course} {#each communityApi.courses as course (course.id)}
{#if course.id} <Select.Item value={course.id}>{course.title}</Select.Item>
<Select.Item value={course.id}>{course.title}</Select.Item>
{/if}
{/each} {/each}
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
@@ -224,7 +221,7 @@
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
{#each communityApi.question?.comments as commentItem} {#each communityApi.question?.comments as commentItem (commentItem.id)}
<CommunityCommentItem <CommunityCommentItem
comment={commentItem} comment={commentItem}
isVoted={!!voted.comment[commentItem.id]} isVoted={!!voted.comment[commentItem.id]}
@@ -266,7 +263,7 @@
console.log('comment', communityApi.comment); console.log('comment', communityApi.comment);
console.log('question', communityApi.question); console.log('question', communityApi.question);
if (communityApi.question) { if (communityApi.question) {
communityApi.createComment(Number(communityApi.question.id), isLMS, () => { communityApi.createComment(Number(communityApi.question.id), () => {
commentEditor?.commands?.clearContent?.(); commentEditor?.commands?.clearContent?.();
}); });
} }
@@ -11,18 +11,18 @@
import { profile } from '$lib/utils/store/user'; import { profile } from '$lib/utils/store/user';
import { t } from '$lib/utils/functions/translations'; import { t } from '$lib/utils/functions/translations';
import { calDateDiff } from '$lib/utils/functions/date'; import { calDateDiff } from '$lib/utils/functions/date';
import { currentOrg, currentOrgPath } from '$lib/utils/store/org'; import { currentOrg } from '$lib/utils/store/org';
import { basePath } from '$lib/utils/store/app';
import { CommunityListLoader, AskCommunityButton } from '../components'; import { CommunityListLoader, AskCommunityButton } from '../components';
import { communityApi } from '../api/community.svelte'; import { communityApi } from '../api/community.svelte';
import type { CommunityQuestionData } from '../utils/types'; import type { CommunityQuestionData } from '../utils/types';
interface Props { interface Props {
isLMS?: boolean;
questions?: CommunityQuestionData; questions?: CommunityQuestionData;
} }
let { isLMS = false, questions }: Props = $props(); let { questions }: Props = $props();
let searchValue = $state(''); let searchValue = $state('');
let selectedId = $state(''); let selectedId = $state('');
@@ -80,7 +80,7 @@
{#each filteredQuestions as question} {#each filteredQuestions as question}
<Item.Root variant="outline"> <Item.Root variant="outline">
{#snippet child({ props })} {#snippet child({ props })}
<a href="{isLMS ? '/lms' : $currentOrgPath}/community/{question.slug}" {...props}> <a href="{$basePath}/community/{question.slug}" {...props}>
<Vote value={question.votes} /> <Vote value={question.votes} />
<Item.Content class="gap-y-0.5"> <Item.Content class="gap-y-0.5">
<Item.Title class="mt-0"> <Item.Title class="mt-0">
@@ -31,7 +31,7 @@
import { courseApi } from '$features/course/api'; import { courseApi } from '$features/course/api';
import { t } from '$lib/utils/functions/translations'; import { t } from '$lib/utils/functions/translations';
import { currentOrg, isFreePlan, currentOrgPath } from '$lib/utils/store/org'; import { currentOrg, isFreePlan, currentOrgPath } from '$lib/utils/store/org';
import { globalStore } from '$lib/utils/store/app'; import { isStudentExperience } from '$lib/utils/store/app';
import { getNavItemRoute, getLessonsRoute } from '$features/course/utils/functions'; import { getNavItemRoute, getLessonsRoute } from '$features/course/utils/functions';
import { useSidebar } from '@cio/ui/base/sidebar'; import { useSidebar } from '@cio/ui/base/sidebar';
import { IconButton } from '@cio/ui/custom/icon-button'; import { IconButton } from '@cio/ui/custom/icon-button';
@@ -45,7 +45,7 @@
let { path, id, isStudent = false }: Props = $props(); let { path, id, isStudent = false }: Props = $props();
const coursesListPath = $derived($globalStore.isStudent ? '/lms/mylearning' : `${$currentOrgPath}/courses`); const coursesListPath = $derived($isStudentExperience ? '/lms/mylearning' : `${$currentOrgPath}/courses`);
const contentData = $derived(getCourseContent(courseApi.course)); const contentData = $derived(getCourseContent(courseApi.course));
const sidebar = useSidebar(); const sidebar = useSidebar();
const contentCount = $derived.by(() => { const contentCount = $derived.by(() => {
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { globalStore } from '$lib/utils/store/app'; import { basePath, globalStore } from '$lib/utils/store/app';
import * as DropdownMenu from '@cio/ui/base/dropdown-menu'; import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
import RocketIcon from '@lucide/svelte/icons/rocket'; import RocketIcon from '@lucide/svelte/icons/rocket';
import BellPlusIcon from '@lucide/svelte/icons/bell-plus'; import BellPlusIcon from '@lucide/svelte/icons/bell-plus';
@@ -11,8 +11,6 @@
import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down'; import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
import ThemeToggle from './theme-toggle.svelte'; import ThemeToggle from './theme-toggle.svelte';
import { currentOrgPath } from '$lib/utils/store/org';
import { t } from '$lib/utils/functions/translations'; import { t } from '$lib/utils/functions/translations';
import { logout } from '$lib/utils/functions/logout'; import { logout } from '$lib/utils/functions/logout';
import { profile } from '$lib/utils/store/user'; import { profile } from '$lib/utils/store/user';
@@ -38,10 +36,7 @@
{#snippet usertrigger()} {#snippet usertrigger()}
<DropdownMenu.Label class="p-0"> <DropdownMenu.Label class="p-0">
<a <a href={`${$basePath}/settings`} class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
href={`${!$globalStore.isOrgSite ? $currentOrgPath : '/lms'}/settings`}
class="flex items-center gap-2 px-1 py-1.5 text-left text-sm"
>
{@render avatarblock()} {@render avatarblock()}
</a> </a>
</DropdownMenu.Label> </DropdownMenu.Label>
@@ -2,11 +2,9 @@
import * as Avatar from '@cio/ui/base/avatar'; import * as Avatar from '@cio/ui/base/avatar';
import * as Sidebar from '@cio/ui/base/sidebar'; import * as Sidebar from '@cio/ui/base/sidebar';
import { Skeleton } from '@cio/ui/base/skeleton'; import { Skeleton } from '@cio/ui/base/skeleton';
import { currentOrg, currentOrgPath } from '$lib/utils/store/org'; import { currentOrg } from '$lib/utils/store/org';
import { shortenName } from '$lib/utils/functions/string'; import { shortenName } from '$lib/utils/functions/string';
import { globalStore } from '$lib/utils/store/app'; import { basePath } from '$lib/utils/store/app';
const orgPath = $derived($globalStore.isOrgSite ? '/lms' : $currentOrgPath);
</script> </script>
<Sidebar.Menu> <Sidebar.Menu>
@@ -16,7 +14,7 @@
class="ui:data-[state=open]:bg-sidebar-accent ui:data-[state=open]:text-sidebar-accent-foreground" class="ui:data-[state=open]:bg-sidebar-accent ui:data-[state=open]:text-sidebar-accent-foreground"
> >
{#snippet child({ props })} {#snippet child({ props })}
<a href={orgPath} {...props}> <a href={$basePath} {...props}>
{#if $currentOrg.name} {#if $currentOrg.name}
<Avatar.Root class="flex size-6! items-center justify-center rounded-md!"> <Avatar.Root class="flex size-6! items-center justify-center rounded-md!">
<Avatar.Image src={$currentOrg.avatarUrl} alt={$currentOrg.name} /> <Avatar.Image src={$currentOrg.avatarUrl} alt={$currentOrg.name} />
+20 -1
View File
@@ -1,4 +1,6 @@
import { writable } from 'svelte/store'; import { derived, writable } from 'svelte/store';
import { PUBLIC_IS_SELFHOSTED } from '$env/static/public';
import { currentOrgPath } from './org';
export const globalStore = writable<{ export const globalStore = writable<{
isDark: boolean; isDark: boolean;
@@ -11,3 +13,20 @@ export const globalStore = writable<{
orgSiteName: '', orgSiteName: '',
isStudent: undefined isStudent: undefined
}); });
/**
* Single source of truth: is this a student experience?
* Cloud: orgSite is always student. Self-hosted: check role.
*/
export const isStudentExperience = derived(globalStore, ($gs) => {
const isCloud = PUBLIC_IS_SELFHOSTED !== 'true';
if (isCloud) return $gs.isOrgSite;
return $gs.isStudent ?? false;
});
/**
* The root path for navigation: '/lms' for students, '/org/{siteName}' for admin/teacher
*/
export const basePath = derived([isStudentExperience, currentOrgPath], ([$isStudent, $orgPath]) =>
$isStudent ? '/lms' : $orgPath
);
@@ -15,12 +15,12 @@
<Page.Title>{$t('community.title')}</Page.Title> <Page.Title>{$t('community.title')}</Page.Title>
</Page.HeaderContent> </Page.HeaderContent>
<Page.Action> <Page.Action>
<AskCommunityButton isLMS={true} /> <AskCommunityButton />
</Page.Action> </Page.Action>
</Page.Header> </Page.Header>
<Page.Body> <Page.Body>
{#snippet child()} {#snippet child()}
<CommunityPage isLMS={true} /> <CommunityPage />
{/snippet} {/snippet}
</Page.Body> </Page.Body>
</Page.Root> </Page.Root>
@@ -8,4 +8,4 @@
<title>Question - ClassroomIO</title> <title>Question - ClassroomIO</title>
</svelte:head> </svelte:head>
<CommunityQuestionPage slug={data.slug} isLMS={true} /> <CommunityQuestionPage slug={data.slug} />
@@ -16,7 +16,7 @@
</Page.Header> </Page.Header>
<Page.Body> <Page.Body>
{#snippet child()} {#snippet child()}
<AskCommunityPage isLMS={true} /> <AskCommunityPage />
{/snippet} {/snippet}
</Page.Body> </Page.Body>
</Page.Root> </Page.Root>
@@ -8,4 +8,4 @@
<title>Question - ClassroomIO</title> <title>Question - ClassroomIO</title>
</svelte:head> </svelte:head>
<CommunityQuestionPage slug={data.slug} question={data.question} isLMS={false} /> <CommunityQuestionPage slug={data.slug} question={data.question} />
@@ -16,7 +16,7 @@
</Page.Header> </Page.Header>
<Page.Body> <Page.Body>
{#snippet child()} {#snippet child()}
<AskCommunityPage isLMS={false} /> <AskCommunityPage />
{/snippet} {/snippet}
</Page.Body> </Page.Body>
</Page.Root> </Page.Root>