docker improvements + delete unused package
This commit is contained in:
+97
@@ -0,0 +1,97 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:18-alpine AS builder
|
||||||
|
|
||||||
|
# Install pnpm
|
||||||
|
RUN npm install -g [email protected] && \
|
||||||
|
pnpm config set use-git-checks false && \
|
||||||
|
pnpm config set git-checks false && \
|
||||||
|
apk add --no-cache git
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy root package files
|
||||||
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
|
||||||
|
# Copy workspace package files
|
||||||
|
COPY apps/dashboard/package.json ./apps/dashboard/
|
||||||
|
COPY packages/shared/package.json ./packages/shared/
|
||||||
|
COPY packages/tsconfig/package.json ./packages/tsconfig/
|
||||||
|
|
||||||
|
# Set environment variables for build
|
||||||
|
ENV IS_SELFHOSTED=true
|
||||||
|
ENV DEPLOYMENT_PROVIDER=docker
|
||||||
|
ENV NODE_OPTIONS="--max-old-space-size=40960"
|
||||||
|
ENV VITE_DISABLE_GIT=true
|
||||||
|
ENV SENTRY_DISABLE_GIT=true
|
||||||
|
ENV GIT_TERMINAL_PROMPT=0
|
||||||
|
ENV GIT_ASKPASS=/bin/true
|
||||||
|
ENV VITE_DEBUG=true
|
||||||
|
|
||||||
|
# Install all dependencies (including dev dependencies)
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN echo "Root node_modules contents:" && \
|
||||||
|
ls -la ./node_modules && \
|
||||||
|
echo "\nDashboard node_modules contents:" && \
|
||||||
|
ls -la ./apps/dashboard/node_modules
|
||||||
|
|
||||||
|
# Set production environment for build
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV DEPLOYMENT_PROVIDER=docker
|
||||||
|
ENV IS_SELFHOSTED=true
|
||||||
|
|
||||||
|
RUN git init && \
|
||||||
|
git config --global user.email "[email protected]" && \
|
||||||
|
git config --global user.name "Crom CI" && \
|
||||||
|
git add . && \
|
||||||
|
git commit -m "Initial commit for build"
|
||||||
|
|
||||||
|
# Build the application with increased timeout
|
||||||
|
RUN pnpm build --filter=dashboard -- --timeout 600000
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:18-alpine AS runner
|
||||||
|
|
||||||
|
# Install pnpm
|
||||||
|
RUN npm install -g [email protected]
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy only necessary files from builder
|
||||||
|
COPY --from=builder /app/package.json /app/pnpm-lock.yaml ./
|
||||||
|
COPY --from=builder /app/apps/dashboard/build ./apps/dashboard/build
|
||||||
|
COPY --from=builder /app/apps/dashboard/package.json ./apps/dashboard/
|
||||||
|
COPY --from=builder /app/packages/shared ./packages/shared
|
||||||
|
COPY --from=builder /app/packages/tsconfig ./packages/tsconfig
|
||||||
|
|
||||||
|
# Install production dependencies only
|
||||||
|
RUN pnpm i --prod --filter=dashboard
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV IS_SELFHOSTED=true
|
||||||
|
ENV DEPLOYMENT_PROVIDER=docker
|
||||||
|
ENV PUBLIC_SUPABASE_URL=$PUBLIC_SUPABASE_URL
|
||||||
|
ENV PUBLIC_SUPABASE_ANON_KEY=$PUBLIC_SUPABASE_ANON_KEY
|
||||||
|
ENV PRIVATE_SUPABASE_SERVICE_ROLE=$PRIVATE_SUPABASE_SERVICE_ROLE
|
||||||
|
ENV PRIVATE_APP_HOST=$PRIVATE_APP_HOST
|
||||||
|
ENV PRIVATE_APP_SUBDOMAINS=$PRIVATE_APP_SUBDOMAINS
|
||||||
|
ENV UNSPLASH_API_KEY=$UNSPLASH_API_KEY
|
||||||
|
ENV OPENAI_API_KEY=$OPENAI_API_KEY
|
||||||
|
ENV PUBLIC_SERVER_URL=$PUBLIC_SERVER_URL
|
||||||
|
ENV PUBLIC_IP_REGISTRY_KEY=$PUBLIC_IP_REGISTRY_KEY
|
||||||
|
ENV NODE_OPTIONS="--max-old-space-size=40960"
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Set working directory for the application
|
||||||
|
WORKDIR /app/apps/dashboard
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["node", "build"]
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"@tailwindcss/forms": "^0.5.4",
|
"@tailwindcss/forms": "^0.5.4",
|
||||||
"@types/pluralize": "0.0.30",
|
"@types/pluralize": "0.0.30",
|
||||||
"ai": "^2.1.31",
|
"ai": "^2.1.31",
|
||||||
"axios": "^1.5.1",
|
"axios": "^1.8.4",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"canvas-confetti": "^1.6.0",
|
"canvas-confetti": "^1.6.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|||||||
@@ -7,13 +7,22 @@
|
|||||||
export let isPageNavHidden = false;
|
export let isPageNavHidden = false;
|
||||||
|
|
||||||
$: padding = isPageNavHidden ? `pb-20 px-4` : padding;
|
$: padding = isPageNavHidden ? `pb-20 px-4` : padding;
|
||||||
|
|
||||||
|
function handleKeydown(event) {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
class="overflow-y-auto {isPageNavHidden
|
class="overflow-y-auto {isPageNavHidden
|
||||||
? 'h-[calc(100vh-65px)] lg:h-[calc(100vh-127px)]'
|
? 'h-[calc(100vh-65px)] lg:h-[calc(100vh-127px)]'
|
||||||
: 'h-[calc(100vh-127px)]'} mx-auto mt-4 {width} {className} relative {padding}"
|
: 'h-[calc(100vh-127px)]'} mx-auto mt-4 {width} {className} relative {padding}"
|
||||||
on:click={onClick}
|
on:click={onClick}
|
||||||
|
on:keydown={handleKeydown}
|
||||||
>
|
>
|
||||||
{#if $$slots.header}
|
{#if $$slots.header}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
|
||||||
import { config } from '$lib/config';
|
import { config } from '$lib/config';
|
||||||
|
import { createClient, type SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
export let supabase: SupabaseClient;
|
export let supabase: SupabaseClient;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { config } from '$lib/config';
|
import { config } from '$lib/config';
|
||||||
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
import { createClient, type SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
export let supabase: SupabaseClient;
|
export let supabase: SupabaseClient;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// Code from https://github.com/vercel/platforms/blob/1ed55b13bcae97b037c69ec40b4c32df21c2412c/lib/domains.ts
|
// Code from https://github.com/vercel/platforms/blob/1ed55b13bcae97b037c69ec40b4c32df21c2412c/lib/domains.ts
|
||||||
|
|
||||||
import {
|
import { env } from '$env/dynamic/private';
|
||||||
DomainResponse,
|
import type {
|
||||||
DomainConfigResponse,
|
DomainConfigResponse,
|
||||||
|
DomainResponse,
|
||||||
DomainVerificationResponse
|
DomainVerificationResponse
|
||||||
} from '$lib/utils/types/org';
|
} from '$lib/utils/types/org';
|
||||||
import { env } from '$env/dynamic/private';
|
|
||||||
|
|
||||||
export const addDomainToVercel = async (domain: string) => {
|
export const addDomainToVercel = async (domain: string) => {
|
||||||
return await fetch(
|
return await fetch(
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { writable, derived } from 'svelte/store';
|
import { browser, dev } from '$app/environment';
|
||||||
import { dev, browser } from '$app/environment';
|
|
||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
import { STEPS } from '../constants/quiz';
|
|
||||||
import type { Writable } from 'svelte/store';
|
|
||||||
import type { CurrentOrg, OrgTeamMember, OrgAudience } from '../types/org';
|
|
||||||
import { ROLE } from '$lib/utils/constants/roles';
|
import { ROLE } from '$lib/utils/constants/roles';
|
||||||
import type { UserLessonDataType } from '$lib/utils/types';
|
import type { UserLessonDataType } from '$lib/utils/types';
|
||||||
import { PLAN } from 'shared/src/plans/constants';
|
import { PLAN } from 'shared/src/plans/constants';
|
||||||
|
import type { Writable } from 'svelte/store';
|
||||||
|
import { derived, writable } from 'svelte/store';
|
||||||
|
import { STEPS } from '../constants/quiz';
|
||||||
|
import type { CurrentOrg, OrgAudience, OrgTeamMember } from '../types/org';
|
||||||
|
|
||||||
export const defaultCurrentOrgState: CurrentOrg = {
|
export const defaultCurrentOrgState: CurrentOrg = {
|
||||||
id: '',
|
id: '',
|
||||||
@@ -86,9 +86,31 @@ export const deleteModal = writable({
|
|||||||
isQuestion: false
|
isQuestion: false
|
||||||
});
|
});
|
||||||
|
|
||||||
export const quizesStore = writable([]);
|
interface QuizOption {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
isCorrect: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const quizStore = writable({
|
interface QuizQuestion {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
type: string;
|
||||||
|
options: QuizOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QuizStore {
|
||||||
|
uuid: string;
|
||||||
|
title: string;
|
||||||
|
questions: QuizQuestion[];
|
||||||
|
timelimit: string;
|
||||||
|
theme: string;
|
||||||
|
pin: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const quizesStore = writable<QuizStore[]>([]);
|
||||||
|
|
||||||
|
export const quizStore = writable<QuizStore>({
|
||||||
uuid: '',
|
uuid: '',
|
||||||
title: '',
|
title: '',
|
||||||
questions: [],
|
questions: [],
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import { dev } from '$app/environment';
|
import { dev } from '$app/environment';
|
||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
import { IS_SELFHOSTED } from '$env/static/private';
|
import { IS_SELFHOSTED } from '$env/static/private';
|
||||||
@@ -12,6 +13,8 @@ if (!supabase) {
|
|||||||
getSupabase();
|
getSupabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ssr = IS_SELFHOSTED === 'true' ? false : true;
|
||||||
|
|
||||||
interface LoadOutput {
|
interface LoadOutput {
|
||||||
orgSiteName: string;
|
orgSiteName: string;
|
||||||
isOrgSite: boolean;
|
isOrgSite: boolean;
|
||||||
@@ -21,7 +24,7 @@ interface LoadOutput {
|
|||||||
serverLang: string;
|
serverLang: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const APP_SUBDOMAINS = env.PRIVATE_APP_SUBDOMAINS.split(',');
|
const APP_SUBDOMAINS = env.PRIVATE_APP_SUBDOMAINS?.split(',');
|
||||||
|
|
||||||
export const load = async ({ url, cookies, request }): Promise<LoadOutput> => {
|
export const load = async ({ url, cookies, request }): Promise<LoadOutput> => {
|
||||||
const response: LoadOutput = {
|
const response: LoadOutput = {
|
||||||
@@ -106,7 +109,7 @@ export const load = async ({ url, cookies, request }): Promise<LoadOutput> => {
|
|||||||
if (!response.org && !isDev) {
|
if (!response.org && !isDev) {
|
||||||
throw redirect(307, 'https://app.classroomio.com/404?type=org');
|
throw redirect(307, 'https://app.classroomio.com/404?type=org');
|
||||||
} else if (!response.org && _orgSiteName) {
|
} else if (!response.org && _orgSiteName) {
|
||||||
cookies.delete('_orgSiteName');
|
cookies.delete('_orgSiteName', { path: '/' });
|
||||||
}
|
}
|
||||||
} else if (subdomain === 'play' || debugPlay === 'true') {
|
} else if (subdomain === 'play' || debugPlay === 'true') {
|
||||||
response.skipAuth = true;
|
response.skipAuth = true;
|
||||||
@@ -170,10 +173,10 @@ function getBaseMetaTags(url: URL) {
|
|||||||
|
|
||||||
function getSubdomain(url: URL) {
|
function getSubdomain(url: URL) {
|
||||||
const host = url.host.replace('www.', '');
|
const host = url.host.replace('www.', '');
|
||||||
const parts = host.split('.');
|
const parts = host?.split('.');
|
||||||
|
|
||||||
if (host.endsWith(env.PRIVATE_APP_HOST)) {
|
if (host?.endsWith(env.PRIVATE_APP_HOST)) {
|
||||||
return parts.length >= 3 ? parts[0] : null;
|
return parts?.length >= 3 ? parts[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getServerSupabase } from '$lib/utils/functions/supabase.server';
|
import { getServerSupabase } from '$lib/utils/functions/supabase.server';
|
||||||
import { OrganisationAnalytics } from '$lib/utils/types/analytics';
|
import type { OrganisationAnalytics } from '$lib/utils/types/analytics';
|
||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
|
|
||||||
const supabase = getServerSupabase();
|
const supabase = getServerSupabase();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getServerSupabase } from '$lib/utils/functions/supabase.server';
|
import { getServerSupabase } from '$lib/utils/functions/supabase.server';
|
||||||
import { fetchCourses, fetchProfileCourseProgress } from '$lib/utils/services/courses';
|
import { fetchCourses, fetchProfileCourseProgress } from '$lib/utils/services/courses';
|
||||||
import {
|
import type {
|
||||||
UserAnalytics,
|
UserAnalytics,
|
||||||
UserCourseAnalytics,
|
UserCourseAnalytics,
|
||||||
UserExerciseStatsQuery,
|
UserExerciseStatsQuery,
|
||||||
|
|||||||
@@ -160,7 +160,7 @@
|
|||||||
transition:fade={{ duration: 300 }}
|
transition:fade={{ duration: 300 }}
|
||||||
>
|
>
|
||||||
<div class="flex w-2/3 items-center gap-4">
|
<div class="flex w-2/3 items-center gap-4">
|
||||||
<Notebook size={24} class="text-black" />
|
<svelte:component this={Notebook} size={24} class="text-black" />
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -1,40 +1,65 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import cloneDeep from 'lodash/cloneDeep';
|
|
||||||
import isBoolean from 'lodash/isBoolean';
|
|
||||||
import ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte';
|
import ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte';
|
||||||
import CheckmarkFilledIcon from 'carbon-icons-svelte/lib/CheckmarkFilled.svelte';
|
import CheckmarkFilledIcon from 'carbon-icons-svelte/lib/CheckmarkFilled.svelte';
|
||||||
import WarningFilledIcon from 'carbon-icons-svelte/lib/WarningFilled.svelte';
|
import WarningFilledIcon from 'carbon-icons-svelte/lib/WarningFilled.svelte';
|
||||||
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
|
import isBoolean from 'lodash/isBoolean';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
import { Select, SelectItem } from 'carbon-components-svelte';
|
|
||||||
import { currentOrgPath, deleteModal } from '$lib/utils/store/org';
|
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
|
||||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
|
||||||
import { themeImages, allOptions, booleanOptions, allThemes } from '$lib/utils/constants/quiz';
|
|
||||||
import { quizStore, quizesStore } from '$lib/utils/store/org';
|
|
||||||
import DeleteModal from '$lib/components/Org/Quiz/DeleteModal.svelte';
|
import DeleteModal from '$lib/components/Org/Quiz/DeleteModal.svelte';
|
||||||
import Preview from '$lib/components/Org/Quiz/Play/Preview.svelte';
|
import Preview from '$lib/components/Org/Quiz/Play/Preview.svelte';
|
||||||
import QuizQuestion from '$lib/components/Org/Quiz/QuizQuestion.svelte';
|
import QuizQuestion from '$lib/components/Org/Quiz/QuizQuestion.svelte';
|
||||||
|
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||||
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import { snackbar } from '$lib/components/Snackbar/store';
|
import { snackbar } from '$lib/components/Snackbar/store';
|
||||||
|
import { allOptions, allThemes, booleanOptions, themeImages } from '$lib/utils/constants/quiz';
|
||||||
import { supabase } from '$lib/utils/functions/supabase';
|
import { supabase } from '$lib/utils/functions/supabase';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
|
import { currentOrgPath, deleteModal, quizStore, quizesStore } from '$lib/utils/store/org';
|
||||||
|
import { Select, SelectItem } from 'carbon-components-svelte';
|
||||||
|
|
||||||
|
interface QuizOption {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
isCorrect: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QuizQuestion {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
type: string;
|
||||||
|
options: QuizOption[];
|
||||||
|
}
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
const { quizId } = data;
|
const { quizId } = data;
|
||||||
|
|
||||||
// Questionnaire State
|
// Questionnaire State
|
||||||
let currentQuestion = $quizStore.questions[0] || {
|
let currentQuestion: QuizQuestion = $quizStore.questions[0] || {
|
||||||
|
id: 0,
|
||||||
|
label: '',
|
||||||
|
type: 'multichoice',
|
||||||
options: []
|
options: []
|
||||||
};
|
};
|
||||||
|
|
||||||
// Behavioural State
|
// Behavioural State
|
||||||
let openPreview = false;
|
let openPreview = false;
|
||||||
let type = 'multichoice';
|
let type = 'multichoice';
|
||||||
let errors = [];
|
let errors: Array<{
|
||||||
let currentError = {};
|
isLabelEmpty: boolean;
|
||||||
|
hasOneAnswer: boolean;
|
||||||
|
id: number;
|
||||||
|
options: Array<{ id: number; error: boolean }>;
|
||||||
|
}> = [];
|
||||||
|
let currentError: {
|
||||||
|
isLabelEmpty?: boolean;
|
||||||
|
hasOneAnswer?: boolean;
|
||||||
|
id?: number;
|
||||||
|
options?: Array<{ id: number; error: boolean }>;
|
||||||
|
} = {};
|
||||||
let isFocused = false;
|
let isFocused = false;
|
||||||
let selectEl = null;
|
let selectEl: Select | null = null;
|
||||||
|
|
||||||
function activeClass(q, cq) {
|
function activeClass(q, cq) {
|
||||||
if (q.id === cq.id) {
|
if (q.id === cq.id) {
|
||||||
@@ -44,60 +69,80 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addQuestion() {
|
function addQuestion() {
|
||||||
$quizStore.questions = [
|
const newQuestion: QuizQuestion = {
|
||||||
...$quizStore.questions,
|
id: new Date().getTime(),
|
||||||
{
|
label: '',
|
||||||
id: new Date().getTime(),
|
type: 'multichoice',
|
||||||
label: '',
|
options: []
|
||||||
type: 'multichoice',
|
};
|
||||||
options: []
|
$quizStore.questions = [...$quizStore.questions, newQuestion];
|
||||||
}
|
|
||||||
];
|
|
||||||
type = 'multichoice';
|
type = 'multichoice';
|
||||||
}
|
}
|
||||||
|
|
||||||
function addOption() {
|
function addOption() {
|
||||||
const cOptIds = currentQuestion.options.map((o) => o.id);
|
const cOptIds = currentQuestion.options.map((o) => o.id);
|
||||||
const nextOption = cloneDeep(allOptions).find((o) => !cOptIds.includes(o.id));
|
const nextOption = cloneDeep(allOptions).find((o) => !cOptIds.includes(Number(o.id)));
|
||||||
|
|
||||||
if (!nextOption) return;
|
if (!nextOption) return;
|
||||||
|
|
||||||
currentQuestion.options = [...currentQuestion.options, nextOption];
|
currentQuestion.options = [
|
||||||
|
...currentQuestion.options,
|
||||||
|
{
|
||||||
|
id: Number(nextOption.id),
|
||||||
|
label: nextOption.label,
|
||||||
|
isCorrect: nextOption.isCorrect
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteOption() {
|
function deleteOption() {
|
||||||
const opt = cloneDeep(currentQuestion.options);
|
const opt = cloneDeep(currentQuestion.options);
|
||||||
opt.pop();
|
opt.pop();
|
||||||
currentQuestion.options = opt;
|
currentQuestion.options = opt;
|
||||||
}
|
}
|
||||||
function handleQuestionTypeChange(type) {
|
|
||||||
|
function handleQuestionTypeChange(type: string) {
|
||||||
const opt =
|
const opt =
|
||||||
type === 'multichoice'
|
type === 'multichoice'
|
||||||
? cloneDeep(allOptions).filter((o, i) => i < 2)
|
? cloneDeep(allOptions)
|
||||||
: cloneDeep(booleanOptions);
|
.filter((o, i) => i < 2)
|
||||||
|
.map((o) => ({
|
||||||
|
id: Number(o.id),
|
||||||
|
label: o.label,
|
||||||
|
isCorrect: o.isCorrect
|
||||||
|
}))
|
||||||
|
: cloneDeep(booleanOptions).map((o) => ({
|
||||||
|
id: Number(o.id),
|
||||||
|
label: o.label,
|
||||||
|
isCorrect: o.isCorrect
|
||||||
|
}));
|
||||||
|
|
||||||
currentQuestion.type = type;
|
currentQuestion.type = type;
|
||||||
currentQuestion.options = opt;
|
currentQuestion.options = opt;
|
||||||
// $quizStore.questions = $quizStore.questions.map((q) =>
|
|
||||||
// q.id === currentQuestion.id ? cloneDeep(currentQuestion) : q
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteQuestion() {
|
function deleteQuestion() {
|
||||||
// Only leave one question
|
// Only leave one question
|
||||||
if ($quizStore.questions.length === 1) return;
|
if ($quizStore.questions.length === 1) return;
|
||||||
$quizStore.questions = $quizStore.questions.filter((q) => q.id !== currentQuestion.id);
|
$quizStore.questions = $quizStore.questions.filter((q) => q.id !== currentQuestion.id);
|
||||||
|
|
||||||
currentQuestion = $quizStore.questions[0];
|
currentQuestion = $quizStore.questions[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
function previewQuiz() {
|
function previewQuiz() {
|
||||||
openPreview = !openPreview;
|
openPreview = !openPreview;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateQuiz() {
|
function validateQuiz() {
|
||||||
const _errors = [];
|
const _errors: Array<{
|
||||||
|
isLabelEmpty: boolean;
|
||||||
|
hasOneAnswer: boolean;
|
||||||
|
id: number;
|
||||||
|
options: Array<{ id: number; error: boolean }>;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
$quizStore.questions.forEach((q) => {
|
$quizStore.questions.forEach((q) => {
|
||||||
const labelError = !!((q.label?.length || 0) < 3);
|
const labelError = !!((q.label?.length || 0) < 3);
|
||||||
const options = [];
|
const options: Array<{ id: number; error: boolean }> = [];
|
||||||
let hasOneAnswer = false;
|
let hasOneAnswer = false;
|
||||||
|
|
||||||
q.options.forEach((o) => {
|
q.options.forEach((o) => {
|
||||||
@@ -124,11 +169,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
errors = _errors;
|
errors = _errors;
|
||||||
|
|
||||||
currentError = errors.find((e) => e.id === currentQuestion.id) || {};
|
currentError = errors.find((e) => e.id === currentQuestion.id) || {};
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveQuiz() {
|
async function saveQuiz() {
|
||||||
const _errors = validateQuiz();
|
const _errors = validateQuiz();
|
||||||
if (Array.isArray(_errors) && _errors.length && qHasError(null, _errors)) {
|
if (Array.isArray(_errors) && _errors.length && qHasError(null, _errors)) {
|
||||||
@@ -161,6 +205,7 @@
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function optionHasError(eId, _errs) {
|
function optionHasError(eId, _errs) {
|
||||||
if (Array.isArray(_errs) && _errs.length) {
|
if (Array.isArray(_errs) && _errs.length) {
|
||||||
return _errs.some((e) => e.id === eId && e.error);
|
return _errs.some((e) => e.id === eId && e.error);
|
||||||
@@ -190,12 +235,12 @@
|
|||||||
<Preview exitPreview={previewQuiz} />
|
<Preview exitPreview={previewQuiz} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<section class="w-screen h-full flex justify-between">
|
<section class="flex h-full w-screen justify-between">
|
||||||
<!-- Questions list -->
|
<!-- Questions list -->
|
||||||
<aside class="root w-1/5 p-4 bg-gray-100 dark:bg-neutral-800 h-full">
|
<aside class="root h-full w-1/5 bg-gray-100 p-4 dark:bg-neutral-800">
|
||||||
<div class="h-full flex flex-col">
|
<div class="flex h-full flex-col">
|
||||||
<a
|
<a
|
||||||
class="text-gray-500 dark:text-white text-md flex items-center"
|
class="text-md flex items-center text-gray-500 dark:text-white"
|
||||||
href={`${$currentOrgPath}/quiz`}
|
href={`${$currentOrgPath}/quiz`}
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" /> Back to Quizzes
|
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" /> Back to Quizzes
|
||||||
@@ -206,7 +251,7 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{#each $quizStore.questions as question, i}
|
{#each $quizStore.questions as question, i}
|
||||||
<button
|
<button
|
||||||
class="w-full rounded p-3 mb-3 font-bold text-left text-gray-500 dark:text-white flex justify-between {activeClass(
|
class="mb-3 flex w-full justify-between rounded p-3 text-left font-bold text-gray-500 dark:text-white {activeClass(
|
||||||
question,
|
question,
|
||||||
currentQuestion
|
currentQuestion
|
||||||
)}"
|
)}"
|
||||||
@@ -214,6 +259,12 @@
|
|||||||
currentQuestion = question;
|
currentQuestion = question;
|
||||||
type = question.type;
|
type = question.type;
|
||||||
}}
|
}}
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
currentQuestion = question;
|
||||||
|
type = question.type;
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Question {i + 1}
|
Question {i + 1}
|
||||||
|
|
||||||
@@ -224,20 +275,20 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex justify-end">
|
<div class="flex w-full justify-end">
|
||||||
<PrimaryButton label="Add Question" variant={VARIANTS.CONTAINED} onClick={addQuestion} />
|
<PrimaryButton label="Add Question" variant={VARIANTS.CONTAINED} onClick={addQuestion} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div class="container w-3/6 h-full">
|
<div class="container h-full w-3/6">
|
||||||
<div
|
<div
|
||||||
class="p-5 h-full"
|
class="h-full p-5"
|
||||||
style="background: url({themeImages[$quizStore.theme]
|
style="background: url({themeImages[$quizStore.theme]
|
||||||
?.editor}) no-repeat center center fixed; -webkit-background-size: cover;-moz-background-size: cover;-o-background-size: cover;background-size: cover;"
|
?.editor}) no-repeat center center fixed; -webkit-background-size: cover;-moz-background-size: cover;-o-background-size: cover;background-size: cover;"
|
||||||
>
|
>
|
||||||
<div class="content m-auto">
|
<div class="content m-auto">
|
||||||
<h1 class="text-white font-bold my-5">{$quizStore.title}</h1>
|
<h1 class="my-5 font-bold text-white">{$quizStore.title}</h1>
|
||||||
|
|
||||||
<QuizQuestion {currentQuestion} {optionHasError} {currentError} />
|
<QuizQuestion {currentQuestion} {optionHasError} {currentError} />
|
||||||
|
|
||||||
@@ -248,7 +299,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if currentQuestion.type !== 'boolean'}
|
{#if currentQuestion.type !== 'boolean'}
|
||||||
<div class="w-full flex justify-center mb-4">
|
<div class="mb-4 flex w-full justify-center">
|
||||||
{#if currentQuestion.options.length < allOptions.length}
|
{#if currentQuestion.options.length < allOptions.length}
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
label={$t('components.quiz.add_more')}
|
label={$t('components.quiz.add_more')}
|
||||||
@@ -271,7 +322,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quiz Settings -->
|
<!-- Quiz Settings -->
|
||||||
<aside class="settings w-1/5 p-4 bg-gray-100 dark:bg-neutral-800 h-full">
|
<aside class="settings h-full w-1/5 bg-gray-100 p-4 dark:bg-neutral-800">
|
||||||
<div class="py-5">
|
<div class="py-5">
|
||||||
<h5>Quiz settings</h5>
|
<h5>Quiz settings</h5>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
@@ -305,7 +356,7 @@
|
|||||||
labelText="Question type"
|
labelText="Question type"
|
||||||
bind:this={selectEl}
|
bind:this={selectEl}
|
||||||
bind:selected={type}
|
bind:selected={type}
|
||||||
class="flex items-center mb-3"
|
class="mb-3 flex items-center"
|
||||||
on:focus={() => (isFocused = true)}
|
on:focus={() => (isFocused = true)}
|
||||||
on:blur={() => (isFocused = false)}
|
on:blur={() => (isFocused = false)}
|
||||||
on:change={() => {
|
on:change={() => {
|
||||||
@@ -329,7 +380,7 @@
|
|||||||
<Select
|
<Select
|
||||||
labelText="Time limit"
|
labelText="Time limit"
|
||||||
bind:selected={$quizStore.timelimit}
|
bind:selected={$quizStore.timelimit}
|
||||||
class="flex items-center mb-3"
|
class="mb-3 flex items-center"
|
||||||
>
|
>
|
||||||
<SelectItem value="10 seconds" text="10s" />
|
<SelectItem value="10 seconds" text="10s" />
|
||||||
<SelectItem value="20 seconds" text="20s" />
|
<SelectItem value="20 seconds" text="20s" />
|
||||||
@@ -346,22 +397,29 @@
|
|||||||
|
|
||||||
{#each allThemes as _theme}
|
{#each allThemes as _theme}
|
||||||
<div
|
<div
|
||||||
class="theme rounded-md w-full border cursor-pointer mb-5 relative {$quizStore.theme ===
|
class="theme relative mb-5 w-full cursor-pointer rounded-md border {$quizStore.theme ===
|
||||||
_theme.id && 'border-primary-700'}"
|
_theme.id && 'border-primary-700'}"
|
||||||
>
|
>
|
||||||
{#if $quizStore.theme === _theme.id}
|
{#if $quizStore.theme === _theme.id}
|
||||||
<CheckmarkFilledIcon
|
<CheckmarkFilledIcon
|
||||||
size={24}
|
size={24}
|
||||||
class="carbon-icon absolute top-4 right-4"
|
class="carbon-icon absolute right-4 top-4"
|
||||||
style="fill:white;"
|
style="fill:white;"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<div
|
||||||
class="w-full rounded-md h-full border flex flex-col-reverse"
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
class="flex h-full w-full flex-col-reverse rounded-md border"
|
||||||
style="background: url({themeImages[_theme.id]?.card});"
|
style="background: url({themeImages[_theme.id]?.card});"
|
||||||
on:click={() => ($quizStore.theme = _theme.id)}
|
on:click={() => ($quizStore.theme = _theme.id)}
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
$quizStore.theme = _theme.id;
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<p class="ml-3 mb-3 text-white">{_theme.label}</p>
|
<p class="mb-3 ml-3 text-white">{_theme.label}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { sentryVitePlugin } from '@sentry/vite-plugin';
|
|
||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
import { defineConfig, loadEnv } from 'vite';
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
|
|
||||||
export default ({ mode }) => {
|
export default ({ mode }) => {
|
||||||
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
|
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
|
||||||
@@ -14,18 +13,22 @@ export default ({ mode }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [sveltekit(), sentryVitePlugin(getSentryConfig(process.env))],
|
plugins: [sveltekit()],
|
||||||
server: getServer(process.env),
|
server: getServer(process.env),
|
||||||
build: {
|
build: {
|
||||||
sourcemap: true
|
sourcemap: true
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
entries: ['src/routes/**/+*.{js,ts,svelte}']
|
entries: ['src/routes/**/+*.{js,ts,svelte}']
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
mainFields: ['browser']
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function getServer({ VITE_USE_HTTPS_ON_LOCALHOST }) {
|
function getServer(params: any) {
|
||||||
|
const { VITE_USE_HTTPS_ON_LOCALHOST } = params || {};
|
||||||
if (VITE_USE_HTTPS_ON_LOCALHOST === 'true') {
|
if (VITE_USE_HTTPS_ON_LOCALHOST === 'true') {
|
||||||
return {
|
return {
|
||||||
https: {
|
https: {
|
||||||
@@ -36,18 +39,18 @@ function getServer({ VITE_USE_HTTPS_ON_LOCALHOST }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSentryConfig({
|
// function getSentryConfig(params: any) {
|
||||||
VITE_SENTRY_AUTH_TOKEN,
|
// const { VITE_SENTRY_AUTH_TOKEN, VITE_SENTRY_ORG_NAME, VITE_SENTRY_PROJECT_NAME } = params || {};
|
||||||
VITE_SENTRY_ORG_NAME,
|
// if (VITE_SENTRY_AUTH_TOKEN) {
|
||||||
VITE_SENTRY_PROJECT_NAME
|
// return {
|
||||||
}) {
|
// url: 'https://sentry.io',
|
||||||
if (VITE_SENTRY_AUTH_TOKEN) {
|
// authToken: VITE_SENTRY_AUTH_TOKEN,
|
||||||
return {
|
// org: VITE_SENTRY_ORG_NAME,
|
||||||
url: 'https://sentry.io',
|
// project: VITE_SENTRY_PROJECT_NAME,
|
||||||
authToken: VITE_SENTRY_AUTH_TOKEN,
|
// options: {
|
||||||
org: VITE_SENTRY_ORG_NAME,
|
// telemetry: false,
|
||||||
project: VITE_SENTRY_PROJECT_NAME
|
// }
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
return {};
|
// return {};
|
||||||
}
|
// }
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
"format": "prettier . --write",
|
"format": "prettier . --write",
|
||||||
"release": "standard-version",
|
"release": "standard-version",
|
||||||
"ci": "cypress run",
|
"ci": "cypress run",
|
||||||
"dashboard:build": "cd apps/dashboard && pnpm i && IS_SELFHOSTED=true DEPLOYMENT_PROVIDER=docker pnpm build",
|
"dashboard:build": "cd apps/dashboard && pnpm i && IS_SELFHOSTED=true NODE_OPTIONS='--max-old-space-size=20384' DEPLOYMENT_PROVIDER=docker pnpm build",
|
||||||
"dashboard:start": "cd apps/dashboard && node build"
|
"dashboard:start": "cd apps/dashboard && node build"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
function handleClick() {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button on:click={handleClick}>
|
|
||||||
clicks: {count}
|
|
||||||
</button>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "svelte-ui",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"description": "Styles and components for use in svelte websites",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"lint": "prettier --check --ignore-path=../../.prettierignore .",
|
|
||||||
"format": "prettier --write --ignore-path=../../.prettierignore ."
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"eslint-config-custom": "workspace:*",
|
|
||||||
"svelte": "^3.9.2"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "MIT"
|
|
||||||
}
|
|
||||||
Generated
+11397
-15004
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user