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:
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { AuthUI } from '$lib/features/ui';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Card from '@cio/ui/base/card';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
|
||||
const errorCode = $derived(new URLSearchParams(page.url.search).get('error'));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Reset Password - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<AuthUI isLogin={false} showOnlyContent={true}>
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<Avatar
|
||||
src={$currentOrg.avatarUrl ? $currentOrg.avatarUrl : '/logo-192.png'}
|
||||
name={$currentOrg.name ? $currentOrg.name : 'ClassroomIO'}
|
||||
shape="rounded-md"
|
||||
width="w-10"
|
||||
height="max-h-10"
|
||||
className="mr-2"
|
||||
/>
|
||||
|
||||
<p class="text-xl font-semibold">Something went wrong</p>
|
||||
|
||||
{#if errorCode}
|
||||
<Card.Description class="border px-2 py-1">
|
||||
CODE: {errorCode}
|
||||
</Card.Description>
|
||||
{/if}
|
||||
|
||||
<Card.Description class="text-center">
|
||||
We encountered an unexpected error. Please try again or return to the home page. If you're a developer, you can
|
||||
find more information about the error here.
|
||||
</Card.Description>
|
||||
|
||||
<Button href="/" class="">Go Home</Button>
|
||||
</div>
|
||||
</AuthUI>
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { ROUTE } from '$lib/utils/constants/routes';
|
||||
import { AuthUI } from '$lib/features/ui';
|
||||
import EmailSentIcon from '$lib/components/Icons/EmailSentIcon.svelte';
|
||||
import { forgotApi } from '$lib/features/auth/api/forgot.svelte';
|
||||
import type { TForgotPasswordForm } from '$lib/features/auth/utils/types';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Card from '@cio/ui/base/card';
|
||||
|
||||
let fields: TForgotPasswordForm = $state({ email: '' });
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Forgot Password - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<AuthUI handleSubmit={() => forgotApi.submit(fields)} showOnlyContent={true} showLogo={!forgotApi.success}>
|
||||
{#if forgotApi.success}
|
||||
<div class="ui:flex ui:flex-col ui:items-center ui:justify-center ui:gap-4">
|
||||
<EmailSentIcon />
|
||||
<Card.Title class="ui:text-xl">Email Sent!</Card.Title>
|
||||
<p class="ui:text-center ui:text-sm ui:text-muted-foreground">
|
||||
We have sent a confirmation email to <span class="ui:text-primary">{fields.email}</span>. Kindly check your
|
||||
inbox to reset password or spam to reset your password.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="ui:mt-6 ui:flex ui:w-full ui:items-center ui:justify-end">
|
||||
<Button type="button" class="ui:w-full" onclick={() => goto(ROUTE.LOGIN)}>Okay</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="ui:flex ui:flex-col ui:gap-6">
|
||||
<div>
|
||||
<Card.Title class="ui:text-xl">Forgot Password</Card.Title>
|
||||
<Card.Description class="ui:mt-2">
|
||||
We will send you a reset link to your email
|
||||
</Card.Description>
|
||||
</div>
|
||||
<Field.Field>
|
||||
<Field.Label for="email">Your email</Field.Label>
|
||||
<Field.Content>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
bind:value={fields.email}
|
||||
placeholder="you@domain.com"
|
||||
disabled={forgotApi.isLoading}
|
||||
autofocus
|
||||
aria-invalid={forgotApi.errors.email ? 'true' : undefined}
|
||||
/>
|
||||
{#if forgotApi.errors.email}
|
||||
<Field.Error>{forgotApi.errors.email}</Field.Error>
|
||||
{/if}
|
||||
</Field.Content>
|
||||
</Field.Field>
|
||||
|
||||
<div class="ui:flex ui:w-full ui:flex-col ui:gap-2">
|
||||
<Button type="submit" disabled={forgotApi.isLoading} loading={forgotApi.isLoading} class="ui:w-full">
|
||||
Reset Password
|
||||
</Button>
|
||||
<Button type="button" variant="ghost" class="ui:w-full" onclick={() => goto(ROUTE.LOGIN)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</AuthUI>
|
||||
+54
-47
@@ -1,18 +1,17 @@
|
||||
<script lang="ts">
|
||||
import AuthUI from '$lib/components/AuthUI/index.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { AuthUI } from '$lib/features/ui';
|
||||
import { LOGIN_FIELDS } from '$lib/utils/constants/authentication';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { authValidation } from '$lib/utils/functions/validator';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { authClient } from '$lib/utils/services/auth/client';
|
||||
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import { Password } from '@cio/ui/custom/password';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
|
||||
let formRef: HTMLFormElement | undefined = $state();
|
||||
let supabase = getSupabase();
|
||||
let fields = $state(Object.assign({}, LOGIN_FIELDS));
|
||||
let submitError: string | undefined = $state();
|
||||
let loading = $state(false);
|
||||
@@ -34,7 +33,6 @@
|
||||
{
|
||||
email: fields.email,
|
||||
password: fields.password
|
||||
// callbackURL: '/' // the root page should shows a spinner until the account is loaded and redirect happens in +layout.svelte (accountManager.init())
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
@@ -67,46 +65,55 @@
|
||||
<title>Welcome back to {$currentOrg.name || 'ClassroomIO'}</title>
|
||||
</svelte:head>
|
||||
|
||||
<AuthUI {supabase} isLogin={true} {handleSubmit} isLoading={loading} bind:formRef>
|
||||
<div class="mt-4 w-full">
|
||||
<p class="mb-6 text-lg font-semibold dark:text-white">{$t('login.welcome')}</p>
|
||||
<TextField
|
||||
label={$t('login.email')}
|
||||
bind:value={fields.email}
|
||||
type="email"
|
||||
autoFocus={true}
|
||||
placeholder="you@domain.com"
|
||||
className="mb-6"
|
||||
inputClassName="w-full"
|
||||
isDisabled={loading}
|
||||
errorMessage={$t(errors.email)}
|
||||
/>
|
||||
<TextField
|
||||
label={$t('login.password')}
|
||||
bind:value={fields.password}
|
||||
type="password"
|
||||
placeholder="************"
|
||||
className="mb-6"
|
||||
inputClassName="w-full"
|
||||
isDisabled={loading}
|
||||
errorMessage={$t(errors.password)}
|
||||
/>
|
||||
{#if submitError}
|
||||
<p class="text-sm text-red-500">{submitError}</p>
|
||||
{/if}
|
||||
<div class="w-full text-right">
|
||||
<a class="text-md text-primary-700" href="/forgot"> {$t('login.forgot')} </a>
|
||||
</div>
|
||||
</div>
|
||||
<AuthUI isLogin={true} {handleSubmit} isLoading={loading}>
|
||||
<div class="ui:flex ui:flex-col ui:gap-6">
|
||||
<Field.Field>
|
||||
<Field.Label for="email">{$t('login.email')}</Field.Label>
|
||||
<Field.Content>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
bind:value={fields.email}
|
||||
placeholder="you@domain.com"
|
||||
disabled={loading}
|
||||
autofocus
|
||||
aria-invalid={errors.email ? 'true' : undefined}
|
||||
autocomplete="username"
|
||||
/>
|
||||
{#if errors.email}
|
||||
<Field.Error>{$t(errors.email)}</Field.Error>
|
||||
{/if}
|
||||
</Field.Content>
|
||||
</Field.Field>
|
||||
|
||||
<div class="my-4 flex w-full items-center justify-end">
|
||||
<!-- <a href="/login" class="text-primary-700 text-sm">Create an account</a> -->
|
||||
<PrimaryButton
|
||||
label={$t('login.login')}
|
||||
type="submit"
|
||||
className="sm:w-full w-full"
|
||||
isDisabled={loading}
|
||||
isLoading={loading}
|
||||
/>
|
||||
<Field.Field>
|
||||
<div class="ui:flex ui:items-center ui:justify-between">
|
||||
<Field.Label for="password">{$t('login.password')}</Field.Label>
|
||||
<a class="ui:text-sm ui:text-primary ui:hover:underline" href="/forgot">
|
||||
{$t('login.forgot')}
|
||||
</a>
|
||||
</div>
|
||||
<Field.Content>
|
||||
<Password
|
||||
id="password"
|
||||
bind:value={fields.password}
|
||||
placeholder="************"
|
||||
disabled={loading}
|
||||
aria-invalid={errors.password ? 'true' : undefined}
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
{#if errors.password}
|
||||
<Field.Error>{$t(errors.password)}</Field.Error>
|
||||
{/if}
|
||||
</Field.Content>
|
||||
</Field.Field>
|
||||
|
||||
{#if submitError}
|
||||
<p class="ui:text-sm ui:text-destructive">{submitError}</p>
|
||||
{/if}
|
||||
|
||||
<Button type="submit" disabled={loading} {loading} class="ui:w-full">
|
||||
{$t('login.login')}
|
||||
</Button>
|
||||
</div>
|
||||
</AuthUI>
|
||||
+1
-1
@@ -7,6 +7,6 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="app-background flex min-h-screen w-full items-center justify-center">
|
||||
<div class="flex min-h-screen w-full items-center justify-center">
|
||||
<div>Logging you out...</div>
|
||||
</div>
|
||||
+41
-56
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import * as Select from '@cio/ui/base/select';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import { DomainInput } from '@cio/ui/custom/domain-input';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import UserProfileIcon from '$lib/components/Icons/UserProfileIcon.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { onboardingApi } from '$lib/features/onboarding/api/onboarding.svelte';
|
||||
import { generateSitename } from '$lib/utils/functions/org';
|
||||
@@ -72,50 +73,42 @@
|
||||
<div class="form-container w-full overflow-y-auto px-2">
|
||||
{#if onboardingApi.step === ONBOARDING_STEPS.ORG_SETUP}
|
||||
<!-- Name/Organization Question -->
|
||||
<div id="role-question" class="mb-6 flex flex-col items-start">
|
||||
<Field.Group class="mb-6">
|
||||
<!-- Full name -->
|
||||
<TextField
|
||||
label={$t('onboarding.fullname')}
|
||||
bind:value={fields.fullname}
|
||||
name="fullname"
|
||||
type="text"
|
||||
placeholder="e.g Joke Silva"
|
||||
className="mb-5 w-full"
|
||||
labelClassName="text-lg font-normal"
|
||||
errorMessage={onboardingApi.errors.fullname}
|
||||
isRequired
|
||||
/>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('onboarding.fullname')}</Field.Label>
|
||||
<Input bind:value={fields.fullname} name="fullname" type="text" placeholder="e.g Joke Silva" />
|
||||
{#if onboardingApi.errors.fullname}
|
||||
<Field.Error>{onboardingApi.errors.fullname}</Field.Error>
|
||||
{/if}
|
||||
</Field.Field>
|
||||
|
||||
<!-- Org name -->
|
||||
<TextField
|
||||
label={$t('onboarding.name')}
|
||||
bind:value={fields.orgName}
|
||||
name="orgname"
|
||||
type="text"
|
||||
placeholder="e.g Education For All"
|
||||
className="mb-5 w-full"
|
||||
labelClassName="text-lg font-normal"
|
||||
errorMessage={onboardingApi.errors.orgName}
|
||||
isRequired
|
||||
/>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('onboarding.name')}</Field.Label>
|
||||
<Input bind:value={fields.orgName} name="orgname" type="text" placeholder="e.g My School Name" />
|
||||
{#if onboardingApi.errors.orgName}
|
||||
<Field.Error>{onboardingApi.errors.orgName}</Field.Error>
|
||||
{/if}
|
||||
</Field.Field>
|
||||
|
||||
<!-- Org Site Name -->
|
||||
<TextField
|
||||
label={$t('onboarding.organisation_sitename')}
|
||||
helperMessage={`https://${fields.siteName || ''}.classroomio.com`}
|
||||
bind:value={fields.siteName}
|
||||
name="sitename"
|
||||
type="text"
|
||||
placeholder="e.g edforall"
|
||||
className="mb-5 w-full"
|
||||
labelClassName="text-lg font-normal"
|
||||
errorMessage={onboardingApi.errors.siteName}
|
||||
onChange={() => {
|
||||
isSiteNameTouched = true;
|
||||
}}
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('onboarding.organisation_sitename')}</Field.Label>
|
||||
<DomainInput
|
||||
bind:value={fields.siteName}
|
||||
placeholder="myschool"
|
||||
prefix="https://"
|
||||
suffix=".classroomio.com"
|
||||
oninput={() => {
|
||||
isSiteNameTouched = true;
|
||||
}}
|
||||
/>
|
||||
{#if onboardingApi.errors.siteName}
|
||||
<Field.Error>{onboardingApi.errors.siteName}</Field.Error>
|
||||
{/if}
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
{:else}
|
||||
<!-- Goal/Source Question -->
|
||||
<div id="goal-question" class="mb-6 flex flex-col items-center">
|
||||
@@ -189,21 +182,13 @@
|
||||
|
||||
<div class="flex">
|
||||
{#if onboardingApi.step > ONBOARDING_STEPS.ORG_SETUP}
|
||||
<PrimaryButton
|
||||
label={$t('onboarding.back')}
|
||||
variant={VARIANTS.NONE}
|
||||
className="py-3 px-6 mr-2 text-primary-700"
|
||||
onClick={() => (onboardingApi.step = ONBOARDING_STEPS.ORG_SETUP)}
|
||||
/>
|
||||
<Button variant="ghost" onclick={() => (onboardingApi.step = ONBOARDING_STEPS.ORG_SETUP)}>
|
||||
{$t('onboarding.back')}
|
||||
</Button>
|
||||
{/if}
|
||||
<PrimaryButton
|
||||
label={$t('onboarding.continue')}
|
||||
variant={VARIANTS.CONTAINED}
|
||||
type="button"
|
||||
className="px-5 py-3"
|
||||
isLoading={onboardingApi.isLoading}
|
||||
onClick={() => onboardingApi.submit(fields)}
|
||||
/>
|
||||
<Button loading={onboardingApi.isLoading} onclick={() => onboardingApi.submit(fields)}>
|
||||
{$t('onboarding.continue')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,110 @@
|
||||
<script lang="ts">
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { getConfirmPasswordError } from '$lib/utils/functions/validator';
|
||||
import { AuthUI } from '$lib/features/ui';
|
||||
import { resetApi } from '$lib/features/auth/api/reset.svelte';
|
||||
import type { TResetPasswordForm } from '$lib/features/auth/utils/types';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
import { Password } from '@cio/ui/custom/password';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Card from '@cio/ui/base/card';
|
||||
|
||||
let fields: TResetPasswordForm = $state({
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
token: ''
|
||||
});
|
||||
|
||||
const isSubmitDisabled = $derived!!(
|
||||
fields.password && fields.confirmPassword && fields.password !== fields.confirmPassword
|
||||
);
|
||||
const token = $derived(new URLSearchParams(page.url.search).get('token'));
|
||||
|
||||
onMount(() => {
|
||||
if (token) return;
|
||||
|
||||
snackbar.error('Invalid Token');
|
||||
|
||||
setTimeout(() => {
|
||||
goto(resolve('/login', {}));
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
function setConfirmPasswordError(fields: TResetPasswordForm) {
|
||||
untrack(() => {
|
||||
const errors = { ...resetApi.errors };
|
||||
errors.confirmPassword = getConfirmPasswordError(fields) ?? '';
|
||||
resetApi.setError(errors);
|
||||
});
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
setConfirmPasswordError(fields);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Reset Password - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<AuthUI
|
||||
isLogin={false}
|
||||
handleSubmit={() => resetApi.submit({ ...fields, token: token ?? '' })}
|
||||
showOnlyContent={true}
|
||||
isLoading={resetApi.isLoading}
|
||||
showLogo={true}
|
||||
>
|
||||
<div class="ui:flex ui:flex-col ui:gap-6">
|
||||
<div>
|
||||
<Card.Title class="ui:text-xl">New Password</Card.Title>
|
||||
<Card.Description class="ui:mt-2">Enter your new password details</Card.Description>
|
||||
</div>
|
||||
<Field.Field>
|
||||
<Field.Label for="password">Your Password</Field.Label>
|
||||
<Field.Content>
|
||||
<Password
|
||||
id="password"
|
||||
bind:value={fields.password}
|
||||
placeholder="************"
|
||||
disabled={resetApi.isLoading}
|
||||
aria-invalid={resetApi.errors.password ? 'true' : undefined}
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
{#if resetApi.errors.password}
|
||||
<Field.Error>{resetApi.errors.password}</Field.Error>
|
||||
{/if}
|
||||
<Field.Description>Password must be more than 8 characters</Field.Description>
|
||||
</Field.Content>
|
||||
</Field.Field>
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label for="confirmPassword">Confirm Password</Field.Label>
|
||||
<Field.Content>
|
||||
<Password
|
||||
id="confirmPassword"
|
||||
bind:value={fields.confirmPassword}
|
||||
placeholder="************"
|
||||
disabled={resetApi.isLoading}
|
||||
aria-invalid={resetApi.errors.confirmPassword ? 'true' : undefined}
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
{#if resetApi.errors.confirmPassword}
|
||||
<Field.Error>{resetApi.errors.confirmPassword}</Field.Error>
|
||||
{/if}
|
||||
</Field.Content>
|
||||
</Field.Field>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitDisabled || resetApi.isLoading}
|
||||
loading={resetApi.isLoading}
|
||||
class="ui:w-full"
|
||||
>
|
||||
Reset Password
|
||||
</Button>
|
||||
</div>
|
||||
</AuthUI>
|
||||
+67
-67
@@ -1,18 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import AuthUI from '$lib/components/AuthUI/index.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { AuthUI } from '$lib/features/ui';
|
||||
import SenjaEmbed from '$lib/components/Senja/Embed.svelte';
|
||||
import { SIGNUP_FIELDS } from '$lib/utils/constants/authentication';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { authValidation, getConfirmPasswordError, getDisableSubmit } from '$lib/utils/functions/validator';
|
||||
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { authClient } from '$lib/utils/services/auth/client';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import { Password } from '@cio/ui/custom/password';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
|
||||
let supabase = getSupabase();
|
||||
let fields = $state(Object.assign({}, SIGNUP_FIELDS));
|
||||
let loading = $state(false);
|
||||
let errors: {
|
||||
@@ -21,7 +21,6 @@
|
||||
confirmPassword?: string;
|
||||
} = $state({});
|
||||
let submitError: string = $state('');
|
||||
let formRef: HTMLFormElement | undefined = $state();
|
||||
|
||||
const disableSubmit = $derived(getDisableSubmit(fields));
|
||||
|
||||
@@ -90,67 +89,68 @@
|
||||
|
||||
<SenjaEmbed id="aa054658-1e15-4d00-8920-91f424326c4e" />
|
||||
|
||||
<AuthUI {supabase} isLogin={false} {handleSubmit} isLoading={loading} bind:formRef>
|
||||
<div class="mt-4 w-full">
|
||||
<p class="mb-6 text-lg font-semibold dark:text-white">Create a free account</p>
|
||||
<!-- <TextField
|
||||
label="Full Name"
|
||||
bind:value={fields.name}
|
||||
type="text"
|
||||
autoFocus={true}
|
||||
placeholder="e.g Joke Silva"
|
||||
className="mb-6"
|
||||
inputClassName="w-full"
|
||||
isDisabled={loading}
|
||||
errorMessage={errors.name}
|
||||
isRequired
|
||||
/> -->
|
||||
<TextField
|
||||
label={$t('login.fields.email')}
|
||||
bind:value={fields.email}
|
||||
type="email"
|
||||
placeholder="you@domain.com"
|
||||
className="mb-6"
|
||||
inputClassName="w-full"
|
||||
isDisabled={loading}
|
||||
errorMessage={$t(errors.email ?? '')}
|
||||
isRequired
|
||||
/>
|
||||
<TextField
|
||||
label={$t('login.fields.password')}
|
||||
bind:value={fields.password}
|
||||
type="password"
|
||||
placeholder="************"
|
||||
className="mb-6"
|
||||
inputClassName="w-full"
|
||||
isDisabled={loading}
|
||||
errorMessage={$t(errors.password ?? '')}
|
||||
helperMessage={$t('login.fields.password_helper_message')}
|
||||
isRequired
|
||||
/>
|
||||
<TextField
|
||||
label={$t('login.fields.confirm_password')}
|
||||
bind:value={fields.confirmPassword}
|
||||
type="password"
|
||||
placeholder="************"
|
||||
className="mb-6"
|
||||
inputClassName="w-full"
|
||||
isDisabled={loading}
|
||||
errorMessage={errors.confirmPassword}
|
||||
isRequired
|
||||
/>
|
||||
{#if submitError}
|
||||
<p class="text-sm text-red-500">{submitError}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<AuthUI isLogin={false} {handleSubmit} isLoading={loading}>
|
||||
<div class="ui:flex ui:flex-col ui:gap-6">
|
||||
<Field.Field>
|
||||
<Field.Label for="email">{$t('login.fields.email')}</Field.Label>
|
||||
<Field.Content>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
bind:value={fields.email}
|
||||
placeholder="you@domain.com"
|
||||
disabled={loading}
|
||||
autofocus
|
||||
aria-invalid={errors.email ? 'true' : undefined}
|
||||
autocomplete="email"
|
||||
/>
|
||||
{#if errors.email}
|
||||
<Field.Error>{$t(errors.email)}</Field.Error>
|
||||
{/if}
|
||||
</Field.Content>
|
||||
</Field.Field>
|
||||
|
||||
<div class="my-4 flex w-full items-center justify-end">
|
||||
<PrimaryButton
|
||||
label={$t('login.create_account')}
|
||||
type="submit"
|
||||
className="sm:w-full w-full"
|
||||
isDisabled={disableSubmit || loading}
|
||||
isLoading={loading}
|
||||
/>
|
||||
<Field.Field>
|
||||
<Field.Label for="password">{$t('login.fields.password')}</Field.Label>
|
||||
<Field.Content>
|
||||
<Password
|
||||
id="password"
|
||||
bind:value={fields.password}
|
||||
placeholder="************"
|
||||
disabled={loading}
|
||||
aria-invalid={errors.password ? 'true' : undefined}
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
{#if errors.password}
|
||||
<Field.Error>{$t(errors.password)}</Field.Error>
|
||||
{/if}
|
||||
<Field.Description>{$t('login.fields.password_helper_message')}</Field.Description>
|
||||
</Field.Content>
|
||||
</Field.Field>
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label for="confirmPassword">{$t('login.fields.confirm_password')}</Field.Label>
|
||||
<Field.Content>
|
||||
<Password
|
||||
id="confirmPassword"
|
||||
bind:value={fields.confirmPassword}
|
||||
placeholder="************"
|
||||
disabled={loading}
|
||||
aria-invalid={errors.confirmPassword ? 'true' : undefined}
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
{#if errors.confirmPassword}
|
||||
<Field.Error>{errors.confirmPassword}</Field.Error>
|
||||
{/if}
|
||||
</Field.Content>
|
||||
</Field.Field>
|
||||
|
||||
{#if submitError}
|
||||
<p class="ui:text-sm ui:text-destructive">{submitError}</p>
|
||||
{/if}
|
||||
|
||||
<Button type="submit" disabled={disableSubmit || loading} {loading} class="ui:w-full">
|
||||
{$t('login.create_account')}
|
||||
</Button>
|
||||
</div>
|
||||
</AuthUI>
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getSupabase, supabase } from '$lib/utils/functions/supabase';
|
||||
|
||||
import type { CurrentOrg } from '$lib/utils/types/org';
|
||||
import type { AccountOrg } from '$lib/features/app/types';
|
||||
import type { MetaTagsProps } from 'svelte-meta-tags';
|
||||
import { PUBLIC_IS_SELFHOSTED } from '$env/static/public';
|
||||
import { dev } from '$app/environment';
|
||||
@@ -20,7 +20,7 @@ interface LoadOutput {
|
||||
orgSiteName: string;
|
||||
isOrgSite: boolean;
|
||||
skipAuth: boolean;
|
||||
org: CurrentOrg | null;
|
||||
org: AccountOrg | null;
|
||||
baseMetaTags: MetaTagsProps;
|
||||
serverLang: string;
|
||||
locals: App.Locals;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import PlayQuiz from '$lib/components/Org/Quiz/Play/index.svelte';
|
||||
import { PageRestricted } from '$lib/components/Page';
|
||||
import Snackbar from '$lib/components/Snackbar/index.svelte';
|
||||
import UpgradeModal from '$lib/components/Upgrade/Modal.svelte';
|
||||
import { UpgradeModal } from '$lib/features/ui';
|
||||
import { user } from '$lib/utils/store/user';
|
||||
import { setupAnalytics } from '$lib/utils/functions/appSetup';
|
||||
import { setTheme } from '$lib/utils/functions/theme';
|
||||
@@ -101,11 +101,11 @@
|
||||
|
||||
<Snackbar />
|
||||
|
||||
{#if data.org?.is_restricted || $currentOrg.isRestricted}
|
||||
{#if data.org?.isRestricted || $currentOrg.isRestricted}
|
||||
<PageRestricted />
|
||||
{:else if data.skipAuth}
|
||||
<PlayQuiz />
|
||||
{:else if data.isOrgSite && !path}
|
||||
{:else if data.isOrgSite && data.org && !path}
|
||||
<OrgLandingPage orgSiteName={data.orgSiteName} org={data.org} />
|
||||
{:else}
|
||||
<PageLoadProgress zIndex={10000} />
|
||||
@@ -117,16 +117,6 @@
|
||||
:global(:root) {
|
||||
--main-primary-color: rgba(29, 78, 216, 1);
|
||||
--border-color: #eaecef;
|
||||
--app-background-color: #fafbfc;
|
||||
--app-background: radial-gradient(circle at 10% 20%, rgb(239, 246, 249) 0%, rgb(206, 239, 253) 90%);
|
||||
--dark-app-background: radial-gradient(circle at 10% 20%, rgb(0 0 0) 0%, rgb(27 60 74) 90%);
|
||||
}
|
||||
|
||||
:global(.app-background) {
|
||||
background: var(--app-background);
|
||||
}
|
||||
:global(.dark .app-background) {
|
||||
background: var(--dark-app-background);
|
||||
}
|
||||
|
||||
:global(.dark svg.dark) {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script>
|
||||
import { appInitApi } from '$lib/features/app/init.svelte';
|
||||
import { Circle } from 'svelte-loading-spinners';
|
||||
import { Spinner } from '@cio/ui/base/spinner';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>ClassroomIO - Beautiful and Robust Course Platform</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="m-2 flex w-screen flex-col items-center justify-center font-sans sm:m-0 md:min-h-[93vh] dark:bg-black">
|
||||
<div class="m-2 flex h-screen w-screen flex-col items-center justify-center font-sans sm:m-0">
|
||||
{#if appInitApi.loading || appInitApi.data}
|
||||
<Circle size="50" color="#1d4ee2" unit="px" duration="1s" />
|
||||
<Spinner class="size-14! text-blue-700!" />
|
||||
{:else if appInitApi.error}
|
||||
<p class="text-red-500">{appInitApi.error}</p>
|
||||
{/if}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { cancelOrgPlan, createOrgPlan, updateOrgPlan } from '$lib/utils/services/org';
|
||||
|
||||
import { OrgPlanApiServer } from '$lib/features/org/api/org-plan.server';
|
||||
import { PLAN } from '@cio/utils/plans';
|
||||
import type { PolarWebhookPayload } from '$lib/utils/types/polar';
|
||||
import { Webhooks } from '@polar-sh/sveltekit';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { getServerSupabase } from '$lib/utils/functions/supabase.server';
|
||||
|
||||
export const POST = Webhooks({
|
||||
webhookSecret: env.POLAR_WEBHOOK_SECRET!,
|
||||
@@ -12,8 +10,6 @@ export const POST = Webhooks({
|
||||
});
|
||||
|
||||
async function onPayload(payload: PolarWebhookPayload) {
|
||||
const supabase = getServerSupabase();
|
||||
|
||||
const metadata = payload.data.metadata;
|
||||
const orgId = metadata.orgId;
|
||||
const triggeredBy = metadata.triggeredBy;
|
||||
@@ -39,45 +35,45 @@ async function onPayload(payload: PolarWebhookPayload) {
|
||||
break;
|
||||
case 'subscription.created':
|
||||
if (isSubscriptionActive) {
|
||||
const { data, error } = await createOrgPlan({
|
||||
supabase,
|
||||
subscriptionId,
|
||||
try {
|
||||
const planData = {
|
||||
orgId,
|
||||
triggeredBy: parseInt(triggeredBy),
|
||||
planName: PLAN.EARLY_ADOPTER,
|
||||
data: payload.data
|
||||
});
|
||||
planName: PLAN.EARLY_ADOPTER as 'EARLY_ADOPTER' | 'ENTERPRISE' | 'BASIC',
|
||||
subscriptionId,
|
||||
payload: payload.data as unknown as Record<string, unknown>
|
||||
};
|
||||
|
||||
if (error) {
|
||||
const result = await OrgPlanApiServer.createOrgPlan(planData);
|
||||
console.log('Subscription created', result);
|
||||
} catch (error) {
|
||||
console.error('Error creating org plan', error);
|
||||
}
|
||||
console.log('Subscription created', data);
|
||||
}
|
||||
break;
|
||||
case 'subscription.updated':
|
||||
if (!isSubscriptionActive) {
|
||||
// Handle Cancel Subscription
|
||||
const { data, error } = await cancelOrgPlan({
|
||||
try {
|
||||
const result = await OrgPlanApiServer.cancelOrgPlan({
|
||||
subscriptionId,
|
||||
data: payload.data
|
||||
payload: payload.data as unknown as Record<string, unknown>
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.log('Subscription canceled', result);
|
||||
} catch (error) {
|
||||
console.error('Error canceling org plan', error);
|
||||
}
|
||||
console.log('Subscription canceled', data);
|
||||
} else {
|
||||
// Handle Update Subscription
|
||||
const { data, error } = await updateOrgPlan({
|
||||
supabase,
|
||||
try {
|
||||
const result = await OrgPlanApiServer.updateOrgPlan({
|
||||
subscriptionId,
|
||||
data: payload.data
|
||||
payload: payload.data as unknown as Record<string, unknown>
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.log('Subscription updated', result);
|
||||
} catch (error) {
|
||||
console.error('Error updating org plan', error);
|
||||
}
|
||||
console.log('Subscription updated', data);
|
||||
}
|
||||
break;
|
||||
case 'subscription.active':
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import { CourseContainer } from '$lib/components/CourseContainer';
|
||||
import { PageBody, PageNav } from '$lib/components/Page';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import { RoleBasedSecurity } from '$lib/features/ui';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
@@ -27,9 +27,9 @@
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import type { Feed } from '$lib/utils/types/feed';
|
||||
import type { CurrentOrg } from '$lib/utils/types/org';
|
||||
import PinIcon from '@lucide/svelte/icons/pin';
|
||||
import { onMount } from 'svelte';
|
||||
import type { AccountOrg } from '$lib/features/app/types';
|
||||
|
||||
let { data = $bindable() } = $props();
|
||||
|
||||
@@ -214,7 +214,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
function getPageRoles(org: CurrentOrg) {
|
||||
function getPageRoles(org: AccountOrg) {
|
||||
const roles: number[] = [1, 2];
|
||||
|
||||
if (org.customization.course.newsfeed) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import IssueCertificateModal from '$lib/components/Course/components/Ceritficate/IssueCertificateModal.svelte';
|
||||
import { CourseContainer } from '$lib/components/CourseContainer';
|
||||
import { PageBody, PageNav } from '$lib/components/Page';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import { RoleBasedSecurity } from '$lib/features/ui';
|
||||
// import DeleteConfirmation from '$lib/components/Course/components/People/DeleteConfirmation.svelte';
|
||||
import Design from '$lib/components/Course/components/Ceritficate/Design.svelte';
|
||||
// import Reports from '$lib/components/Course/components/Ceritficate/Reports.svelte';
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import { PageBody, PageNav } from '$lib/components/Page';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import { RoleBasedSecurity } from '$lib/features/ui';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import type { Lesson } from '$lib/utils/types';
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import { PageBody, PageNav } from '$lib/components/Page';
|
||||
import { CourseContainer } from '$lib/components/CourseContainer';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import { RoleBasedSecurity } from '$lib/features/ui';
|
||||
import Exercises from '$lib/components/Course/components/Lesson/Exercises/index.svelte';
|
||||
import Materials from '$lib/components/Course/components/Lesson/Materials/index.svelte';
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import type { GroupPerson } from '$lib/utils/types';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { fetchMarks } from '$lib/utils/services/marks';
|
||||
import type { CurrentOrg } from '$lib/utils/types/org';
|
||||
import type { AccountOrg } from '$lib/features/app/types';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { getLectureNo } from '$lib/components/Course/function.js';
|
||||
import { fetchExercisesByMarks } from '$lib/utils/services/courses';
|
||||
@@ -22,7 +22,7 @@
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { PageBody, PageNav } from '$lib/components/Page';
|
||||
import { CourseContainer } from '$lib/components/CourseContainer';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import { RoleBasedSecurity } from '$lib/features/ui';
|
||||
import { lessons } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
|
||||
let { data } = $props();
|
||||
@@ -90,10 +90,10 @@
|
||||
}
|
||||
// TODO WRITE A REDIRECT FUNCTION FOR THIS THIS PAGE
|
||||
|
||||
function getPageRoles(org: CurrentOrg) {
|
||||
function getPageRoles(org: AccountOrg) {
|
||||
const roles = [1, 2];
|
||||
|
||||
if (org.customization.course.grading) {
|
||||
if (org.customization?.course?.grading) {
|
||||
roles.push(3);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import { PageBody, PageNav } from '$lib/components/Page';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import { RoleBasedSecurity } from '$lib/features/ui';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
import { CourseContainer } from '$lib/components/CourseContainer';
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import { ComingSoon } from '$lib/features/ui';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import { RoleBasedSecurity } from '$lib/features/ui';
|
||||
import InvitationModal from '$lib/components/Course/components/People/InvitationModal.svelte';
|
||||
import DeleteConfirmation from '$lib/components/Course/components/People/DeleteConfirmation.svelte';
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
import Chip from '$lib/components/Chip/index.svelte';
|
||||
import { PageBody, PageNav } from '$lib/components/Page';
|
||||
import { CourseContainer } from '$lib/components/CourseContainer';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import { RoleBasedSecurity } from '$lib/features/ui';
|
||||
import MarkExerciseModal from '$lib/components/Course/components/Lesson/Exercise/MarkExerciseModal.svelte';
|
||||
|
||||
let { data = $bindable() } = $props();
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { ROUTE } from '$lib/utils/constants/routes';
|
||||
import AuthUI from '$lib/components/AuthUI/index.svelte';
|
||||
import EmailSentIcon from '$lib/components/Icons/EmailSentIcon.svelte';
|
||||
import { forgotApi } from '$lib/features/auth/api/forgot.svelte';
|
||||
import type { TForgotPasswordForm } from '$lib/features/auth/utils/types';
|
||||
|
||||
let fields: TForgotPasswordForm = $state({ email: '' });
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Forgot Password - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<AuthUI handleSubmit={() => forgotApi.submit(fields)} showOnlyContent={true} showLogo={!forgotApi.success}>
|
||||
{#if forgotApi.success}
|
||||
<div class="mt-4 flex w-full flex-col items-center justify-center">
|
||||
<EmailSentIcon />
|
||||
<h3 class="my-3 text-xl font-semibold dark:text-white">Email Sent!</h3>
|
||||
<p class="text-md mb-6 text-center dark:text-white">
|
||||
We have sent a confirmation email to <span class="text-primary-700">{fields.email}</span>. Kindly check your
|
||||
inbox to reset password or spam to reset your password.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="my-4 flex w-full flex-col items-center justify-end">
|
||||
<PrimaryButton label="Okay" type="button" className="sm:w-full w-full mb-4" onClick={() => goto(ROUTE.LOGIN)} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-4 w-full">
|
||||
<h3 class="my-3 text-xl font-semibold dark:text-white">Forgot Password</h3>
|
||||
<p class="mb-6 text-sm dark:text-white">We will send you a reset link to your email</p>
|
||||
<TextField
|
||||
label="Your email"
|
||||
bind:value={fields.email}
|
||||
type="email"
|
||||
autoFocus={true}
|
||||
placeholder="you@domain.com"
|
||||
className="mb-6"
|
||||
inputClassName="w-full"
|
||||
isDisabled={forgotApi.isLoading}
|
||||
errorMessage={forgotApi.errors.email}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="my-4 flex w-full flex-col items-center justify-end">
|
||||
<PrimaryButton
|
||||
label="Reset Password"
|
||||
type="submit"
|
||||
className="sm:w-full w-full mb-4"
|
||||
isLoading={forgotApi.isLoading}
|
||||
/>
|
||||
<PrimaryButton
|
||||
label="Cancel"
|
||||
type="button"
|
||||
className="sm:w-full w-full text-primary-700"
|
||||
variant={VARIANTS.NONE}
|
||||
onClick={() => goto(ROUTE.LOGIN)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</AuthUI>
|
||||
@@ -1,10 +1,5 @@
|
||||
import { OrgApiServer } from '$lib/features/org/api/org.server';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { getSupabase, supabase } from '$lib/utils/functions/supabase';
|
||||
import { getCurrentOrg } from '$lib/utils/services/org';
|
||||
|
||||
if (!supabase) {
|
||||
getSupabase();
|
||||
}
|
||||
|
||||
export const load = async ({ params = { hash: '' } }) => {
|
||||
try {
|
||||
@@ -16,7 +11,8 @@ export const load = async ({ params = { hash: '' } }) => {
|
||||
if (!id || !name || !description || !orgSiteName) {
|
||||
throw 'Validation failed';
|
||||
}
|
||||
const currentOrg = await getCurrentOrg(orgSiteName, true);
|
||||
|
||||
const currentOrg = await OrgApiServer.getOrgBySiteName(orgSiteName);
|
||||
|
||||
return {
|
||||
id,
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
import { onMount } from 'svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import AuthUI from '$lib/components/AuthUI/index.svelte';
|
||||
import { AuthUI } from '$lib/features/ui';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { setTheme } from '$lib/utils/functions/theme';
|
||||
import { addGroupMember } from '$lib/utils/services/courses';
|
||||
import type { CurrentOrg } from '$lib/utils/types/org.js';
|
||||
import { ROLE } from '@cio/utils/constants';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { triggerSendEmail, NOTIFICATION_NAME } from '$lib/utils/services/notification/notification';
|
||||
@@ -21,7 +20,6 @@
|
||||
let loading = $state(false);
|
||||
|
||||
let disableSubmit = false;
|
||||
let formRef: HTMLFormElement | undefined = $state();
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
@@ -104,11 +102,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
function setCurOrg(cOrg: CurrentOrg) {
|
||||
if (!cOrg) return;
|
||||
currentOrg.set(cOrg);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// check if user has session, if not redirect to sign up with redirect back to this page
|
||||
const {
|
||||
@@ -118,8 +111,11 @@
|
||||
return goto(`/login?redirect=${page.url?.pathname || ''}`);
|
||||
}
|
||||
|
||||
if (!data.currentOrg) return;
|
||||
|
||||
setTheme(data.currentOrg?.theme || '');
|
||||
setCurOrg(data.currentOrg as CurrentOrg);
|
||||
|
||||
currentOrg.set(data.currentOrg);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -127,15 +123,7 @@
|
||||
<title>Join {data.name} on ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<AuthUI
|
||||
{supabase}
|
||||
isLogin={false}
|
||||
{handleSubmit}
|
||||
isLoading={loading || !$profile.id}
|
||||
showOnlyContent={true}
|
||||
showLogo={true}
|
||||
bind:formRef
|
||||
>
|
||||
<AuthUI isLogin={false} {handleSubmit} isLoading={loading || !$profile.id} showOnlyContent={true} showLogo={true}>
|
||||
<div class="mt-0 w-full">
|
||||
<h3 class="mb-4 mt-0 text-center text-lg font-medium dark:text-white">{data.name}</h3>
|
||||
<p class="text-center text-sm font-light dark:text-white">{data.description}</p>
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { getCurrentOrg } from '$lib/utils/services/org';
|
||||
import { getSupabase, supabase } from '$lib/utils/functions/supabase';
|
||||
import { OrgApiServer } from '$lib/features/org/api/org.server';
|
||||
import { getProfile } from '$lib/utils/functions/user';
|
||||
|
||||
if (!supabase) {
|
||||
getSupabase();
|
||||
}
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
// we need to know if the email exists or not.
|
||||
// with this we can only ask the user to accept
|
||||
@@ -16,7 +11,7 @@ export const load = async ({ params = { hash: '' } }) => {
|
||||
|
||||
const { orgId, email, orgSiteName } = JSON.parse(hashData);
|
||||
|
||||
const currentOrg = await getCurrentOrg(orgSiteName, true);
|
||||
const currentOrg = await OrgApiServer.getOrgBySiteName(orgSiteName);
|
||||
|
||||
const profile = await getProfile(email);
|
||||
console.log('profile data', profile);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import AuthUI from '$lib/components/AuthUI/index.svelte';
|
||||
import { AuthUI } from '$lib/features/ui';
|
||||
import type { Profile } from '$lib/components/Course/components/People/types';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
@@ -13,7 +13,6 @@
|
||||
import { profile, user } from '$lib/utils/store/user';
|
||||
import { authValidation, getConfirmPasswordError, getDisableSubmit } from '$lib/utils/functions/validator';
|
||||
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
|
||||
import type { CurrentOrg } from '$lib/utils/types/org';
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { snackbar } from '$lib/components/Snackbar/store.js';
|
||||
|
||||
@@ -32,12 +31,11 @@
|
||||
} = $state({});
|
||||
|
||||
let submitError: string = $state('');
|
||||
let formRef: HTMLFormElement | undefined = $state();
|
||||
|
||||
const confirmPasswordError = $derived(getConfirmPasswordError(fields));
|
||||
const disableSubmit = $derived(getDisableSubmit(fields));
|
||||
|
||||
async function joinOrg(profileId: string, email: string) {
|
||||
async function joinOrg(profileId: string, email: string | null) {
|
||||
if (!profileId || !email || !data.invite.currentOrg?.id) return;
|
||||
|
||||
// Update member response
|
||||
@@ -51,8 +49,6 @@
|
||||
|
||||
console.log('Update member response', updateMemberRes);
|
||||
|
||||
formRef?.reset();
|
||||
|
||||
window.location.href = $currentOrgPath;
|
||||
}
|
||||
|
||||
@@ -146,17 +142,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
function setCurOrg(cOrg: CurrentOrg) {
|
||||
if (!cOrg) return;
|
||||
|
||||
console.log(cOrg);
|
||||
currentOrg.set(cOrg);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (!data.invite.currentOrg) return;
|
||||
|
||||
setTheme(data.invite.currentOrg?.theme || '');
|
||||
|
||||
setCurOrg(data.invite.currentOrg as CurrentOrg);
|
||||
currentOrg.set(data.invite.currentOrg);
|
||||
});
|
||||
|
||||
const isLoading = $derived(loading || $user.fetchingUser);
|
||||
@@ -179,15 +170,7 @@
|
||||
<title>Join ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<AuthUI
|
||||
{supabase}
|
||||
redirectPathname={page.url.pathname}
|
||||
isLogin={false}
|
||||
{handleSubmit}
|
||||
{isLoading}
|
||||
showLogo={true}
|
||||
bind:formRef
|
||||
>
|
||||
<AuthUI redirectPathname={page.url.pathname} isLogin={false} {handleSubmit} {isLoading} showLogo={true}>
|
||||
<div class="mt-4 w-full {shouldLogout ? 'hidden' : ''}">
|
||||
<p class="mb-6 text-lg font-semibold dark:text-white">
|
||||
{#if data.invite.profile}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
|
||||
import { courseMetaDeta, courses } from '$lib/components/Courses/store';
|
||||
import { courseMetaDeta, courses } from '$lib/features/course/utils/store';
|
||||
import Learning from '$lib/components/LMS/components/Learning.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import Community from '$lib/components/Org/Community/index.svelte';
|
||||
import { CommunityPage } from '$lib/features/community/pages';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
@@ -20,6 +20,6 @@
|
||||
<PrimaryButton label={$t('community.ask_button')} onClick={askCommunity} />
|
||||
</div>
|
||||
|
||||
<Community isLMS={true} />
|
||||
<CommunityPage isLMS={true} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { courses } from '$lib/components/Courses/store';
|
||||
import { courses } from '$lib/features/course/utils/store';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
@@ -25,8 +25,8 @@
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import DeleteModal from '$lib/components/Org/Community/DeleteModal.svelte';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
import { CommunityDeleteModal } from '$lib/features/community/components';
|
||||
|
||||
interface Comment {
|
||||
id: string;
|
||||
@@ -203,7 +203,7 @@
|
||||
id: _c.id,
|
||||
authorId: $profile.id || '',
|
||||
name: $profile?.fullname || '',
|
||||
avatar: $profile?.avatar_url || '',
|
||||
avatar: $profile?.avatarUrl || '',
|
||||
votes: 0,
|
||||
comment: _c.body,
|
||||
createdAt: calDateDiff(_c.created_at)
|
||||
@@ -370,7 +370,7 @@
|
||||
<title>{question?.title || 'Question'}</title>
|
||||
</svelte:head>
|
||||
|
||||
<DeleteModal
|
||||
<CommunityDeleteModal
|
||||
bind:open={deleteQuestion.shouldDelete}
|
||||
isDeleting={deleteQuestion.isDeleting}
|
||||
onCancel={() => {
|
||||
@@ -381,15 +381,6 @@
|
||||
isQuestion={true}
|
||||
/>
|
||||
|
||||
<DeleteModal
|
||||
bind:open={deleteComment.shouldDelete}
|
||||
isDeleting={deleteComment.isDeleting}
|
||||
onCancel={() => {
|
||||
deleteComment.shouldDelete = false;
|
||||
deleteComment.commentId = '';
|
||||
}}
|
||||
onDelete={() => handleDelete(false)}
|
||||
/>
|
||||
<section class="mx-auto max-w-3xl md:mx-10 lg:mb-20">
|
||||
{#if !question}
|
||||
<div class="mb-3 px-5 py-10">
|
||||
@@ -479,7 +470,7 @@
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<section class="prose prose-sm p-2 sm:prose">
|
||||
<section class="prose prose-sm sm:prose p-2">
|
||||
{@html question.body}
|
||||
</section>
|
||||
{/if}
|
||||
@@ -522,7 +513,7 @@
|
||||
</IconButton>
|
||||
{/if}
|
||||
</header>
|
||||
<article class="prose prose-sm p-2 sm:prose">
|
||||
<article class="prose prose-sm sm:prose p-2">
|
||||
{@html comment.comment}
|
||||
</article>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { courses } from '$lib/components/Courses/store';
|
||||
import { courses } from '$lib/features/course/utils/store';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { fetchCourses } from '$lib/utils/services/courses';
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import Courses from '$lib/components/Courses/index.svelte';
|
||||
import { courseMetaDeta } from '$lib/components/Courses/store';
|
||||
import { CoursesPage } from '$lib/features/course/pages';
|
||||
import { courseMetaDeta } from '$lib/features/course/utils/store';
|
||||
import { fetchExploreCourses } from '$lib/utils/services/courses';
|
||||
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
@@ -125,7 +125,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<Courses
|
||||
<CoursesPage
|
||||
courses={filteredExploreCourses}
|
||||
emptyTitle={$t('explore.empty_heading')}
|
||||
emptyDescription={$t('explore.empty_description')}
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
import SearchIcon from '@lucide/svelte/icons/search';
|
||||
|
||||
import Tabs from '$lib/components/Tabs/index.svelte';
|
||||
import Courses from '$lib/components/Courses/index.svelte';
|
||||
import { CoursesPage } from '$lib/features/course/pages';
|
||||
import { fetchCourses } from '$lib/utils/services/courses';
|
||||
import TabContent from '$lib/components/TabContent/index.svelte';
|
||||
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { courses, courseMetaDeta, coursesComplete, coursesInProgress } from '$lib/components/Courses/store';
|
||||
import { courses, courseMetaDeta, coursesComplete, coursesInProgress } from '$lib/features/course/utils/store';
|
||||
|
||||
let hasFetched = false;
|
||||
let searchValue = $state('');
|
||||
@@ -73,14 +73,14 @@
|
||||
{#snippet content()}
|
||||
<slot:fragment>
|
||||
<TabContent value={tabs[0].value} index={currentTab}>
|
||||
<Courses
|
||||
<CoursesPage
|
||||
courses={$coursesInProgress}
|
||||
emptyTitle={$t('my_learning.not_in_progress')}
|
||||
emptyDescription={$t('my_learning.any_progress')}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value={tabs[1].value} index={currentTab}>
|
||||
<Courses
|
||||
<CoursesPage
|
||||
courses={$coursesComplete}
|
||||
emptyTitle={$t('my_learning.not_completed')}
|
||||
emptyDescription={$t('my_learning.any_course')}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<Sidebar.Inset>
|
||||
<AppHeader />
|
||||
|
||||
<div class="container mx-auto flex max-w-6xl flex-1 flex-col gap-4 p-4 pt-0">
|
||||
<div class="mx-auto flex w-full max-w-6xl flex-1 flex-col gap-4 overflow-y-auto px-4">
|
||||
{#if data.orgName === '*'}
|
||||
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||
<Skeleton class="aspect-video rounded-xl" />
|
||||
|
||||
@@ -4,14 +4,10 @@
|
||||
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||
import UserIcon from '@lucide/svelte/icons/user';
|
||||
import UsersIcon from '@lucide/svelte/icons/users';
|
||||
import LibraryBigIcon from '@lucide/svelte/icons/library-big';
|
||||
import DollarSignIcon from '@lucide/svelte/icons/dollar-sign';
|
||||
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { getGreeting } from '$lib/utils/functions/date';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
@@ -19,22 +15,19 @@
|
||||
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
|
||||
import type { OrganisationAnalytics } from '$lib/utils/types/analytics';
|
||||
|
||||
import { CreateCourseButton } from '$lib/features/course/components';
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import { ActivityCard } from '$lib/components/Analytics';
|
||||
import Progress from '$lib/components/Progress/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { Grid } from '$lib/components/Org/Settings/Layout';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { WelcomeModal } from '$lib/features/onboarding/components';
|
||||
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
import { Empty } from '@cio/ui/custom/empty';
|
||||
|
||||
let dashAnalytics: OrganisationAnalytics | undefined = $state();
|
||||
let hasFetched = $state(false);
|
||||
|
||||
function createCourse() {
|
||||
goto(`${$currentOrgPath}/courses?create=true`);
|
||||
}
|
||||
|
||||
async function fetchDashAnalytics(orgId: string) {
|
||||
if (!orgId || hasFetched) return;
|
||||
|
||||
@@ -98,153 +91,135 @@
|
||||
|
||||
<WelcomeModal />
|
||||
|
||||
<div class="w-full">
|
||||
<div class="mb-5 flex items-center justify-between">
|
||||
<h1 class="m-0 text-xl font-medium dark:text-white">
|
||||
{$t(getGreeting())}
|
||||
{$profile.fullname}!
|
||||
</h1>
|
||||
<div class="flex items-center">
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={createCourse}
|
||||
isDisabled={!$isOrgAdmin}
|
||||
className="min-h-[36px]"
|
||||
>
|
||||
{#if $isMobile}
|
||||
<PlusIcon size={16} />
|
||||
{:else}
|
||||
{$t('dashboard.create_course')}
|
||||
{/if}
|
||||
</PrimaryButton>
|
||||
<Page.Root class="w-full">
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>
|
||||
{$t(getGreeting())}
|
||||
{$profile.fullname}!
|
||||
</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<CreateCourseButton variant="outline" isResponsive />
|
||||
|
||||
<VisitOrgSiteButton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-10 flex flex-wrap items-start">
|
||||
<Grid class="px-0">
|
||||
<div class="mt-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{#each cards as card}
|
||||
{#if !dashAnalytics}
|
||||
<Skeleton style="width: 100%; min-width: 300px; height: 10rem;" class="rounded-md" />
|
||||
{:else}
|
||||
<ActivityCard activity={card} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 md:p-5 dark:border-neutral-600 dark:text-white">
|
||||
<h3 class="mt-0 text-2xl">
|
||||
{$t('dashboard.top_courses')}
|
||||
</h3>
|
||||
|
||||
<div class="h-full space-y-6">
|
||||
{#if !dashAnalytics}
|
||||
{#each Array(5) as _}
|
||||
<Skeleton class="h-10 w-full rounded-md" />
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<div class="mb-10 flex flex-wrap items-start">
|
||||
<div class="grid w-full grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{#each cards as card}
|
||||
{#if !dashAnalytics}
|
||||
<Skeleton style="width: 100%; height: 10rem;" class="rounded-md" />
|
||||
{:else}
|
||||
<ActivityCard activity={card} />
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
{#each dashAnalytics.topCourses as course}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4/6 space-y-1">
|
||||
<a href={`/courses/${course.id}`}>
|
||||
<p class="line-clamp-2 pb-[0.1rem] text-sm font-medium leading-none">
|
||||
{course.title}
|
||||
</p>
|
||||
</a>
|
||||
<p class="text-sm">
|
||||
{course.enrollments}
|
||||
{$t(course.enrollments === 1 ? 'dashboard.student' : 'dashboard.students')}
|
||||
</p>
|
||||
</div>
|
||||
<div class="ml-auto w-2/6">
|
||||
<Progress value={course.completion} />
|
||||
<div class="text-sm font-medium">
|
||||
{course.completion}%
|
||||
{$t('dashboard.completion')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div
|
||||
class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 md:p-5 dark:border-neutral-600 dark:text-white"
|
||||
>
|
||||
<h3 class="mt-0 text-lg">
|
||||
{$t('dashboard.top_courses')}
|
||||
</h3>
|
||||
|
||||
<div class="h-full space-y-6">
|
||||
{#if !dashAnalytics}
|
||||
{#each Array(5) as _}
|
||||
<Skeleton class="h-10 w-full rounded-md" />
|
||||
{/each}
|
||||
{:else}
|
||||
{#each dashAnalytics.topCourses as course}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4/6 space-y-1">
|
||||
<a href={`/courses/${course.id}`}>
|
||||
<p class="line-clamp-2 pb-[0.1rem] text-sm font-medium leading-none">
|
||||
{course.title}
|
||||
</p>
|
||||
</a>
|
||||
<p class="text-sm">
|
||||
{course.enrollments}
|
||||
{$t(course.enrollments === 1 ? 'dashboard.student' : 'dashboard.students')}
|
||||
</p>
|
||||
</div>
|
||||
<div class="ml-auto w-2/6">
|
||||
<Progress value={course.completion} />
|
||||
<div class="text-sm font-medium">
|
||||
{course.completion}%
|
||||
{$t('dashboard.completion')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col h-full items-center justify-center p-3">
|
||||
<div class="bg-primary-200 w-fit rounded-full p-4 text-black">
|
||||
<LibraryBigIcon size={30} />
|
||||
</div>
|
||||
<div class="my-4 text-center">
|
||||
<p class=" text-xl font-semibold">
|
||||
{$t('dashboard.create_first_course')}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-300">
|
||||
{$t('dashboard.create_first_course_description')}
|
||||
</p>
|
||||
</div>
|
||||
<PrimaryButton variant={VARIANTS.OUTLINED} onClick={createCourse} label={$t('dashboard.create_course')} />
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Empty
|
||||
title={$t('dashboard.create_first_course')}
|
||||
description={$t('dashboard.create_first_course_description')}
|
||||
icon={BookIcon}
|
||||
class="h-full"
|
||||
>
|
||||
<CreateCourseButton variant="outline" />
|
||||
</Empty>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 md:p-5 dark:border-neutral-600 dark:text-white">
|
||||
<h3 class="mt-0 text-2xl">
|
||||
{$t('dashboard.recent_enrollments')}
|
||||
</h3>
|
||||
<div
|
||||
class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 md:p-5 dark:border-neutral-600 dark:text-white"
|
||||
>
|
||||
<h3 class="mt-0 text-lg">
|
||||
{$t('dashboard.recent_enrollments')}
|
||||
</h3>
|
||||
|
||||
<div class="h-full space-y-6">
|
||||
{#if !dashAnalytics}
|
||||
{#each Array(5) as _}
|
||||
<Skeleton class="h-10 w-full rounded-md" />
|
||||
{/each}
|
||||
{:else}
|
||||
{#each dashAnalytics.enrollments as enrollment}
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Avatar src={enrollment.avatarUrl} name={enrollment.name} width="w-6" height="h-6" />
|
||||
<div class="h-full space-y-6">
|
||||
{#if !dashAnalytics}
|
||||
{#each Array(5) as _}
|
||||
<Skeleton class="h-10 w-full rounded-md" />
|
||||
{/each}
|
||||
{:else}
|
||||
{#each dashAnalytics.enrollments as enrollment}
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Avatar src={enrollment.avatarUrl} name={enrollment.name} width="w-6" height="h-6" />
|
||||
|
||||
<div class="min-h-[45px] space-y-1">
|
||||
<p class="text-sm font-medium capitalize leading-none">{enrollment.name}</p>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
<span class="italic">
|
||||
{calDateDiff(enrollment.date)}
|
||||
</span>
|
||||
</p>
|
||||
<div class="min-h-[45px] space-y-1">
|
||||
<p class="text-sm font-medium capitalize leading-none">{enrollment.name}</p>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
<span class="italic">
|
||||
{calDateDiff(enrollment.date)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-2/4">
|
||||
<a href={`/courses/${enrollment.courseId}`}>
|
||||
<p class="line-clamp-2 pb-[0.1rem] text-sm font-medium leading-none">
|
||||
{enrollment.course}
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-2/4">
|
||||
<a href={`/courses/${enrollment.courseId}`}>
|
||||
<p class="line-clamp-2 pb-[0.1rem] text-sm font-medium leading-none">
|
||||
{enrollment.course}
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col h-full items-center justify-center p-3">
|
||||
<div class="bg-primary-200 w-fit rounded-full p-4 text-black">
|
||||
<UserIcon size={16} />
|
||||
</div>
|
||||
<div class="my-4 text-center">
|
||||
<p class=" text-xl font-semibold">
|
||||
{$t('dashboard.publish_first_course')}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-300">
|
||||
{$t('dashboard.publish_first_course_description')}
|
||||
</p>
|
||||
</div>
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => goto(`${$currentOrgPath}/courses`)}
|
||||
label={$t('dashboard.publish_course')}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{:else}
|
||||
<Empty
|
||||
title={$t('dashboard.publish_first_course')}
|
||||
description={$t('dashboard.publish_first_course_description')}
|
||||
icon={UserIcon}
|
||||
class="h-full"
|
||||
>
|
||||
<Button variant="outline" onclick={() => goto(`${$currentOrgPath}/courses`)}>
|
||||
{$t('dashboard.publish_course')}
|
||||
</Button>
|
||||
</Empty>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
</Page.Root>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script>
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import Audience from '$lib/components/Org/Audience/index.svelte';
|
||||
import { AudiencePage } from '$lib/features/audience/pages';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { orgAudience, currentOrgPlan, currentOrgMaxAudience } from '$lib/utils/store/org';
|
||||
import { currentOrgPlan, currentOrgMaxAudience } from '$lib/utils/store/org';
|
||||
import { PLAN } from '@cio/utils/plans';
|
||||
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
import { orgApi } from '$lib/features/org/api/org.svelte';
|
||||
|
||||
let isLoading = $state(false);
|
||||
|
||||
@@ -19,23 +20,25 @@
|
||||
<title>Audience - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="mx-auto w-full max-w-6xl">
|
||||
<div class="px-5 py-10">
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<h1 class="m-0 text-2xl md:text-3xl dark:text-white">{$t('audience.title')}</h1>
|
||||
<Page.Root class="mx-auto w-full max-w-6xl">
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>
|
||||
{$t('audience.title')}
|
||||
{#if $currentOrgPlan?.planName !== PLAN.ENTERPRISE}
|
||||
<span class="ml-2">
|
||||
({$orgAudience.length} / {$currentOrgMaxAudience})
|
||||
<span class="ml-2 text-sm">
|
||||
({orgApi.audience.length} / {$currentOrgMaxAudience})
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<PrimaryButton label={$t('audience.export')} onClick={exportAudience} isDisabled={isLoading} {isLoading} />
|
||||
</div>
|
||||
|
||||
{#if $orgAudience.length >= $currentOrgMaxAudience}
|
||||
<UpgradeBanner>{$t('audience.upgrade')}</UpgradeBanner>
|
||||
{/if}
|
||||
<Audience />
|
||||
</div>
|
||||
</section>
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<AudiencePage />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
</Page.Root>
|
||||
|
||||
@@ -1,208 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { Badge } from '@cio/ui/base/badge';
|
||||
import UnfoldVerticalIcon from '@lucide/svelte/icons/unfold-vertical';
|
||||
|
||||
import BookOpenIcon from '@lucide/svelte/icons/book-open';
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
import ChartLineIcon from '@lucide/svelte/icons/chart-line';
|
||||
import Progress from '$lib/components/Progress/index.svelte';
|
||||
import { Grid } from '$lib/components/Org/Settings/Layout';
|
||||
import { ActivityCard, HeroProfileCard, LoadingPage } from '$lib/components/Analytics';
|
||||
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { currentOrgPath } from '$lib/utils/store/org';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { getAccessToken } from '$lib/utils/functions/supabase';
|
||||
import type { UserAnalytics } from '$lib/utils/types/analytics';
|
||||
import { UserAnalyticsPage } from '$lib/features/audience/pages';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let userAnalytics: UserAnalytics | undefined = $state();
|
||||
|
||||
let courseFilter = $state('all');
|
||||
function toggleCourseFilter(filter: 'completed' | 'incomplete') {
|
||||
if (courseFilter === filter) {
|
||||
courseFilter = 'all';
|
||||
} else {
|
||||
courseFilter = filter;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUserAnalytics() {
|
||||
const accessToken = await getAccessToken();
|
||||
const response = await fetch('/api/analytics/user', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: accessToken
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(response);
|
||||
snackbar.error('Failed to fetch analytics data');
|
||||
}
|
||||
|
||||
userAnalytics = (await response.json()) as UserAnalytics;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchUserAnalytics();
|
||||
});
|
||||
|
||||
let userMetrics = $derived([
|
||||
{
|
||||
icon: BookOpenIcon,
|
||||
title: $t('analytics.enrolled_courses'),
|
||||
description: $t('analytics.enrolled_courses_description'),
|
||||
percentage: userAnalytics?.courses?.length || 0,
|
||||
hidePercentage: true
|
||||
},
|
||||
{
|
||||
icon: ChartLineIcon,
|
||||
title: $t('analytics.overall_course_progress'),
|
||||
description: $t('analytics.overall_course_progress_description'),
|
||||
percentage: userAnalytics?.overallCourseProgress || 0
|
||||
},
|
||||
{
|
||||
icon: UnfoldVerticalIcon,
|
||||
title: $t('analytics.total_average_grade'),
|
||||
description: $t('analytics.total_average_grade_description'),
|
||||
percentage: userAnalytics?.overallAverageGrade || 0
|
||||
}
|
||||
]);
|
||||
|
||||
let completedCourses = $derived(
|
||||
userAnalytics?.courses?.filter((course) => course.lessons_count === course.lessons_completed)?.length
|
||||
);
|
||||
let incompleteCourses = $derived(
|
||||
userAnalytics?.courses?.filter((course) => course.lessons_count !== course.lessons_completed)?.length
|
||||
);
|
||||
|
||||
let filteredCourses = $derived(
|
||||
userAnalytics?.courses?.filter((course) => {
|
||||
if (courseFilter === 'all') {
|
||||
return true;
|
||||
}
|
||||
return (course.lessons_count === course.lessons_completed) === (courseFilter === 'completed');
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if userAnalytics}
|
||||
<section class="w-full md:mx-auto md:max-w-5xl">
|
||||
<div class="p-5">
|
||||
<a class="text-md flex items-center text-gray-500 dark:text-white" href={`${$currentOrgPath}/audience`}>
|
||||
<ArrowLeftIcon size={16} />
|
||||
{$t('community.ask.go_back')}
|
||||
</a>
|
||||
</div>
|
||||
<svelte:head>
|
||||
<title>User Analytics - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="px-5 py-1">
|
||||
<HeroProfileCard user={userAnalytics.user} />
|
||||
|
||||
<Grid class="mt-5 px-0">
|
||||
<div class="mt-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{#each userMetrics as activity}
|
||||
<ActivityCard {activity} />
|
||||
{/each}
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
<div class="mt-5 rounded-md border p-3 md:p-5 dark:border-neutral-600">
|
||||
<h3 class="text-2xl">
|
||||
{$t('analytics.courses')}
|
||||
</h3>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-200">
|
||||
{$t('analytics.progress')}
|
||||
</p>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-200">
|
||||
{$t('analytics.progress_description', {
|
||||
value: completedCourses,
|
||||
total: userAnalytics.courses.length
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<Progress value={userAnalytics.overallCourseProgress} />
|
||||
<div class="flex items-center justify-between">
|
||||
<Badge
|
||||
type={courseFilter === 'incomplete' ? 'secondary' : 'outline'}
|
||||
class="text-yellow-700 dark:text-yellow-500"
|
||||
onclick={() => toggleCourseFilter('incomplete')}
|
||||
>
|
||||
{incompleteCourses}
|
||||
{$t('analytics.incomplete')}
|
||||
</Badge>
|
||||
<Badge
|
||||
type={courseFilter === 'completed' ? 'secondary' : 'outline'}
|
||||
class="text-green-700 dark:text-green-500"
|
||||
onclick={() => toggleCourseFilter('completed')}
|
||||
>
|
||||
{completedCourses}
|
||||
{$t('analytics.complete')}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#each filteredCourses as course, index}
|
||||
{#key index}
|
||||
<div
|
||||
class={`mt-5 w-full rounded-md border border-gray-200 p-5 ${
|
||||
course.lessons_count === course.lessons_completed
|
||||
? 'border-green-200 bg-green-50 dark:bg-green-100'
|
||||
: 'border-yellow-200 bg-yellow-50 dark:bg-yellow-100'
|
||||
}`}
|
||||
transition:fade={{ duration: 300 }}
|
||||
>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex w-4/5 items-center gap-4">
|
||||
<img
|
||||
src={course.logo || '/images/classroomio-course-img-template.jpg'}
|
||||
alt={course.title}
|
||||
class="h-20 w-24 rounded-md"
|
||||
/>
|
||||
<div class="mb-4 gap-4">
|
||||
<a href={`/courses/${course.id}`}>
|
||||
<p class="text-lg font-semibold text-gray-600">
|
||||
{course.title}
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<p class="line-clamp-2 text-sm text-gray-600">
|
||||
{course.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Badge
|
||||
class={`${
|
||||
course.lessons_count === course.lessons_completed
|
||||
? 'bg-green-200 text-green-700'
|
||||
: 'bg-yellow-200 text-yellow-700'
|
||||
}`}
|
||||
>
|
||||
{course.lessons_count === course.lessons_completed
|
||||
? $t('analytics.completed')
|
||||
: $t('analytics.incomplete')}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full items-center gap-1">
|
||||
<Progress value={course.progress_percentage} />
|
||||
<p>{course.progress_percentage}%</p>
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{:else}
|
||||
<LoadingPage />
|
||||
{/if}
|
||||
<Page.Root class="w-full md:max-w-5xl lg:mx-auto">
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<UserAnalyticsPage {data} />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
</Page.Root>
|
||||
|
||||
@@ -1,32 +1,26 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import Community from '$lib/components/Org/Community/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { currentOrgPath } from '$lib/utils/store/org';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
import { CommunityPage } from '$lib/features/community/pages';
|
||||
import { AskCommunityButton } from '$lib/features/community/components';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
function askCommunity() {
|
||||
goto(`${$currentOrgPath}/community/ask`);
|
||||
}
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Community - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full">
|
||||
<div class="mb-10 flex w-full items-center justify-between">
|
||||
<h1 class="text-2xl md:text-3xl dark:text-white">{$t('community.title')}</h1>
|
||||
{#if $isMobile}
|
||||
<PrimaryButton onClick={askCommunity}>
|
||||
<PlusIcon size={16} />
|
||||
</PrimaryButton>
|
||||
{:else}
|
||||
<PrimaryButton label={$t('community.ask_button')} variant={VARIANTS.CONTAINED_DARK} onClick={askCommunity} />
|
||||
{/if}
|
||||
</div>
|
||||
<Community />
|
||||
</section>
|
||||
<Page.Root class="w-full">
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>{$t('community.title')}</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<AskCommunityButton />
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<CommunityPage />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
</Page.Root>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { courses } from '$lib/components/Courses/store';
|
||||
import { courses } from '$lib/features/course/utils/store';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
@@ -25,8 +25,9 @@
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import DeleteModal from '$lib/components/Org/Community/DeleteModal.svelte';
|
||||
import { CommunityDeleteModal } from '$lib/features/community/components';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
|
||||
interface Comment {
|
||||
id: string;
|
||||
@@ -377,18 +378,7 @@
|
||||
<title>{question?.title || $t('community.ask.question')}</title>
|
||||
</svelte:head>
|
||||
|
||||
<DeleteModal
|
||||
bind:open={deleteQuestion.shouldDelete}
|
||||
isDeleting={deleteQuestion.isDeleting}
|
||||
onCancel={() => {
|
||||
deleteQuestion.shouldDelete = false;
|
||||
deleteQuestion.questionId = '';
|
||||
}}
|
||||
onDelete={() => handleDelete(true)}
|
||||
isQuestion={true}
|
||||
/>
|
||||
|
||||
<DeleteModal
|
||||
<CommunityDeleteModal
|
||||
bind:open={deleteComment.shouldDelete}
|
||||
isDeleting={deleteComment.isDeleting}
|
||||
onCancel={() => {
|
||||
@@ -397,44 +387,50 @@
|
||||
}}
|
||||
onDelete={() => handleDelete(false)}
|
||||
/>
|
||||
<section class="mx-auto max-w-3xl md:mx-10 lg:mb-20">
|
||||
{#if !question}
|
||||
<div class="mb-3 px-5 py-10">
|
||||
<Skeleton class="h-4 w-[25%]" />
|
||||
<Skeleton class="mb-2 h-4 w-full" />
|
||||
<Skeleton class="h-80 w-full" />
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
{#if !question}
|
||||
<div class="mb-3 px-5 py-10">
|
||||
<Skeleton class="h-4 w-[25%]" />
|
||||
<Skeleton class="mb-2 h-4 w-full" />
|
||||
<Skeleton class="h-80 w-full" />
|
||||
</div>
|
||||
{:else}
|
||||
<Page.Root class="mx-auto max-w-3xl md:mx-10 lg:mb-20">
|
||||
<div class="px-5 py-10">
|
||||
<a class="text-md flex items-center text-gray-500 dark:text-white" href={`${$currentOrgPath}/community`}>
|
||||
<ArrowLeftIcon size={16} />
|
||||
{$t('community.ask.go_back')}
|
||||
</a>
|
||||
<div class="my-5 flex items-center justify-between">
|
||||
</div>
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
{#if isEditMode}
|
||||
<TextField bind:value={editContent.title} className="w-full mr-2" errorMessage={errors.title} />
|
||||
<Select.Root type="single" bind:value={editContent.courseId}>
|
||||
<Select.Trigger class="h-full w-[25%]">
|
||||
<p>
|
||||
{editContent.courseId
|
||||
? fetchedCourses.find((course) => course.id === editContent.courseId)?.title
|
||||
: $t('community.ask.select_course')}
|
||||
</p>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each fetchedCourses as course}
|
||||
<Select.Item value={course.id || ''}>{course.title}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<div class="flex w-full items-center gap-2">
|
||||
<TextField bind:value={editContent.title} className="flex-1" errorMessage={errors.title} />
|
||||
<Select.Root type="single" bind:value={editContent.courseId}>
|
||||
<Select.Trigger class="h-full w-[25%]">
|
||||
<p>
|
||||
{editContent.courseId
|
||||
? fetchedCourses.find((course) => course.id === editContent.courseId)?.title
|
||||
: $t('community.ask.select_course')}
|
||||
</p>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each fetchedCourses as course}
|
||||
<Select.Item value={course.id || ''}>{course.title}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<Vote value={question.votes} upVote={() => upvoteQuestion('question')} disabled={voted.question} />
|
||||
<h2 class="text-3xl">{question.title}</h2>
|
||||
<Page.Title>{question.title}</Page.Title>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if question.author.id === $profile.id}
|
||||
</Page.HeaderContent>
|
||||
{#if question.author.id === $profile.id}
|
||||
<Page.Action>
|
||||
<PrimaryButton
|
||||
label={isEditMode ? $t('community.ask.save') : $t('community.ask.edit')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
@@ -450,101 +446,105 @@
|
||||
disablePadding={true}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="border-gray my-1 rounded-lg border px-1">
|
||||
<header class="flex items-center justify-between p-2 leading-none">
|
||||
<div class="flex items-center text-black no-underline hover:underline">
|
||||
<Avatar src={question.author.avatar} name={question.author.name} width="w-7" height="h-7" />
|
||||
<p class="ml-2 text-sm dark:text-white">{question.author.name}</p>
|
||||
<p class="ml-2 text-sm text-gray-500 dark:text-white">
|
||||
{question.createdAt}
|
||||
</p>
|
||||
</div>
|
||||
{#if question.author.id === $profile.id || $isOrgAdmin}
|
||||
<IconButton
|
||||
value="delete-question"
|
||||
onClick={() => {
|
||||
if (!question) return;
|
||||
</Page.Action>
|
||||
{/if}
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<div class="border-gray my-1 rounded-lg border px-1">
|
||||
<header class="flex items-center justify-between p-2 leading-none">
|
||||
<div class="flex items-center text-black no-underline hover:underline">
|
||||
<Avatar src={question.author.avatar} name={question.author.name} width="w-7" height="h-7" />
|
||||
<p class="ml-2 text-sm dark:text-white">{question.author.name}</p>
|
||||
<p class="ml-2 text-sm text-gray-500 dark:text-white">
|
||||
{question.createdAt}
|
||||
</p>
|
||||
</div>
|
||||
{#if question.author.id === $profile.id || $isOrgAdmin}
|
||||
<IconButton
|
||||
value="delete-question"
|
||||
onClick={() => {
|
||||
if (!question) return;
|
||||
|
||||
deleteQuestion.shouldDelete = true;
|
||||
deleteQuestion.questionId = question.id;
|
||||
}}
|
||||
>
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
deleteQuestion.shouldDelete = true;
|
||||
deleteQuestion.questionId = question.id;
|
||||
}}
|
||||
>
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</header>
|
||||
{#if isEditMode && editorInstance}
|
||||
<div class="my-2">
|
||||
<TextEditor
|
||||
placeholder="Give an answer"
|
||||
content={editContent.body}
|
||||
onChange={(content) => (editContent.body = content)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<section class="prose prose-sm sm:prose p-2">
|
||||
{@html question.body}
|
||||
</section>
|
||||
{/if}
|
||||
</header>
|
||||
{#if isEditMode && editorInstance}
|
||||
<div class="my-2">
|
||||
<TextEditor
|
||||
placeholder="Give an answer"
|
||||
content={editContent.body}
|
||||
onChange={(content) => (editContent.body = content)}
|
||||
</div>
|
||||
|
||||
<div class="my-8">
|
||||
{pluralize($t('community.answers'), question.totalComments, true)}
|
||||
</div>
|
||||
|
||||
{#each question.comments as comment}
|
||||
<div class="my-5 flex items-start px-1">
|
||||
<Vote
|
||||
value={comment.votes}
|
||||
upVote={() => upvoteQuestion('comment', comment.id)}
|
||||
disabled={voted.comment[comment.id]}
|
||||
/>
|
||||
<div class="border-gray w-full rounded-lg border">
|
||||
<header class="flex items-center justify-between p-2 leading-none">
|
||||
<div class="flex items-center text-black">
|
||||
<Avatar src={comment.avatar} name={comment.name} width="w-7" height="h-7" />
|
||||
<p class="ml-2 text-sm dark:text-white">{comment.name}</p>
|
||||
<p class="ml-2 text-sm text-gray-500 dark:text-white">
|
||||
{comment.createdAt}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if isValidAnswer}
|
||||
<CircleCheckIcon size={16} />
|
||||
{/if}
|
||||
|
||||
{#if comment.authorId === $profile.id || $isOrgAdmin}
|
||||
<IconButton
|
||||
value="delete-comment"
|
||||
onClick={() => {
|
||||
deleteComment.shouldDelete = true;
|
||||
deleteComment.commentId = comment.id;
|
||||
}}
|
||||
>
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</header>
|
||||
<article class="prose prose-sm sm:prose p-2">
|
||||
{@html comment.comment}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<section class="prose prose-sm p-2 sm:prose">
|
||||
{@html question.body}
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="my-8">
|
||||
{pluralize($t('community.answers'), question.totalComments, true)}
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
{#each question.comments as comment}
|
||||
<div class="my-5 flex items-start px-1">
|
||||
<Vote
|
||||
value={comment.votes}
|
||||
upVote={() => upvoteQuestion('comment', comment.id)}
|
||||
disabled={voted.comment[comment.id]}
|
||||
/>
|
||||
<div class="border-gray w-full rounded-lg border">
|
||||
<header class="flex items-center justify-between p-2 leading-none">
|
||||
<div class="flex items-center text-black">
|
||||
<Avatar src={comment.avatar} name={comment.name} width="w-7" height="h-7" />
|
||||
<p class="ml-2 text-sm dark:text-white">{comment.name}</p>
|
||||
<p class="ml-2 text-sm text-gray-500 dark:text-white">
|
||||
{comment.createdAt}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
{#if !editorInstance}
|
||||
<TextEditor placeholder="Give an answer" content={comment} onChange={(content) => (comment = content)} />
|
||||
{/if}
|
||||
|
||||
{#if isValidAnswer}
|
||||
<CircleCheckIcon size={16} />
|
||||
{/if}
|
||||
|
||||
{#if comment.authorId === $profile.id || $isOrgAdmin}
|
||||
<IconButton
|
||||
value="delete-comment"
|
||||
onClick={() => {
|
||||
deleteComment.shouldDelete = true;
|
||||
deleteComment.commentId = comment.id;
|
||||
}}
|
||||
>
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</header>
|
||||
<article class="prose prose-sm p-2 sm:prose">
|
||||
{@html comment.comment}
|
||||
</article>
|
||||
<div class="mt-2 flex justify-end">
|
||||
<PrimaryButton label={$t('community.ask.comment')} onClick={submitComment} />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="mt-4">
|
||||
{#if !editorInstance}
|
||||
<TextEditor placeholder="Give an answer" content={comment} onChange={(content) => (comment = content)} />
|
||||
{/if}
|
||||
|
||||
<div class="mt-2 flex justify-end">
|
||||
<PrimaryButton label={$t('community.ask.comment')} onClick={submitComment} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
</Page.Root>
|
||||
{/if}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import * as Select from '@cio/ui/base/select';
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { courses } from '$lib/components/Courses/store';
|
||||
import { courses } from '$lib/features/course/utils/store';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { fetchCourses } from '$lib/utils/services/courses';
|
||||
@@ -18,6 +17,7 @@
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
|
||||
let errors: {
|
||||
title?: string;
|
||||
@@ -93,52 +93,59 @@
|
||||
<title>Ask the Community - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full md:mx-auto md:max-w-3xl">
|
||||
<div class="p-5">
|
||||
<a class="text-md flex items-center text-gray-500 dark:text-white" href={`${$currentOrgPath}/community`}>
|
||||
<ArrowLeftIcon size={16} />
|
||||
{$t('community.ask.go_back')}
|
||||
</a>
|
||||
<div class="flex items-center justify-between gap-12">
|
||||
<h1 class="text-2xl md:text-3xl dark:text-white">{$t('community.ask.ask_the')}</h1>
|
||||
<Page.Root>
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>{$t('community.ask.ask_the')}</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<PrimaryButton label={$t('community.ask.publish')} variant={VARIANTS.CONTAINED_DARK} onClick={handleSave} />
|
||||
</div>
|
||||
</div>
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<div class="mb-3 flex justify-between gap-x-5">
|
||||
<TextField
|
||||
bind:value={fields.title}
|
||||
placeholder={$t('community.ask.title')}
|
||||
errorMessage={errors.title}
|
||||
className="w-[75%]"
|
||||
/>
|
||||
<div class="w-[25%]">
|
||||
<Select.Root type="single" bind:value={fields.courseId}>
|
||||
<Select.Trigger class="h-full w-full">
|
||||
<p>
|
||||
{fields.courseId
|
||||
? fetchedCourses.find((course) => course.id === fields.courseId)?.title
|
||||
: $t('community.ask.select_course')}
|
||||
</p>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each fetchedCourses as course}
|
||||
{#if course.id}
|
||||
<Select.Item value={course.id}>{course.title}</Select.Item>
|
||||
{/if}
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{#if errors.courseId}
|
||||
<p class="mt-1 text-sm text-red-500">
|
||||
{errors.courseId}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<TextEditor
|
||||
placeholder="Give an answer"
|
||||
content={fields.body}
|
||||
onChange={(content) => (fields.body = content)}
|
||||
/>
|
||||
|
||||
<div class="mb-3 flex justify-between gap-x-5 p-2">
|
||||
<TextField
|
||||
bind:value={fields.title}
|
||||
placeholder={$t('community.ask.title')}
|
||||
errorMessage={errors.title}
|
||||
className="w-[75%]"
|
||||
/>
|
||||
<div class="w-[25%]">
|
||||
<Select.Root type="single" bind:value={fields.courseId}>
|
||||
<Select.Trigger class="h-full w-full">
|
||||
<p>
|
||||
{fields.courseId
|
||||
? fetchedCourses.find((course) => course.id === fields.courseId)?.title
|
||||
: $t('community.ask.select_course')}
|
||||
</p>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each fetchedCourses as course}
|
||||
<Select.Item value={course.id}>{course.title}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{#if errors.courseId}
|
||||
<p class="mt-1 text-sm text-red-500">
|
||||
{errors.courseId}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-2">
|
||||
<TextEditor placeholder="Give an answer" content={fields.body} onChange={(content) => (fields.body = content)} />
|
||||
|
||||
{#if errors.body}
|
||||
<p class="mt-2 text-sm text-red-500">{errors.body}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
{#if errors.body}
|
||||
<p class="mt-2 text-sm text-red-500">{errors.body}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
</Page.Root>
|
||||
|
||||
@@ -1,27 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import * as Select from '@cio/ui/base/select';
|
||||
import SearchIcon from '@lucide/svelte/icons/search';
|
||||
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { fetchCourses } from '$lib/utils/services/courses';
|
||||
import Courses from '$lib/components/Courses/index.svelte';
|
||||
import NewCourseModal from '$lib/components/Courses/components/NewCourseModal/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { courses, courseMetaDeta } from '$lib/components/Courses/store';
|
||||
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
import { CoursesPage } from '$lib/features/course/pages';
|
||||
import { CreateCourseButton } from '$lib/features/course/components';
|
||||
import { courses, courseMetaDeta } from '$lib/features/course/utils/store';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { browser } from '$app/environment';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import GridIcon from '@lucide/svelte/icons/grid-2x2';
|
||||
import ListIcon from '@lucide/svelte/icons/list';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
@@ -30,16 +18,6 @@
|
||||
let selectedId: string = $state('0');
|
||||
let hasFetched = false;
|
||||
|
||||
const filterOptions = $derived([
|
||||
{ value: '0', label: $t('courses.course_filter.date_created') },
|
||||
{ value: '1', label: $t('courses.course_filter.published') },
|
||||
{ value: '2', label: $t('courses.course_filter.lessons') }
|
||||
]);
|
||||
|
||||
const selectedLabel = $derived(
|
||||
filterOptions.find((opt) => opt.value === selectedId)?.label || filterOptions[0].label
|
||||
);
|
||||
|
||||
const filteredCourses: Course[] = $derived(filterCourses(searchValue, selectedId, $courses));
|
||||
|
||||
async function getCourses(userId: string | undefined, orgId: string) {
|
||||
@@ -89,15 +67,6 @@
|
||||
return filteredCourses;
|
||||
}
|
||||
|
||||
const setViewPreference = (preference: 'grid' | 'list') => {
|
||||
$courseMetaDeta.view = preference;
|
||||
localStorage.setItem('courseView', preference);
|
||||
};
|
||||
|
||||
const openNewCourseModal = () => {
|
||||
goto($currentOrgPath + '/courses?create=true');
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
const courseView = localStorage.getItem('courseView') as 'grid' | 'list' | null;
|
||||
|
||||
@@ -115,51 +84,18 @@
|
||||
<title>Courses - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full">
|
||||
<div class="mb-5 flex items-center justify-between">
|
||||
<h1 class="text-2xl md:text-3xl dark:text-white">{$t('courses.heading')}</h1>
|
||||
{#if $isMobile}
|
||||
<PrimaryButton isDisabled={!$isOrgAdmin} onClick={openNewCourseModal}>
|
||||
<PlusIcon size={16} />
|
||||
</PrimaryButton>
|
||||
{:else}
|
||||
<PrimaryButton
|
||||
label={$t('courses.heading_button')}
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
isDisabled={!$isOrgAdmin}
|
||||
onClick={openNewCourseModal}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mb-5 flex flex-row-reverse">
|
||||
<div class="filter-containter flex items-end justify-start gap-2">
|
||||
<div class="relative">
|
||||
<Input type="text" placeholder={$t('courses.search_placeholder')} bind:value={searchValue} class="pl-9" />
|
||||
|
||||
<SearchIcon class="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2" />
|
||||
</div>
|
||||
<Select.Root type="single" bind:value={selectedId}>
|
||||
<Select.Trigger class="min-w-[150px]">
|
||||
<p>{selectedId ? selectedLabel : filterOptions[0].label}</p>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each filterOptions as option}
|
||||
<Select.Item value={option.value}>{option.label}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{#if $courseMetaDeta.view === 'list'}
|
||||
<IconButton onClick={() => setViewPreference('grid')}>
|
||||
<GridIcon size={16} />
|
||||
</IconButton>
|
||||
{:else}
|
||||
<IconButton onClick={() => setViewPreference('list')}>
|
||||
<ListIcon size={16} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NewCourseModal />
|
||||
<Courses courses={filteredCourses} />
|
||||
</section>
|
||||
<Page.Root class="w-full">
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>{$t('courses.heading')}</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<CreateCourseButton isResponsive />
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<CoursesPage courses={filteredCourses} bind:searchValue bind:selectedId />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
</Page.Root>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
</script>
|
||||
|
||||
<Page.Root class="w-full pb-10 md:max-w-3xl lg:mx-auto">
|
||||
{@render children?.()}
|
||||
</Page.Root>
|
||||
@@ -1,21 +1,40 @@
|
||||
<script>
|
||||
import OrgSettings from '$lib/components/Org/Settings/index.svelte';
|
||||
<script lang="ts">
|
||||
import { ProfilePage } from '$lib/features/settings/pages';
|
||||
import ViewSiteBtn from '$lib/components/Buttons/VisitOrgSite.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
|
||||
let profileComponent: ProfilePage | null = $state(null);
|
||||
let isLoading = $state(false);
|
||||
|
||||
async function handleUpdate() {
|
||||
isLoading = true;
|
||||
try {
|
||||
await profileComponent?.handleUpdate();
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Settings - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full md:max-w-6xl lg:mx-auto">
|
||||
<div class="px-5 md:py-10">
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<h1 class="text-3xl dark:text-white">{$t('settings.heading')}</h1>
|
||||
|
||||
<ViewSiteBtn />
|
||||
</div>
|
||||
|
||||
<OrgSettings />
|
||||
</div>
|
||||
</section>
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>{$t('settings.heading')}</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<Button variant="default" loading={isLoading} onclick={handleUpdate}>
|
||||
{$t('settings.profile.update_profile')}
|
||||
</Button>
|
||||
<ViewSiteBtn />
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<ProfilePage bind:this={profileComponent} />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import { BillingPage } from '$lib/features/settings/pages';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Billing - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>{$t('settings.billing.sub_title')}</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<BillingPage />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
@@ -1,24 +1,40 @@
|
||||
<script>
|
||||
import CustomizeLMS from '$lib/components/Org/Settings/CustomizeLms.svelte';
|
||||
<script lang="ts">
|
||||
import { CustomizeLmsPage } from '$lib/features/settings/pages';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { currentOrgPath } from '$lib/utils/store/org';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
|
||||
let customizeLmsComponent: CustomizeLmsPage | null = $state(null);
|
||||
let isSaving = $state(false);
|
||||
|
||||
async function handleSave() {
|
||||
isSaving = true;
|
||||
try {
|
||||
await customizeLmsComponent?.handleSave();
|
||||
} finally {
|
||||
isSaving = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Customize LMS - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="mx-auto w-full md:max-w-4xl">
|
||||
<div class="px-3 py-10 md:px-5">
|
||||
<a class="text-md text-gray-500 dark:text-white" href={`${$currentOrgPath}/settings?tab=org`}
|
||||
>{$t('upgrade.back')}</a
|
||||
>
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<h1 class="text-3xl dark:text-white">
|
||||
{$t('components.settings.customize_lms.title')}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<CustomizeLMS />
|
||||
</div>
|
||||
</section>
|
||||
<Page.Root class="w-full md:max-w-4xl lg:mx-auto">
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>{$t('components.settings.customize_lms.title')}</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<Button variant="default" loading={isSaving} disabled={isSaving} onclick={handleSave}>
|
||||
{$t('components.settings.customize_lms.save')}
|
||||
</Button>
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<CustomizeLmsPage bind:this={customizeLmsComponent} />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
</Page.Root>
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
<script>
|
||||
import Domains from '$lib/components/Org/Settings/Domains.svelte';
|
||||
import { DomainsPage } from '$lib/features/settings/pages';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { currentOrgPath } from '$lib/utils/store/org';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Custom domain - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="mx-auto w-full md:max-w-4xl">
|
||||
<div class="px-3 py-10 md:px-5">
|
||||
<a class="text-md text-gray-500 dark:text-white" href={`${$currentOrgPath}/settings?tab=org`}
|
||||
>{$t('upgrade.back')}</a
|
||||
>
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<h1 class="text-3xl dark:text-white">{$t('components.settings.domains.title')}</h1>
|
||||
</div>
|
||||
|
||||
<Domains />
|
||||
</div>
|
||||
</section>
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>{$t('components.settings.domains.title')}</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<DomainsPage />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import { IntegrationsPage } from '$lib/features/settings/pages';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Integrations - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>{$t('settings.integrations.heading')}</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<IntegrationsPage />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { LandingpagePage } from '$lib/features/settings/pages';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
|
||||
let landingPageComponent: LandingpagePage | null = $state(null);
|
||||
let isSaving = $state(false);
|
||||
|
||||
async function handleSave() {
|
||||
isSaving = true;
|
||||
try {
|
||||
await landingPageComponent?.handleSave();
|
||||
} finally {
|
||||
isSaving = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Landing Page Settings - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>{$t('settings.landing_page.heading')}</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<Button variant="default" loading={isSaving} disabled={isSaving} onclick={handleSave}>
|
||||
{$t('settings.landing_page.save_changes')}
|
||||
</Button>
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<LandingpagePage bind:this={landingPageComponent} />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { OrgPage } from '$lib/features/settings/pages';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
|
||||
let orgComponent: OrgPage | null = $state(null);
|
||||
let loading = $state(false);
|
||||
|
||||
async function handleUpdate() {
|
||||
loading = true;
|
||||
try {
|
||||
await orgComponent?.handleUpdate();
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Organization Settings - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>{$t('settings.organization.organization_profile.heading')}</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<Button variant="default" {loading} disabled={loading} onclick={handleUpdate}>
|
||||
{$t('settings.organization.organization_profile.update_organization')}
|
||||
</Button>
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<OrgPage bind:this={orgComponent} />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
@@ -1,24 +1,20 @@
|
||||
<script>
|
||||
import TeamMembers from '$lib/components/Org/Settings/TeamMembers.svelte';
|
||||
import { currentOrgPath } from '$lib/utils/store/org';
|
||||
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
|
||||
import { TeamsPage } from '$lib/features/settings/pages';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Teams - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="mx-auto w-full md:max-w-4xl">
|
||||
<div class="px-3 py-10 md:px-5">
|
||||
<a class="text-md text-gray-500 dark:text-white" href={`${$currentOrgPath}/settings?tab=org`}
|
||||
>{$t('upgrade.back')}</a
|
||||
>
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<h1 class="text-3xl dark:text-white">{$t('upgrade.team_members')}</h1>
|
||||
</div>
|
||||
|
||||
<UpgradeBanner>{$t('upgrade.team')}</UpgradeBanner>
|
||||
<TeamMembers />
|
||||
</div>
|
||||
</section>
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>{$t('upgrade.team_members')}</Page.Title>
|
||||
</Page.HeaderContent>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<TeamsPage />
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
|
||||
@@ -1,26 +1,46 @@
|
||||
<script lang="ts">
|
||||
import Chip from '$lib/components/Chip/Text.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Item from '@cio/ui/base/item';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
import BadgeCheckIcon from '@lucide/svelte/icons/badge-check';
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||
import BookOpenIcon from '@lucide/svelte/icons/book-open';
|
||||
import UserPlusIcon from '@lucide/svelte/icons/user-plus';
|
||||
import UsersIcon from '@lucide/svelte/icons/users';
|
||||
import GlobeIcon from '@lucide/svelte/icons/globe';
|
||||
import FileTextIcon from '@lucide/svelte/icons/file-text';
|
||||
import SetupProgress from '$lib/features/setup/components/setup-progress.svelte';
|
||||
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { goto } from '$app/navigation';
|
||||
import { snackbar } from '$lib/components/Snackbar/store.js';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { setupProgressApi } from '$lib/features/setup/api/setup-progress.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let { data } = $props();
|
||||
const setupList = $derived(
|
||||
(data.setup || []).map((item) => {
|
||||
setupProgressApi.setupList.map((item) => {
|
||||
if (item.id === 'profile') {
|
||||
item.is_completed = !$profile.avatarUrl?.includes('avatars/avatar.png');
|
||||
return {
|
||||
...item,
|
||||
is_completed: !$profile.avatarUrl?.includes('avatars/avatar.png')
|
||||
};
|
||||
}
|
||||
return item;
|
||||
})
|
||||
);
|
||||
|
||||
const completed = $derived(setupList.filter((list) => list.is_completed).length);
|
||||
const total = $derived(setupList.length);
|
||||
|
||||
// Ensure setup progress is fetched when page loads
|
||||
onMount(() => {
|
||||
if ($currentOrg.siteName && setupProgressApi.progress.setup.length === 0) {
|
||||
// The app-header should have already fetched it, but ensure it's loaded
|
||||
setupProgressApi.fetchSetupProgress($currentOrg.siteName);
|
||||
}
|
||||
});
|
||||
|
||||
const StepsEnum = {
|
||||
UPDATE_PROFILE: 'profile',
|
||||
@@ -43,8 +63,11 @@
|
||||
|
||||
case StepsEnum.CREATE_LESSON:
|
||||
if (isCompleted('course')) {
|
||||
const courseId = data?.courses[0].id;
|
||||
goto(`/courses/${courseId}/lessons`);
|
||||
const courses = setupProgressApi.progress.courses || [];
|
||||
const courseId = courses[0]?.id;
|
||||
if (courseId) {
|
||||
goto(`/courses/${courseId}/lessons`);
|
||||
}
|
||||
} else {
|
||||
snackbar.info('setup.info_course');
|
||||
}
|
||||
@@ -52,9 +75,13 @@
|
||||
|
||||
case StepsEnum.CREATE_EXERCISE:
|
||||
if (isCompleted('lesson')) {
|
||||
const courseId = data?.courses[0].id;
|
||||
const lessonId = data?.lessons[0].id;
|
||||
goto(`/courses/${courseId}/lessons/${lessonId}`);
|
||||
const courses = setupProgressApi.progress.courses || [];
|
||||
const lessons = setupProgressApi.progress.lessons || [];
|
||||
const courseId = courses[0]?.id;
|
||||
const lessonId = lessons[0]?.id;
|
||||
if (courseId && lessonId) {
|
||||
goto(`/courses/${courseId}/lessons/${lessonId}`);
|
||||
}
|
||||
} else {
|
||||
snackbar.info('setup.info_lesson');
|
||||
}
|
||||
@@ -62,8 +89,11 @@
|
||||
|
||||
case StepsEnum.PUBLISH_COURSE:
|
||||
if (isCompleted('course')) {
|
||||
const courseId = data?.courses[0].id;
|
||||
goto(`/courses/${courseId}/settings`);
|
||||
const courses = setupProgressApi.progress.courses || [];
|
||||
const courseId = courses[0]?.id;
|
||||
if (courseId) {
|
||||
goto(`/courses/${courseId}/settings`);
|
||||
}
|
||||
} else {
|
||||
snackbar.info('setup.info_course');
|
||||
}
|
||||
@@ -74,52 +104,98 @@
|
||||
break;
|
||||
|
||||
case StepsEnum.UPDATE_ORG_PROFILE:
|
||||
goto(`/org/${$currentOrg.siteName}/settings?tab=org`);
|
||||
goto(`/org/${$currentOrg.siteName}/settings/org`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="mx-auto w-full md:max-w-3xl">
|
||||
<div class="px-2 py-2 md:px-5 md:py-10">
|
||||
<div class="flex items-center gap-2">
|
||||
<h1 class="text-2xl md:text-3xl dark:text-white">{$t('setup.get_started')}</h1>
|
||||
<Chip
|
||||
value={`${completed}/${setupList.length}`}
|
||||
className="text-[10px] font-semibold px-3 py-1!"
|
||||
shape="rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<svelte:head>
|
||||
<title>Setup - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="px-4">
|
||||
{#each setupList as list, i}
|
||||
<div
|
||||
class="flex w-full flex-col justify-between gap-2 border-b border-gray-300 py-8 lg:flex-row lg:items-center"
|
||||
>
|
||||
<div class={`flex-1 space-y-1 ${list.is_completed ? 'opacity-50' : ''} lg:max-w-[50%]`}>
|
||||
<div class="flex items-center gap-3">
|
||||
<Chip value={i + 1} className="text-[10px] font-semibold py-1" shape="rounded-full" />
|
||||
<p class="text-lg font-medium">{$t(list.title)}</p>
|
||||
<Page.Root class="w-full md:max-w-4xl lg:mx-auto">
|
||||
<Page.Header>
|
||||
<Page.HeaderContent>
|
||||
<Page.Title>{$t('setup.get_started')}</Page.Title>
|
||||
<Page.Subtitle>{$t('setup.description')}</Page.Subtitle>
|
||||
</Page.HeaderContent>
|
||||
|
||||
<Page.Action>
|
||||
<SetupProgress setupItems={setupList} />
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<section class="space-y-6 px-4">
|
||||
<Item.Root variant="outline" class="ui:cursor-default">
|
||||
<Item.Content>
|
||||
<Item.Description class="mb-4">
|
||||
{$t('setup.description')}
|
||||
</Item.Description>
|
||||
<div class="flex items-center gap-2">
|
||||
<Item.Description class="text-sm">
|
||||
{completed} of {total}
|
||||
{$t('setup.completed')}
|
||||
</Item.Description>
|
||||
<div class="flex gap-1">
|
||||
{#each Array(total) as _, i}
|
||||
<div
|
||||
class="size-2 rounded-full {i < completed ? 'bg-green-600' : 'bg-gray-300 dark:bg-gray-600'}"
|
||||
></div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm">
|
||||
{$t(list.desc)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-full lg:w-[30%]">
|
||||
<PrimaryButton
|
||||
variant={list.is_completed ? VARIANTS.CONTAINED_DARK : VARIANTS.OUTLINED}
|
||||
className="w-full font-normal text-sm flex items-center gap-2"
|
||||
onClick={() => handleClick(list.id)}
|
||||
isDisabled={list.is_completed}
|
||||
>
|
||||
{#if list.is_completed}
|
||||
<CircleCheckIcon size={16} />
|
||||
{/if}
|
||||
{$t(list.button_label)}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</Item.Content>
|
||||
</Item.Root>
|
||||
|
||||
<!-- Setup Items -->
|
||||
<Item.Group class="space-y-2">
|
||||
{#each setupList as list}
|
||||
<Item.Root variant="muted" class={list.is_completed ? 'opacity-60' : ''}>
|
||||
<Item.Media variant="icon">
|
||||
{#if list.is_completed}
|
||||
<div class="flex size-5 items-center justify-center rounded">
|
||||
<BadgeCheckIcon class="size-5 text-white" />
|
||||
</div>
|
||||
{:else if list.id === StepsEnum.CREATE_COURSE}
|
||||
<BookOpenIcon class="size-5 text-gray-600" />
|
||||
{:else if list.id === StepsEnum.CREATE_LESSON || list.id === StepsEnum.CREATE_EXERCISE}
|
||||
<FileTextIcon class="size-5 text-gray-600" />
|
||||
{:else if list.id === StepsEnum.PUBLISH_COURSE}
|
||||
<GlobeIcon class="size-5 text-gray-600" />
|
||||
{:else if list.id === StepsEnum.UPDATE_PROFILE}
|
||||
<UserPlusIcon class="size-5 text-gray-600" />
|
||||
{:else if list.id === StepsEnum.UPDATE_ORG_PROFILE}
|
||||
<UsersIcon class="size-5 text-gray-600" />
|
||||
{:else}
|
||||
<BookOpenIcon class="size-5 text-gray-600" />
|
||||
{/if}
|
||||
</Item.Media>
|
||||
<Item.Content>
|
||||
<Item.Title class={list.is_completed ? 'text-gray-500 line-through' : 'font-semibold'}>
|
||||
{$t(list.title)}
|
||||
</Item.Title>
|
||||
<Item.Description class={list.is_completed ? 'text-gray-500 line-through' : ''}>
|
||||
{$t(list.desc)}
|
||||
</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
{#if list.is_completed}
|
||||
<Button variant="secondary" size="sm" disabled class="bg-gray-400 text-white">
|
||||
{$t('setup.done')}
|
||||
</Button>
|
||||
{:else}
|
||||
<Button variant="outline" size="sm" onclick={() => handleClick(list.id)} class="w-full sm:w-auto">
|
||||
{$t('setup.todo')}
|
||||
<ChevronRightIcon class="ml-2 size-4" />
|
||||
</Button>
|
||||
{/if}
|
||||
</Item.Actions>
|
||||
</Item.Root>
|
||||
{/each}
|
||||
</Item.Group>
|
||||
</section>
|
||||
{/snippet}
|
||||
</Page.Body>
|
||||
</Page.Root>
|
||||
|
||||
@@ -1,160 +1,5 @@
|
||||
import get from 'lodash/get';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
|
||||
const supabase = getSupabase();
|
||||
|
||||
async function getGenericData(orgSlug: string): Promise<{
|
||||
isCoursePublished: boolean;
|
||||
isCourseCreated: boolean;
|
||||
orgHasAvatarUrl: boolean;
|
||||
courseData: any;
|
||||
}> {
|
||||
const { data: publishedCourse } = await supabase
|
||||
.from('course')
|
||||
.select(
|
||||
`
|
||||
is_published,
|
||||
group:group_id!inner(
|
||||
organization!inner(
|
||||
siteName
|
||||
)
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('is_published', true)
|
||||
.eq('group.organization.siteName', orgSlug)
|
||||
.limit(1)
|
||||
.returns<{ is_published: boolean }>();
|
||||
|
||||
const { data } = await supabase
|
||||
.from('course')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
is_published,
|
||||
group:group_id!inner(
|
||||
organization!inner(
|
||||
avatar_url,
|
||||
siteName
|
||||
)
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('group.organization.siteName', orgSlug)
|
||||
.limit(1);
|
||||
|
||||
const result = data || [];
|
||||
|
||||
return {
|
||||
isCoursePublished: publishedCourse?.is_published === true,
|
||||
isCourseCreated: result.length > 0,
|
||||
orgHasAvatarUrl: !!get(result[0], 'group.organization.avatar_url', ''),
|
||||
courseData: result
|
||||
};
|
||||
}
|
||||
|
||||
async function getIsLessonCreated(orgSlug: string): Promise<{ isLessonCreated: boolean; lessonData: any }> {
|
||||
const { data } = await supabase
|
||||
.from('lesson')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
course:course_id!inner(
|
||||
group:group_id!inner(
|
||||
organization!inner(
|
||||
siteName
|
||||
)
|
||||
)
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('course.group.organization.siteName', orgSlug)
|
||||
.limit(1);
|
||||
console.log('lessonsData', data);
|
||||
|
||||
return {
|
||||
isLessonCreated: Array.isArray(data) ? data.length > 0 : false,
|
||||
lessonData: data
|
||||
};
|
||||
}
|
||||
|
||||
async function getIsExerciseCreated(orgSlug: string): Promise<boolean> {
|
||||
const { data } = await supabase
|
||||
.from('exercise')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
lesson:lesson_id!inner(
|
||||
course:course_id!inner(
|
||||
group:group_id!inner(
|
||||
organization!inner(
|
||||
siteName
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('lesson.course.group.organization.siteName', orgSlug)
|
||||
.limit(1);
|
||||
|
||||
return Array.isArray(data) ? data?.length > 0 : false;
|
||||
}
|
||||
|
||||
export const load = async ({ params = { slug: '' } }) => {
|
||||
const { isCourseCreated, isCoursePublished, orgHasAvatarUrl, courseData } = await getGenericData(params.slug);
|
||||
const { isLessonCreated, lessonData } = await getIsLessonCreated(params.slug);
|
||||
const isExerciseCreated = await getIsExerciseCreated(params.slug);
|
||||
|
||||
const data = [
|
||||
{
|
||||
id: 'profile',
|
||||
title: 'setup.personal_profile.title',
|
||||
desc: 'setup.personal_profile.desc',
|
||||
is_completed: false,
|
||||
button_label: 'setup.personal_profile.button_label'
|
||||
},
|
||||
{
|
||||
id: 'organization',
|
||||
title: 'setup.organization_profile.title',
|
||||
desc: 'setup.organization_profile.desc',
|
||||
is_completed: orgHasAvatarUrl,
|
||||
button_label: 'setup.organization_profile.button_label'
|
||||
},
|
||||
{
|
||||
id: 'course',
|
||||
title: 'setup.course.title',
|
||||
desc: 'setup.course.desc',
|
||||
is_completed: isCourseCreated,
|
||||
button_label: 'setup.course.button_label'
|
||||
},
|
||||
{
|
||||
id: 'lesson',
|
||||
title: 'setup.lesson.title',
|
||||
desc: 'setup.lesson.desc',
|
||||
is_completed: isLessonCreated,
|
||||
button_label: 'setup.lesson.button_label'
|
||||
},
|
||||
{
|
||||
id: 'exercise',
|
||||
title: 'setup.exercise.title',
|
||||
desc: 'setup.exercise.desc',
|
||||
is_completed: isExerciseCreated,
|
||||
button_label: 'setup.exercise.button_label'
|
||||
},
|
||||
{
|
||||
id: 'publish',
|
||||
title: 'setup.publish_course.title',
|
||||
desc: 'setup.publish_course.desc',
|
||||
is_completed: isCoursePublished,
|
||||
button_label: 'setup.publish_course.button_label'
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
orgSiteName: params.slug,
|
||||
setup: data,
|
||||
courses: courseData,
|
||||
lessons: lessonData
|
||||
orgSiteName: params.slug
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { getConfirmPasswordError } from '$lib/utils/functions/validator';
|
||||
import AuthUI from '$lib/components/AuthUI/index.svelte';
|
||||
import { resetApi } from '$lib/features/auth/api/reset.svelte';
|
||||
import type { TResetPasswordForm } from '$lib/features/auth/utils/types';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
|
||||
let fields: TResetPasswordForm = $state({
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
token: ''
|
||||
});
|
||||
|
||||
const isSubmitDisabled = $derived!!(
|
||||
fields.password && fields.confirmPassword && fields.password !== fields.confirmPassword
|
||||
);
|
||||
const token = $derived(new URLSearchParams(page.url.search).get('token'));
|
||||
|
||||
onMount(() => {
|
||||
if (token) return;
|
||||
|
||||
snackbar.error('Invalid Token');
|
||||
|
||||
setTimeout(() => {
|
||||
goto(resolve('/login', {}));
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
function setConfirmPasswordError(fields: TResetPasswordForm) {
|
||||
untrack(() => {
|
||||
const errors = { ...resetApi.errors };
|
||||
errors.confirmPassword = getConfirmPasswordError(fields) ?? '';
|
||||
resetApi.setError(errors);
|
||||
});
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
setConfirmPasswordError(fields);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Reset Password - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<AuthUI
|
||||
isLogin={false}
|
||||
handleSubmit={() => resetApi.submit({ ...fields, token: token ?? '' })}
|
||||
showOnlyContent={true}
|
||||
isLoading={resetApi.isLoading}
|
||||
showLogo={true}
|
||||
>
|
||||
<div class="mt-4 w-full">
|
||||
<h3 class="my-3 text-xl font-semibold dark:text-white">New Password</h3>
|
||||
<p class="mb-6 text-sm dark:text-white">Enter your new password details</p>
|
||||
<TextField
|
||||
label="Your Password"
|
||||
bind:value={fields.password}
|
||||
type="password"
|
||||
placeholder="************"
|
||||
className="mb-6"
|
||||
inputClassName="w-full"
|
||||
isDisabled={resetApi.isLoading}
|
||||
errorMessage={resetApi.errors.password}
|
||||
helperMessage="Password must be more than 8 characters"
|
||||
isRequired
|
||||
/>
|
||||
<TextField
|
||||
label="Confirm Password"
|
||||
bind:value={fields.confirmPassword}
|
||||
type="password"
|
||||
placeholder="************"
|
||||
className="mb-6"
|
||||
inputClassName="w-full"
|
||||
isDisabled={resetApi.isLoading}
|
||||
errorMessage={resetApi.errors.confirmPassword}
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="my-4 flex w-full items-center justify-end">
|
||||
<PrimaryButton
|
||||
label="Reset Password"
|
||||
type="submit"
|
||||
className="sm:w-full w-full"
|
||||
isDisabled={isSubmitDisabled || resetApi.isLoading}
|
||||
isLoading={resetApi.isLoading}
|
||||
/>
|
||||
</div>
|
||||
</AuthUI>
|
||||
Reference in New Issue
Block a user