feat: centralize baseURL and app experience for cloud and self hosted
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
+2
-2
@@ -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} />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user