feat: redo settings and all org pages. (#601)
* feat: new components, deploy storybook * refactor: simplify component field definitions and enhance button loading state - Removed unnecessary type imports and simplified the FIELDS array definitions across multiple components in Storybook. - Updated button component to use a loading state instead of isLoading, improving clarity and consistency in props. - Adjusted button stories to reflect the new loading prop and enhanced the button's loading visual feedback. - Minor CSS adjustments for better layout and styling consistency. * chore: add field, card, password components * feat: improve page component & update AuthUI path * chore: update bits-ui to version 2.14.4 and all org related UI's - Updated bits-ui dependency in pnpm-lock.yaml and package.json to version 2.14.4. - Refactored imports for RoleBasedSecurity and UpgradeBanner components to use the new ui feature directory. - Removed unused Course components and related files to clean up the codebase. - Enhanced various components to improve consistency and maintainability. * chore: move all setting buttons to the top and update auth ui styles * chore: remove svelte-loading-spinners and replace with @cio/ui/base/spinner - Removed all instances of svelte-loading-spinners from the project. - Updated components to use the new Spinner from @cio/ui/base/spinner. - Cleaned up pnpm-lock.yaml and package.json to reflect the removal of svelte-loading-spinners. - Added new CSS animations for rocket launch effects in app.css. - Enhanced email change flow with updated verification messages in translations. * chore: remove supabase file upload from dashboard and move to r2 * chore: update organization API and UI components - Added new API methods for organization plan operations and team management. - Introduced middleware for API key authentication and organization admin checks. - Refactored organization-related components to utilize the new API structure. - Updated various UI components to improve consistency and maintainability. - Enhanced error handling and validation for organization-related actions. - Removed deprecated services and streamlined the codebase.
This commit is contained in:
@@ -16,11 +16,12 @@ export const ROUTE = {
|
||||
ASK: '/ask',
|
||||
FORGOT: '/forgot',
|
||||
RESET: '/reset',
|
||||
LOGOUT: '/logout'
|
||||
LOGOUT: '/logout',
|
||||
AUTH_FAILED: '/auth-failed'
|
||||
};
|
||||
|
||||
export const PUBLIC_ROUTES = [
|
||||
// `^${ROUTE.HOME}$`,
|
||||
`^${ROUTE.HOME}$`,
|
||||
ROUTE.LOGIN,
|
||||
ROUTE.SIGN_UP,
|
||||
`^${ROUTE.INVITE_TEACHER}/.*`,
|
||||
@@ -28,7 +29,8 @@ export const PUBLIC_ROUTES = [
|
||||
ROUTE.RESET,
|
||||
`^${ROUTE.PAGES}/.*`,
|
||||
`^${ROUTE.COURSE}/.*`,
|
||||
'/404'
|
||||
'/404',
|
||||
ROUTE.AUTH_FAILED
|
||||
];
|
||||
|
||||
export const PUBLIC_API_ROUTES = [
|
||||
|
||||
@@ -28,17 +28,36 @@ export function isLMSPage(path) {
|
||||
return /lms[/a-z 0-9 -]*/.test(path);
|
||||
}
|
||||
|
||||
export function isActive(pagePath: string, itemPath: string, exact: boolean = false) {
|
||||
const pageLinkItems = pagePath.split('/');
|
||||
const itemLinkItems = itemPath.split('/');
|
||||
export function isActive(pagePath: string, itemPath: string, matchPattern?: string, exact: boolean = false) {
|
||||
// If a regex pattern is provided, use it for matching
|
||||
if (matchPattern) {
|
||||
try {
|
||||
const regex = new RegExp(matchPattern);
|
||||
return regex.test(pagePath);
|
||||
} catch (error) {
|
||||
console.warn('Invalid regex pattern in isActive:', matchPattern, error);
|
||||
// Fall through to default matching logic
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize paths by removing trailing slashes (except root)
|
||||
const normalizedPagePath = pagePath === '/' ? '/' : pagePath.replace(/\/$/, '');
|
||||
const normalizedItemPath = itemPath === '/' ? '/' : itemPath.replace(/\/$/, '');
|
||||
|
||||
if (exact) {
|
||||
return pagePath === itemPath;
|
||||
return normalizedPagePath === normalizedItemPath;
|
||||
}
|
||||
|
||||
if (itemLinkItems.length !== pageLinkItems.length) {
|
||||
return false;
|
||||
// Exact match
|
||||
if (normalizedPagePath === normalizedItemPath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return pagePath.includes(itemPath);
|
||||
// Check if pagePath is a nested route of itemPath
|
||||
// e.g., /org/[slug]/courses matches /org/[slug]/courses/123
|
||||
// We need to ensure we match at path segment boundaries
|
||||
const pagePathWithSlash = normalizedPagePath + '/';
|
||||
const itemPathWithSlash = normalizedItemPath + '/';
|
||||
|
||||
return pagePathWithSlash.startsWith(itemPathWithSlash);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
|
||||
export const sanitizeDomain = (domain: string) => {
|
||||
return domain
|
||||
.trim()
|
||||
@@ -9,15 +7,11 @@ export const sanitizeDomain = (domain: string) => {
|
||||
};
|
||||
|
||||
export async function sendDomainRequest(key: string, domain: string): Promise<Response> {
|
||||
const { data } = await supabase.auth.getSession();
|
||||
const accessToken = data.session?.access_token || '';
|
||||
|
||||
return fetch('/api/domain', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ params: { key, domain } }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `${accessToken}`
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { DEFAULT_CONFIG } from './constants';
|
||||
import { delay } from './utils';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { hcWithType } from '@cio/api/rpc-types';
|
||||
import { get } from 'svelte/store';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
|
||||
class ApiClient {
|
||||
private config: Required<ApiClientConfig>;
|
||||
@@ -26,9 +28,17 @@ class ApiClient {
|
||||
headers.set('Accept', 'application/json');
|
||||
}
|
||||
|
||||
// Add organization ID header if available
|
||||
const org = get(currentOrg);
|
||||
if (org?.id) {
|
||||
headers.set('cio-org-id', org.id);
|
||||
}
|
||||
|
||||
// Handle body serialization and content-type
|
||||
let requestBody = fetchConfig.body;
|
||||
if (fetchConfig.body && typeof fetchConfig.body === 'object' && !(fetchConfig.body instanceof FormData)) {
|
||||
const isFormData = fetchConfig.body instanceof FormData;
|
||||
|
||||
if (fetchConfig.body && typeof fetchConfig.body === 'object' && !isFormData) {
|
||||
// Auto-stringify objects (except FormData)
|
||||
requestBody = JSON.stringify(fetchConfig.body);
|
||||
if (!headers.has('Content-Type')) {
|
||||
@@ -47,7 +57,7 @@ class ApiClient {
|
||||
|
||||
const requestInit: RequestInit = {
|
||||
...fetchConfig,
|
||||
headers,
|
||||
headers: isFormData ? undefined : headers,
|
||||
body: requestBody,
|
||||
signal: controller.signal
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
/**
|
||||
* Server-side helper to get API key headers for RPC client calls
|
||||
* Use this in server-side files (+layout.server.ts, +page.server.ts, +server.ts)
|
||||
* when making API calls that don't have user session cookies
|
||||
*
|
||||
* @returns Headers object with Authorization Bearer token
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const response = await classroomio.organization.$get(
|
||||
* { query: { siteName } },
|
||||
* getApiKeyHeaders()
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export function getApiKeyHeaders(): { headers: { Authorization: string } } {
|
||||
if (!env.PRIVATE_SERVER_KEY) {
|
||||
throw new Error('PRIVATE_SERVER_KEY is not configured in environment variables');
|
||||
}
|
||||
|
||||
return {
|
||||
headers: {
|
||||
Authorization: `Bearer ${env.PRIVATE_SERVER_KEY}`
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { get } from 'svelte/store';
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
import { isUUID } from '$lib/utils/functions/isUUID';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { uploadImage } from '$lib/utils/services/upload';
|
||||
|
||||
export async function fetchCourses(profileId, orgId) {
|
||||
if (!orgId || !profileId) return;
|
||||
@@ -184,42 +185,10 @@ export async function fetchGroup(groupId: Group['id']) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function uploadAvatar(courseId: string, avatar: string) {
|
||||
const filename = `course/${courseId + Date.now()}.webp`;
|
||||
let logo;
|
||||
|
||||
const { data } = await supabase.storage.from('avatars').upload(filename, avatar, {
|
||||
cacheControl: '3600',
|
||||
upsert: false
|
||||
});
|
||||
|
||||
if (data) {
|
||||
const { data } = supabase.storage.from('avatars').getPublicUrl(filename);
|
||||
|
||||
if (!data.publicUrl) return;
|
||||
|
||||
logo = data.publicUrl;
|
||||
}
|
||||
|
||||
return logo;
|
||||
}
|
||||
|
||||
export async function updateCourse(courseId: Course['id'], avatar: string | undefined, course: Partial<Course>) {
|
||||
export async function updateCourse(courseId: Course['id'], avatar: File | undefined, course: Partial<Course>) {
|
||||
if (avatar && courseId) {
|
||||
const filename = `course/${courseId + Date.now()}.webp`;
|
||||
|
||||
const { data } = await supabase.storage.from('avatars').upload(filename, avatar, {
|
||||
cacheControl: '3600',
|
||||
upsert: false
|
||||
});
|
||||
|
||||
if (data) {
|
||||
const { data: response } = supabase.storage.from('avatars').getPublicUrl(filename);
|
||||
|
||||
if (!response.publicUrl) return;
|
||||
|
||||
course.logo = response.publicUrl;
|
||||
}
|
||||
const logo = await uploadImage(avatar);
|
||||
course.logo = logo;
|
||||
}
|
||||
|
||||
await supabase.from('course').update(course).match({ id: courseId });
|
||||
|
||||
@@ -1,308 +0,0 @@
|
||||
import type { CurrentOrg, OrgTeamMember } from '$lib/utils/types/org';
|
||||
import { currentOrg, orgAudience, orgTeam, orgs } from '$lib/utils/store/org';
|
||||
|
||||
import type { OrganizationPlan } from '$lib/utils/types';
|
||||
import type { PostgrestError } from '@supabase/supabase-js';
|
||||
import { ROLE } from '@cio/utils/constants';
|
||||
import { ROLE_LABEL } from '$lib/utils/constants/roles';
|
||||
import { get } from 'svelte/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
|
||||
export async function getOrgTeam(orgId: string) {
|
||||
const { data, error } = await supabase
|
||||
.from('organizationmember')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
email,
|
||||
verified,
|
||||
role_id,
|
||||
profile(
|
||||
id,
|
||||
fullname,
|
||||
email
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('organization_id', orgId)
|
||||
.neq('role_id', ROLE.STUDENT)
|
||||
.order('id', { ascending: false })
|
||||
.returns<
|
||||
{
|
||||
id: number;
|
||||
email: string;
|
||||
verified: boolean;
|
||||
role_id: number;
|
||||
profile: {
|
||||
id: string;
|
||||
fullname: string;
|
||||
email: string;
|
||||
};
|
||||
}[]
|
||||
>();
|
||||
|
||||
const team: OrgTeamMember[] = [];
|
||||
if (data?.length) {
|
||||
data.forEach((teamMember) => {
|
||||
team.push({
|
||||
id: teamMember.id,
|
||||
email: teamMember?.profile?.email || teamMember.email,
|
||||
verified: teamMember.verified,
|
||||
profileId: teamMember?.profile?.id,
|
||||
fullname: teamMember?.profile?.fullname || '',
|
||||
role: ROLE_LABEL[teamMember?.role_id] || '',
|
||||
isAdmin: teamMember?.role_id === ROLE.ADMIN
|
||||
});
|
||||
});
|
||||
|
||||
orgTeam.set(team);
|
||||
}
|
||||
|
||||
return {
|
||||
team: get(orgTeam),
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
export async function getOrganizations(userId: string, isOrgSite?: boolean, orgSiteName?: string) {
|
||||
const { data, error } = await supabase
|
||||
.from('organizationmember')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
profile_id,
|
||||
role_id,
|
||||
created_at,
|
||||
organization!organizationmember_organization_id_fkey (
|
||||
*,
|
||||
organization_plan(
|
||||
plan_name,
|
||||
is_active,
|
||||
provider,
|
||||
subscriptionId:payload->id,
|
||||
customerId:payload->customerId
|
||||
)
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('profile_id', userId)
|
||||
.order('id', { ascending: false })
|
||||
.returns<
|
||||
{
|
||||
id: string;
|
||||
profile_id: string;
|
||||
role_id: string;
|
||||
created_at: string;
|
||||
organization: CurrentOrg;
|
||||
}[]
|
||||
>();
|
||||
|
||||
const orgsArray: CurrentOrg[] = [];
|
||||
|
||||
if (Array.isArray(data) && data.length) {
|
||||
data.forEach((orgMember) => {
|
||||
orgsArray.push({
|
||||
...(orgMember?.organization || {}),
|
||||
memberId: orgMember?.id,
|
||||
role_id: parseInt(orgMember?.role_id),
|
||||
shortName: orgMember?.organization?.name?.substring(0, 2)?.toUpperCase() || ''
|
||||
});
|
||||
});
|
||||
|
||||
orgs.set(orgsArray);
|
||||
|
||||
// If this is a student dashboard
|
||||
if (isOrgSite && orgSiteName) {
|
||||
const orgData = orgsArray.find((org) => org.siteName === orgSiteName);
|
||||
|
||||
if (orgData) {
|
||||
currentOrg.set(orgData);
|
||||
}
|
||||
} else {
|
||||
// Check if org was last visited in localhost
|
||||
if (localStorage) {
|
||||
const lastOrgSiteName = localStorage.getItem('classroomio_org_sitename');
|
||||
|
||||
const lastOrg = orgsArray.find((org) => org.siteName === lastOrgSiteName);
|
||||
|
||||
if (lastOrg) {
|
||||
currentOrg.set(lastOrg);
|
||||
}
|
||||
}
|
||||
|
||||
// Default to setting the first org in the array of orgs
|
||||
const _currentOrg = get(currentOrg);
|
||||
if (!_currentOrg.siteName) {
|
||||
currentOrg.set(orgsArray[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
orgs: orgsArray,
|
||||
currentOrg: get(currentOrg),
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
export async function getOrgAudience(orgId: string) {
|
||||
// get all students who are participants in any course belonging to an org
|
||||
const { data, error } = await supabase
|
||||
.from('profile')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
fullname,
|
||||
email,
|
||||
avatar_url,
|
||||
created_at,
|
||||
groupmember!inner(
|
||||
role_id,
|
||||
group_id:group!inner(
|
||||
organization_id
|
||||
)
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('groupmember.group.organization_id', orgId)
|
||||
.eq('groupmember.role_id', ROLE.STUDENT);
|
||||
|
||||
const audience = (data || []).map((profile) => ({
|
||||
id: profile.id,
|
||||
name: profile.fullname,
|
||||
email: profile.email,
|
||||
avatar_url: profile.avatar_url,
|
||||
date_joined: new Date(profile.created_at).toDateString()
|
||||
}));
|
||||
orgAudience.set(audience);
|
||||
|
||||
return {
|
||||
audience: audience,
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
export async function getCourseBySiteName(siteName: string) {
|
||||
const { data, error } = await supabase
|
||||
.from('course')
|
||||
.select(
|
||||
`
|
||||
*,
|
||||
lessons:lesson(count),
|
||||
group!inner(
|
||||
organization!inner(id, name, siteName, avatar_url)
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('group.organization.siteName', siteName)
|
||||
.eq('status', 'ACTIVE')
|
||||
.eq('is_published', true);
|
||||
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
const CURRENT_ORG_QUERY = `
|
||||
id,
|
||||
name,
|
||||
siteName,
|
||||
avatar_url,
|
||||
landingpage,
|
||||
is_restricted,
|
||||
customization,
|
||||
theme,
|
||||
favicon,
|
||||
customDomain,
|
||||
isCustomDomainVerified,
|
||||
customCode,
|
||||
organization_plan(
|
||||
plan_name,
|
||||
is_active
|
||||
)
|
||||
`;
|
||||
export async function getCurrentOrg(siteName: string, justGet = false, isCustomDomain = false) {
|
||||
let response: { data: CurrentOrg[] | null; error: PostgrestError | null } | null = null;
|
||||
|
||||
if (isCustomDomain) {
|
||||
response = await supabase
|
||||
.from('organization')
|
||||
.select(CURRENT_ORG_QUERY)
|
||||
.eq('customDomain', siteName)
|
||||
.filter('isCustomDomainVerified', 'eq', true)
|
||||
.returns<CurrentOrg[]>();
|
||||
} else {
|
||||
response = await supabase
|
||||
.from('organization')
|
||||
.select(CURRENT_ORG_QUERY)
|
||||
.eq('siteName', siteName)
|
||||
.returns<CurrentOrg[]>();
|
||||
}
|
||||
const { data, error } = response;
|
||||
|
||||
const isDataEmpty = !data?.[0];
|
||||
|
||||
if (!justGet && (error || isDataEmpty)) {
|
||||
console.error('Error getOrganization', error);
|
||||
return goto('/404');
|
||||
}
|
||||
|
||||
if (!justGet) {
|
||||
if (isDataEmpty) return;
|
||||
|
||||
currentOrg.set(data[0]);
|
||||
} else if (!isDataEmpty) {
|
||||
return data[0];
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateOrgPlan(params: {
|
||||
supabase: typeof supabase;
|
||||
subscriptionId: string;
|
||||
data: OrganizationPlan['payload'];
|
||||
}) {
|
||||
return await params.supabase
|
||||
.from('organization_plan')
|
||||
.update({
|
||||
payload: params.data
|
||||
})
|
||||
.match({
|
||||
subscription_id: params.subscriptionId
|
||||
});
|
||||
}
|
||||
|
||||
export async function createOrgPlan(params: {
|
||||
orgId: OrganizationPlan['org_id'];
|
||||
planName: OrganizationPlan['plan_name'];
|
||||
subscriptionId: OrganizationPlan['subscription_id'];
|
||||
triggeredBy: OrganizationPlan['triggered_by'];
|
||||
data: OrganizationPlan['payload'];
|
||||
supabase: typeof supabase;
|
||||
}) {
|
||||
return await params.supabase.from('organization_plan').insert({
|
||||
activated_at: new Date().toDateString(),
|
||||
org_id: params.orgId,
|
||||
triggered_by: params.triggeredBy,
|
||||
plan_name: params.planName,
|
||||
is_active: true,
|
||||
payload: params.data,
|
||||
subscription_id: params.subscriptionId,
|
||||
provider: 'polar'
|
||||
});
|
||||
}
|
||||
|
||||
export async function cancelOrgPlan(params: { subscriptionId: string; data: OrganizationPlan['payload'] }) {
|
||||
return await supabase
|
||||
.from('organization_plan')
|
||||
.update({
|
||||
is_active: false,
|
||||
deactivated_at: new Date().toDateString(),
|
||||
payload: params.data
|
||||
})
|
||||
.match({
|
||||
subscription_id: params.subscriptionId
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { classroomio } from './api';
|
||||
|
||||
/**
|
||||
* Uploads an image file to R2 storage and returns the public URL
|
||||
* @param file - The image file to upload
|
||||
* @returns The public URL of the uploaded image
|
||||
* @throws Error if upload fails
|
||||
*/
|
||||
export async function uploadImage(file: File): Promise<string> {
|
||||
try {
|
||||
const response = await classroomio.media.image.upload.$post({
|
||||
form: {
|
||||
file: file
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success || !data.url) {
|
||||
const errorMessage = 'error' in data ? data.error : 'Failed to upload image';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return data.url;
|
||||
} catch (error) {
|
||||
console.error('Error uploading image:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { OrgAudience, OrgTeamMember } from '../types/org';
|
||||
import { browser, dev } from '$app/environment';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
import type { AccountOrg } from '$lib/features/app/types';
|
||||
import type { OrgTeamMember } from '../types/org';
|
||||
import { PLAN } from '@cio/utils/plans';
|
||||
import { PUBLIC_IS_SELFHOSTED } from '$env/static/public';
|
||||
import { ROLE } from '@cio/utils/constants';
|
||||
@@ -10,7 +10,9 @@ import { STEPS } from '../constants/quiz';
|
||||
import type { UserLessonDataType } from '$lib/utils/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export const defaultCurrentOrgState: AccountOrg = {
|
||||
export const orgs = writable<AccountOrg[]>([]);
|
||||
|
||||
export const currentOrg: Writable<AccountOrg> = writable({
|
||||
avatarUrl: '',
|
||||
createdAt: '',
|
||||
customCode: '',
|
||||
@@ -31,12 +33,7 @@ export const defaultCurrentOrgState: AccountOrg = {
|
||||
settings: {},
|
||||
siteName: '',
|
||||
theme: ''
|
||||
};
|
||||
|
||||
export const orgs = writable<AccountOrg[]>([]);
|
||||
|
||||
export const currentOrg: Writable<AccountOrg> = writable(defaultCurrentOrgState);
|
||||
export const orgAudience = writable<OrgAudience[]>([]);
|
||||
});
|
||||
export const orgTeam = writable<OrgTeamMember[]>([]);
|
||||
export const isOrgAdmin = derived(currentOrg, ($currentOrg) => {
|
||||
if ($currentOrg.roleId === 0) return null;
|
||||
|
||||
@@ -120,7 +120,8 @@
|
||||
"email": "Email",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"email_change_verification_note": "A verification email has been sent to your current email address. The new email will not be in effect until you accept the verification email."
|
||||
"email_change_verification_note": "A verification email has been sent to your current email address. The new email will not be in effect until you accept the verification email.",
|
||||
"email_change_verification_sent": "A verification email has been sent to your new email address. Please check your inbox to verify your new email address."
|
||||
},
|
||||
"update_profile": "Update Profile",
|
||||
"logout": "Log out"
|
||||
@@ -308,7 +309,13 @@
|
||||
},
|
||||
|
||||
"setup": {
|
||||
"your_checklist": "Your Checklist",
|
||||
"get_started": "Get Started",
|
||||
"description": "Complete these steps to set up your learning platform",
|
||||
"progress": "Progress",
|
||||
"completed": "completed",
|
||||
"done": "Done",
|
||||
"todo": "Todo",
|
||||
"personal_profile": {
|
||||
"title": "Upload a profile picture and update username",
|
||||
"desc": "Personalize and add a human touch making interactions more personal and memorable",
|
||||
@@ -1379,6 +1386,7 @@
|
||||
"success_delete": "Deleted successfully",
|
||||
"something": "Something went wrong - Please try later",
|
||||
"update": "Update",
|
||||
"update_failed": "Update failed",
|
||||
|
||||
"poll": {
|
||||
"error": {
|
||||
|
||||
@@ -74,48 +74,6 @@ export interface OrgLandingPageJson {
|
||||
};
|
||||
}
|
||||
|
||||
export interface OrgCustomization {
|
||||
dashboard: {
|
||||
community: boolean;
|
||||
exercise: boolean;
|
||||
bannerImage: string;
|
||||
bannerText: string;
|
||||
};
|
||||
course: {
|
||||
newsfeed: boolean;
|
||||
grading: boolean;
|
||||
};
|
||||
apps: {
|
||||
poll: boolean;
|
||||
comments: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CurrentOrg {
|
||||
id: string;
|
||||
name: string;
|
||||
shortName: string;
|
||||
siteName: string;
|
||||
avatar_url: string;
|
||||
memberId: string;
|
||||
role_id: number;
|
||||
landingpage: OrgLandingPageJson;
|
||||
customization: OrgCustomization;
|
||||
theme: string;
|
||||
organization_plan: {
|
||||
provider: 'polar' | 'lmz';
|
||||
subscriptionId: string;
|
||||
customerId: string;
|
||||
plan_name: string;
|
||||
is_active: boolean;
|
||||
}[];
|
||||
is_restricted: boolean;
|
||||
customDomain?: string;
|
||||
isCustomDomainVerified?: boolean;
|
||||
customCode?: string;
|
||||
favicon?: string;
|
||||
}
|
||||
|
||||
export interface OrgTeamMember {
|
||||
id: number;
|
||||
email: string;
|
||||
|
||||
Reference in New Issue
Block a user