Merge branch 'main' of https://github.com/classroomio/classroomio into feat/release-v2
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
# Prompt
|
||||||
|
|
||||||
|
This prompt is used to give any AI chat bot context about ClassroomIO
|
||||||
|
|
||||||
|
```txt
|
||||||
|
I am building a LMS for teachers and students to collaborate on a course. Students can enroll in a course or be giving access to a course. Teachers can be invited into an org by an admin to create courses and manage students. For auth we support Email/Password and Google.
|
||||||
|
|
||||||
|
The tech stack is
|
||||||
|
- Sveltekit - dashboard (teacher view) and lms (student view)
|
||||||
|
- Supabase - Auth, DB requests, Storage
|
||||||
|
- Hono - API for long running processes like video uploads
|
||||||
|
```
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Handle } from '@sveltejs/kit';
|
import type { Handle } from '@sveltejs/kit';
|
||||||
import { validateUser } from '$lib/utils/services/middlewares';
|
import { validateUser } from '$lib/utils/services/middlewares';
|
||||||
|
|
||||||
const PUBLIC_ROUTES = [
|
const PUBLIC_API_ROUTES = [
|
||||||
'/api/completion',
|
'/api/completion',
|
||||||
'student_prove_payment',
|
'student_prove_payment',
|
||||||
'teacher_student_buycourse',
|
'teacher_student_buycourse',
|
||||||
@@ -11,7 +11,7 @@ const PUBLIC_ROUTES = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function isPublicRoute(pathname: string) {
|
function isPublicRoute(pathname: string) {
|
||||||
return PUBLIC_ROUTES.some((route) => pathname.includes(route));
|
return PUBLIC_API_ROUTES.some((route) => pathname.includes(route));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handle: Handle = async ({ event, resolve }) => {
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
|
|||||||
+7
-3
@@ -2,8 +2,11 @@
|
|||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import getCurrencyFormatter from '$lib/utils/functions/getCurrencyFormatter';
|
import getCurrencyFormatter from '$lib/utils/functions/getCurrencyFormatter';
|
||||||
import { isCourseFree } from '$lib/utils/functions/course';
|
import {
|
||||||
import { getStudentInviteLink } from '$lib/utils/functions/course';
|
isCourseFree,
|
||||||
|
getStudentInviteLink,
|
||||||
|
calcCourseDiscount
|
||||||
|
} from '$lib/utils/functions/course';
|
||||||
import { currentOrg, currentOrgDomain } from '$lib/utils/store/org';
|
import { currentOrg, currentOrgDomain } from '$lib/utils/store/org';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
|
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
|
||||||
@@ -12,7 +15,6 @@
|
|||||||
import { ROLE } from '$lib/utils/constants/roles';
|
import { ROLE } from '$lib/utils/constants/roles';
|
||||||
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
import { calcCourseDiscount } from '$lib/utils/functions/course';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -48,8 +50,10 @@
|
|||||||
course_cost: courseData.cost,
|
course_cost: courseData.cost,
|
||||||
course_free: isFree
|
course_free: isFree
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isFree) {
|
if (isFree) {
|
||||||
const link = getStudentInviteLink(courseData, $currentOrg.siteName, $currentOrgDomain);
|
const link = getStudentInviteLink(courseData, $currentOrg.siteName, $currentOrgDomain);
|
||||||
|
|
||||||
goto(link);
|
goto(link);
|
||||||
} else {
|
} else {
|
||||||
openModal = true;
|
openModal = true;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { globalStore } from '$lib/utils/store/app';
|
import { globalStore } from '$lib/utils/store/app';
|
||||||
import { user, profile, defaultProfileState, defaultUserState } from '$lib/utils/store/user';
|
import { profile } from '$lib/utils/store/user';
|
||||||
import { currentOrg, orgs, defaultCurrentOrgState, currentOrgDomain, currentOrgPath } from '$lib/utils/store/org';
|
import { currentOrg, currentOrgDomain, currentOrgPath } from '$lib/utils/store/org';
|
||||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||||
import SettingsIcon from '@lucide/svelte/icons/settings';
|
import SettingsIcon from '@lucide/svelte/icons/settings';
|
||||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||||
@@ -9,31 +9,13 @@
|
|||||||
import LogOutIcon from '@lucide/svelte/icons/log-out';
|
import LogOutIcon from '@lucide/svelte/icons/log-out';
|
||||||
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';
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import { profileMenu } from '../store';
|
import { profileMenu } from '../store';
|
||||||
import { supabase } from '$lib/utils/functions/supabase';
|
|
||||||
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
import posthog from 'posthog-js';
|
import { logout } from '$lib/utils/functions/logout';
|
||||||
|
|
||||||
async function logout() {
|
|
||||||
const { error } = await supabase.auth.signOut();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error logging out: ', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentOrg.set(defaultCurrentOrgState);
|
|
||||||
orgs.set([]);
|
|
||||||
user.set(defaultUserState);
|
|
||||||
profile.set(defaultProfileState);
|
|
||||||
|
|
||||||
capturePosthogEvent('user_logged_out');
|
|
||||||
posthog.reset();
|
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
await logout();
|
||||||
closeMenu();
|
closeMenu();
|
||||||
|
|
||||||
goto('/login');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeMenu() {
|
function closeMenu() {
|
||||||
@@ -153,7 +135,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button onclick={logout} class="w-full space-y-4 pt-3">
|
<button onclick={handleLogout} class="w-full space-y-4 pt-3">
|
||||||
<span class="flex items-center gap-2">
|
<span class="flex items-center gap-2">
|
||||||
<LogOutIcon size={16} />
|
<LogOutIcon size={16} />
|
||||||
<p class="text-sm font-semibold">{$t('settings.profile.logout')}</p>
|
<p class="text-sm font-semibold">{$t('settings.profile.logout')}</p>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import posthog from 'posthog-js';
|
import posthog from 'posthog-js';
|
||||||
import { supabase } from '$lib/utils/functions/supabase';
|
import { supabase } from '$lib/utils/functions/supabase';
|
||||||
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
||||||
import { currentOrg, orgs, defaultCurrentOrgState } from '$lib/utils/store/org';
|
|
||||||
import { user, profile, defaultProfileState, defaultUserState } from '$lib/utils/store/user';
|
import { user, profile, defaultProfileState, defaultUserState } from '$lib/utils/store/user';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
@@ -12,8 +11,6 @@ export async function logout(redirect = true) {
|
|||||||
console.error('Error logging out: ', error);
|
console.error('Error logging out: ', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
currentOrg.set(defaultCurrentOrgState);
|
|
||||||
orgs.set([]);
|
|
||||||
user.set(defaultUserState);
|
user.set(defaultUserState);
|
||||||
profile.set(defaultProfileState);
|
profile.set(defaultProfileState);
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ export const getSupabase = () => {
|
|||||||
return supabase;
|
return supabase;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const hasSession = async () => {
|
||||||
|
const { data } = await getSupabase().auth.getSession();
|
||||||
|
console.log('has session', data);
|
||||||
|
|
||||||
|
return data.session !== null;
|
||||||
|
};
|
||||||
|
|
||||||
export const isSupabaseTokenInLocalStorage = () => {
|
export const isSupabaseTokenInLocalStorage = () => {
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
const key = localStorage.key(i);
|
const key = localStorage.key(i);
|
||||||
|
|||||||
@@ -100,11 +100,9 @@ export const load = async ({ url, cookies, request }): Promise<LoadOutput> => {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
const answer = !!subdomain;
|
|
||||||
|
|
||||||
console.log('subdomain', subdomain);
|
console.log('subdomain', subdomain);
|
||||||
|
|
||||||
response.isOrgSite = debugMode || answer;
|
response.isOrgSite = debugMode || !!subdomain;
|
||||||
response.orgSiteName = debugMode ? _orgSiteName : subdomain;
|
response.orgSiteName = debugMode ? _orgSiteName : subdomain;
|
||||||
response.org = (await getCurrentOrg(response.orgSiteName, true)) || null;
|
response.org = (await getCurrentOrg(response.orgSiteName, true)) || null;
|
||||||
|
|
||||||
@@ -172,11 +170,16 @@ function getBaseMetaTags(url: URL) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSubdomain(url: URL) {
|
function getSubdomain(url: URL) {
|
||||||
const host = url.host.replace('www.', '');
|
const host = url.hostname.replace('www.', '');
|
||||||
const parts = host?.split('.');
|
const parts = host.split('.');
|
||||||
|
const appHost = env.PRIVATE_APP_HOST;
|
||||||
|
|
||||||
if (host?.endsWith(env.PRIVATE_APP_HOST)) {
|
const appHostParts = appHost.split('.');
|
||||||
return parts?.length >= 3 ? parts[0] : null;
|
const isAppHost = parts.slice(-appHostParts.length).join('.') === appHost;
|
||||||
|
|
||||||
|
if (isAppHost) {
|
||||||
|
// Subdomain exists only if extra part(s) before main domain
|
||||||
|
return parts.length > appHostParts.length ? parts[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { browser } from '$app/environment';
|
// import { browser } from '$app/environment';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
@@ -28,11 +28,14 @@
|
|||||||
import { globalStore } from '$lib/utils/store/app';
|
import { globalStore } from '$lib/utils/store/app';
|
||||||
import { currentOrg } from '$lib/utils/store/org';
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
import { isMobile } from '$lib/utils/store/useMobile';
|
import { isMobile } from '$lib/utils/store/useMobile';
|
||||||
import { Theme } from 'carbon-components-svelte';
|
// import { Theme } from 'carbon-components-svelte';
|
||||||
import type { CarbonTheme } from 'carbon-components-svelte/types/Theme/Theme.svelte';
|
// import type { CarbonTheme } from 'carbon-components-svelte/types/Theme/Theme.svelte';
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { MetaTags } from 'svelte-meta-tags';
|
import { MetaTags } from 'svelte-meta-tags';
|
||||||
|
import isPublicRoute from '$lib/utils/functions/routes/isPublicRoute';
|
||||||
|
import { hasSession } from '$lib/utils/functions/supabase';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
import { ModeWatcher } from '@cio/ui/base/dark-mode';
|
import { ModeWatcher } from '@cio/ui/base/dark-mode';
|
||||||
|
|
||||||
@@ -43,7 +46,7 @@
|
|||||||
let supabase = getSupabase();
|
let supabase = getSupabase();
|
||||||
let path = $derived($page.url?.pathname?.replace('/', ''));
|
let path = $derived($page.url?.pathname?.replace('/', ''));
|
||||||
let queryParam = $page.url?.search;
|
let queryParam = $page.url?.search;
|
||||||
let carbonTheme: CarbonTheme = $derived($globalStore.isDark ? 'g100' : 'white');
|
// let carbonTheme: CarbonTheme = $derived($globalStore.isDark ? 'g100' : 'white');
|
||||||
|
|
||||||
function handleResize() {
|
function handleResize() {
|
||||||
isMobile.update(() => window.innerWidth <= 760);
|
isMobile.update(() => window.innerWidth <= 760);
|
||||||
@@ -51,33 +54,43 @@
|
|||||||
|
|
||||||
const getProfileDebounced = debounce(getProfile, 1000);
|
const getProfileDebounced = debounce(getProfile, 1000);
|
||||||
|
|
||||||
onMount(() => {
|
function pageSetup() {
|
||||||
console.log(
|
console.log(
|
||||||
'Welcome to ClassroomIO, we are grateful you chose us.',
|
'Welcome to ClassroomIO, we are grateful you chose us.',
|
||||||
$page.url.host,
|
$page.url.host,
|
||||||
`\nIs student domain: ${data.isOrgSite}`
|
`\nIs student domain: ${data.isOrgSite}`,
|
||||||
|
data
|
||||||
);
|
);
|
||||||
|
|
||||||
if (browser) {
|
// $globalStore.isDark = localStorage.getItem('mode') === 'dark';
|
||||||
// Update theme - dark or light mode
|
// toggleBodyByMode($globalStore.isDark);
|
||||||
// $globalStore.isDark = localStorage.getItem('mode') === 'dark';
|
|
||||||
// toggleBodyByMode($globalStore.isDark);
|
|
||||||
|
|
||||||
if (data.isOrgSite && data.org?.theme) {
|
|
||||||
setTheme(data.org?.theme);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setupAnalytics();
|
setupAnalytics();
|
||||||
|
|
||||||
handleResize();
|
handleResize();
|
||||||
|
|
||||||
// if (!isSupabaseTokenInLocalStorage() && !isPublicRoute($page.url?.pathname)) {
|
if (!data.isOrgSite || !data.org) return;
|
||||||
// console.log('No auth token and is not a public route, redirect to login', path);
|
|
||||||
// return goto('/login?redirect=/' + path);
|
$globalStore.orgSiteName = data.orgSiteName;
|
||||||
// }
|
$globalStore.isOrgSite = data.isOrgSite;
|
||||||
|
|
||||||
|
currentOrg.set(data.org);
|
||||||
|
|
||||||
|
// Setup internal analytics
|
||||||
|
initOrgAnalytics(data.orgSiteName);
|
||||||
|
|
||||||
|
setTheme(data.org?.theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
pageSetup();
|
||||||
|
|
||||||
|
if (!hasSession() && !isPublicRoute($page.url?.pathname)) {
|
||||||
|
console.log('No auth token and is not a public route, redirect to login', path);
|
||||||
|
return goto('/login?redirect=/' + path);
|
||||||
|
}
|
||||||
|
|
||||||
const { data: authListener } = supabase.auth.onAuthStateChange((event) => {
|
const { data: authListener } = supabase.auth.onAuthStateChange((event) => {
|
||||||
// Log key events
|
|
||||||
console.log(`event`, event);
|
console.log(`event`, event);
|
||||||
|
|
||||||
if (path.includes('reset')) {
|
if (path.includes('reset')) {
|
||||||
@@ -85,10 +98,8 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip Authentication
|
|
||||||
if (data.skipAuth) return;
|
if (data.skipAuth) return;
|
||||||
|
|
||||||
// Authentication Steps
|
|
||||||
if (event === 'SIGNED_IN' || event === 'INITIAL_SESSION') {
|
if (event === 'SIGNED_IN' || event === 'INITIAL_SESSION') {
|
||||||
getProfileDebounced({
|
getProfileDebounced({
|
||||||
path,
|
path,
|
||||||
@@ -97,22 +108,8 @@
|
|||||||
orgSiteName: data.orgSiteName
|
orgSiteName: data.orgSiteName
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// else if (!['TOKEN_REFRESHED'].includes(event)) {
|
|
||||||
// console.log('not logged in, go to login');
|
|
||||||
// return goto('/login');
|
|
||||||
// }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.isOrgSite && data.org && !$currentOrg.siteName) {
|
|
||||||
$globalStore.orgSiteName = data.orgSiteName;
|
|
||||||
$globalStore.isOrgSite = data.isOrgSite;
|
|
||||||
|
|
||||||
currentOrg.set(data.org);
|
|
||||||
|
|
||||||
// Setup internal analytics
|
|
||||||
initOrgAnalytics(data.orgSiteName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
console.log('unsubscribed');
|
console.log('unsubscribed');
|
||||||
authListener.subscription.unsubscribe();
|
authListener.subscription.unsubscribe();
|
||||||
|
|||||||
@@ -110,6 +110,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
// check if user has session, if not redirect to sign up with redirect back to this page
|
||||||
|
const {
|
||||||
|
data: { session }
|
||||||
|
} = await supabase.auth.getSession();
|
||||||
|
if (!session) {
|
||||||
|
return goto(`/login?redirect=${$page.url?.pathname || ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
setTheme(data.currentOrg?.theme || '');
|
setTheme(data.currentOrg?.theme || '');
|
||||||
setCurOrg(data.currentOrg as CurrentOrg);
|
setCurOrg(data.currentOrg as CurrentOrg);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user