docker improvements + delete unused package

This commit is contained in:
Rotimi Best
2025-05-03 14:46:01 +01:00
parent 50e89f90c9
commit f343ca7974
18 changed files with 11683 additions and 15126 deletions
+97
View File
@@ -0,0 +1,97 @@
# Build stage
FROM node:18-alpine AS builder
# Install pnpm
RUN npm install -g pnpm@9.15.4 && \
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 "noreply@getcrom.com" && \
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 pnpm@9.15.4
# 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"]
+1 -1
View File
@@ -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(
+29 -7
View File
@@ -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: [],
+8 -5
View File
@@ -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}
+22 -19
View File
@@ -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
View File
@@ -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>
View File
-17
View File
@@ -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"
}
+11397 -15004
View File
File diff suppressed because it is too large Load Diff