feat: [lw] free tools (#297)

* checkpoint

* checkpoint

* feat: Create free tools page

* responsiveness fix

* rename free-tools to tools

* add slug

* feat: Random name picker

* quick fix

* PR Fixes

* PR Fixes

* - adjustments

* update env example

* remove base url

* remove base url in .env.example

* fix type errors

* add og images

* add signup cta to /tools layout

* update cta

* add socials avatar

* add meta tags

* feat: tic tac toe (#307)

* Feat: tic tac toe

* PR Adjustments

* feat: Implement Timed Activity Tool (#316)

* chore: update verification email content

* chore: landing page improvements

* feat: grade exercise with AI (#296)

* working on the UI of the grading

* working on the grading function of the radio and checkboxes

* ai grading fully implemented with dummy functions for generating scores

* working on the openai response

* working on the openai response

* finished with the ai_grading

* finished with the ai_grading

* creating new migration file

* working on reviews

* updated the translations

* improvements

---------

Co-authored-by: Rotimi Best <best@classroomio.com>

* feat: selfhost tinymce

* fix: logo resolution error in manifest.json

* feat: move from gpt3.5 to gpt-4o

* feat: customize the LMS from dashboard (#304)

* Done with the customization of the LMS task

* generated custom_lms migration file

* updated the migration file

* copy improvements

* update wording

* made updates based on reviews,i hope i didnt miss anything

* updated the translations and migration files

* refactoring

* disabled improvements for apps

* more improvements

* more custom improvements

* pr refactoring

---------

Co-authored-by: Rotimi Best <best@classroomio.com>

* fix: CustomizeLMS not found

* fix: CustomizeLms wrong path

* setup

* docs: update short hero tagline

* debug: courses not showing on vercel

* more debugging

* i think this fixes it

* fix: phew, clean up debug cause it works

* fix: move RBS into coursecontainer on marks page

* chore: cleaner way to implement redirect if role permission denied

* docs: update tagline

* fix: delete /org/# route

* checkpoint

* add reusable header component

* fix: empty lesson title bug (#314)

* finished the lesson title bug

* made chnages based on review

* Update +page.svelte

---------

Co-authored-by: Rotimi Best <best@classroomio.com>

* feat: add sound cdn

* remove console.log

* feat: course types - self paced and live class (#309)

* working on the course type

* trying to mark it persist

* almost done here

* fixed sendig emtpy options bug

* created migration file

* created migration file

* made updates based on PR review

* merge

* update course type card

* add redirect logic with classroomio.com/new

* make new course modal only open/close with url

* feat: page active state (#315)

* finished the active link state

* updated the active link state based on review

* simplify code

---------

Co-authored-by: Rotimi Best <best@classroomio.com>

* add type to course card

---------

Co-authored-by: Rotimi Best <best@classroomio.com>

* general improvements to free tools

* fix timer and styling issues

---------

Co-authored-by: Rotimi Best <best@classroomio.com>
Co-authored-by: Em_Dev <Chifez1@gmail.com>

* update tagline for tools page

* feat: Add Free Section to Landing Page (#332)

* feat: Add Free Section to Landing Page

* PR Fixes

* refactor

* PR Fixes

* restructure

* improve cursor UX

---------

Co-authored-by: Rotimi Best <best@classroomio.com>

* feat: Progress Report Tool (#329)

* checkpoint

* fix: corrections

* finalization

* improve ux of progress tool

* more styling improvements

* PR Fixes

* PR Fixes

* fix: mood bg on report

* fix: generate exercise with AI bug (#331)

* fix: enable lesson cascade delete (#335)

* styling improvements

* chore: hide course grading toggle in settings (#338)

* feat: reset select in community page (#339)

* Feat: Add Reset for comminity dropdown

* - add translation

* refactor

---------

Co-authored-by: Rotimi Best <best@classroomio.com>

* fix: prevent hypen at start or end of organzation or domain name (#340)

* fix: adding hypen to start or end of org or domain name

* fix: add org name validation to settings page

* chore: remove unnecessary variable use

* chore: remove error reset function

* Update OrgSettings.svelte

---------

Co-authored-by: Rotimi Best <best@classroomio.com>

* fix: full screen

* fix: pnpm version error & add top level .nvmrc (#343)

* update name of tool

* chore: add translations for validation messages (#345)

* chore: add validation texts to locale file

* feat: switch all validations from plain text to use i18n locale

* feat: switch review form validations from plain text to use locale file

* run translation script

* chore: add translations for validation messages (#345) (#347)

* chore: add translations for validation messages (#345)

* chore: add validation texts to locale file

* feat: switch all validations from plain text to use i18n locale

* feat: switch review form validations from plain text to use locale file

* run translation script

---------

Co-authored-by: Iniubong Obonguko <iniubongobonguko2018@gmail.com>

* PR Fixes

* Bug fixes on mobile

* Final bug fix

* code improvements

* PR Fixes

* Merge Fix

* PR Fixes

* fix: width

* code cleanup

* feat: Promodo Tool (tracks tim spent)

* feat: Pomodoro timer (without tracking feature)

* fix:
- change pause functionality to next
- correct spelling of pomodoro

* fix: pomodoro logo rename

* styling improvements

---------

Co-authored-by: Rotimi Best <best@classroomio.com>
Co-authored-by: Em_Dev <Chifez1@gmail.com>
Co-authored-by: Iniubong Obonguko <iniubongobonguko2018+github@gmail.com>
Co-authored-by: Iniubong Obonguko <iniubongobonguko2018@gmail.com>
Co-authored-by: Aditya Deshlahre <132184385+adityadeshlahre@users.noreply.github.com>

* feat: Implement Bottom menu (#341)

* popup logic done

* popup UI first phase done

* finished the menupop up for both lms and admin

* finished the menupop up for both lms and admin

* done

* cleanup

* fixed popover position

* still working on the routing

* i think that is it for the day

* updated the menupopup currentorgdomain

* rename popup to bottomMenu

* refactoring

---------

Co-authored-by: Rotimi Best <best@classroomio.com>

* remove logout

* change tagline

* feat: Store todolist to localStorage (#375)

* feat: Save todo to localStorage

* remove fly animation to todos

* feat:  Progress Report avatar update (#376)

* feat: Reorder Avatar list

* quick fix

* change avatar url

* more improvements

* quick background fix

* fix: white background

* body background fix

* update lock

---------

Co-authored-by: Rotimi Best <best@classroomio.com>
Co-authored-by: Em_Dev <Chifez1@gmail.com>
Co-authored-by: Iniubong Obonguko <iniubongobonguko2018+github@gmail.com>
Co-authored-by: Iniubong Obonguko <iniubongobonguko2018@gmail.com>
Co-authored-by: Aditya Deshlahre <132184385+adityadeshlahre@users.noreply.github.com>
This commit is contained in:
Adeyemi Adetunji
2024-09-30 01:36:12 +01:00
committed by GitHub
co-authored by Rotimi Best Em_Dev Iniubong Obonguko Iniubong Obonguko Aditya Deshlahre
parent 91bd9d4414
commit 9c066c79be
106 changed files with 3778 additions and 478 deletions
@@ -0,0 +1,88 @@
<script lang="ts">
import { openModal, htmlBody } from './store';
import Modal from '$lib/Modal/Modal.svelte';
interface Avatar {
src: string;
}
let isAddIcon = false;
let hoveredIndex: number | null = null;
let avatars: Avatar[] = [
{ src: 'avatar_a' },
{ src: 'avatar_b' },
{ src: 'avatar_c' },
{ src: 'avatar_d' },
{ src: 'avatar_e' },
{ src: 'avatar_f' },
{ src: 'avatar_g' },
{ src: 'avatar_h' },
{ src: 'avatar_i' },
{ src: 'avatar_j' },
{ src: 'avatar_k' },
{ src: 'avatar_l' },
{ src: 'avatar_m' },
{ src: 'avatar_n' },
{ src: 'avatar_o' },
{ src: 'avatar_p' },
{ src: 'avatar_q' },
{ src: 'avatar_r' },
{ src: 'avatar_s' },
{ src: 'avatar_t' }
];
function selectAvatar(avatar: Avatar) {
$htmlBody.avatar = avatar.src;
$openModal.avatar = false;
}
</script>
<Modal bind:open={$openModal.avatar}>
<!-- heading -->
<div class="flex justify-between">
<h1 class="text-sm font-semibold">Choose your Avatars</h1>
<button
type="button"
on:click={() => ($openModal.avatar = false)}
class="p-2 bg-[#F1F6FF] hover:scale-110 transition-all duration-300 rounded-full w-6"
>
<img src="/free-tools/progress-report/close-icon.svg" alt="Close icon" />
</button>
</div>
<!-- map for each avatar -->
<div class="flex flex-wrap justify-evenly gap-y-3 mt-3">
{#each avatars as avatar, index}
<button
on:click={() => {
selectAvatar(avatar);
}}
on:mouseenter={() => {
hoveredIndex = index;
isAddIcon = true;
}}
on:mouseleave={() => {
hoveredIndex = null;
isAddIcon = false;
}}
class="relative w-[17%] flex justify-center items-center shadow-sm rounded-full hover:scale-110 transition-all duration-300"
>
<!-- + icon overlay -->
{#if isAddIcon && hoveredIndex === index}
<div
class="w-full h-full flex justify-center items-center bg-white border absolute rounded-full opacity-[0.7] blur-md"
></div>
<img src="/free-tools/progress-report/hover-plus-icon.svg" alt="" class="absolute" />
{/if}
<!-- avatar -->
<img
src="https://assets.cdn.clsrio.com/progress-report/avatar/{avatar.src}.svg"
alt=""
class="w-full"
/>
</button>
{/each}
</div>
<!-- -->
</Modal>
@@ -0,0 +1,87 @@
<script lang="ts">
import Modal from '$lib/Modal/Modal.svelte';
import { htmlBody, openModal } from './store';
interface Background {
src: string;
}
let backgrounds: Background[] = [
{
src: 'blue_tetiary_background'
},
{
src: 'blue_secondary_background'
},
{
src: 'blue_primary_background'
},
{
src: 'purple_tetiary_background'
},
{
src: 'purple_secondary_background'
},
{
src: 'purple_primary_background'
},
{
src: 'black_tetiary_background'
},
{
src: 'black_secondary_background'
},
{
src: 'black_primary_background'
},
{
src: 'yellow_tetiary_background'
},
{
src: 'yellow_secondary_background'
},
{
src: 'yellow_primary_background'
}
];
function selectBackground(background: Background) {
$htmlBody.background = background.src;
$openModal.background = false;
}
</script>
<Modal className="h-full max-h-[60vh]" bind:open={$openModal.background}>
<!-- -->
<div class="flex justify-between">
<h1 class="text-sm font-semibold">Choose your Background</h1>
<button
type="button"
on:click={() => ($openModal.background = false)}
class="p-2 bg-[#F1F6FF] hover:scale-110 transition-all duration-300 rounded-full w-6"
>
<img src="/free-tools/progress-report/close-icon.svg" alt="Close icon" />
</button>
</div>
<!-- -->
<div
class="flex flex-wrap justify-between gap-y-3 mt-3 overflow-y-auto overflow-x-hidden h-[85%]"
>
{#each backgrounds as background}
<button
on:click={() => {
selectBackground(background);
}}
class="flex w-[30%] items-center shadow-sm hover:scale-110 transition-all duration-300"
>
<img
src="https://assets.cdn.clsrio.com/progress-report/backgrounds/{background.src}.webp"
alt=""
class=""
/>
</button>
{/each}
</div>
<!-- -->
</Modal>
@@ -0,0 +1,52 @@
<script lang="ts">
import { toPng } from 'html-to-image';
import { nodeStore, htmlBody, openModal } from './store';
export let isDisabled: boolean;
export let isDownloading: boolean;
export let text: string = 'Generate Progress Report';
function convertToPng() {
isDownloading = true;
$openModal.showFullscreenButton = false;
const fileName = `${$htmlBody.name} - ${$htmlBody.mood.text} (ClassroomIO_com)`;
if ($nodeStore) {
setTimeout(() => {
toPng($nodeStore, {
width: 500,
height: 500,
quality: 1
})
.then((dataUrl) => {
const link = document.createElement('a');
link.download = fileName;
link.href = dataUrl;
link.click();
})
.catch((error) => {
console.error('Oops, something went wrong!', error);
})
.finally(() => {
isDownloading = false;
$openModal.showFullscreenButton = true;
});
}, 300);
} else {
console.error('Node is not defined');
}
}
</script>
<button
type="button"
disabled={isDownloading || !isDisabled ? true : false}
on:click={convertToPng}
class="{isDownloading || !isDisabled
? 'bg-gray-200 cursor-not-allowed text-gray-600'
: 'bg-[#0233BD] text-white'} {text === 'Generate Progress Report'
? 'block md:hidden'
: ''} mt-5 transition-all delay-100 text-white text-xs font-semibold w-full py-3 rounded-md"
>{isDownloading ? 'Loading...' : text}</button
>
@@ -0,0 +1,11 @@
<script lang="ts">
import Modal from '$lib/Modal/Modal.svelte';
import Report from './Report.svelte';
import { openModal } from './store';
</script>
<Modal bind:open={$openModal.fullscreen}>
<!-- -->
<Report />
<!-- -->
</Modal>
@@ -0,0 +1,100 @@
<script lang="ts">
import Modal from '$lib/Modal/Modal.svelte';
import { htmlBody, openModal } from './store';
interface Mood {
text: string;
iconSrc: string;
}
let moods: Mood[] = [
{
text: 'Unmotivated',
iconSrc: 'unmotivated'
},
{
text: 'Feeling Enlightened',
iconSrc: 'feeling-enlightened'
},
{
text: 'Enthusiastic',
iconSrc: 'enthusiastic'
},
{
text: 'Confused',
iconSrc: 'confused'
},
{
text: 'Feeling stuck',
iconSrc: 'feeling-stuck'
},
// {
// text: 'Believing in the process',
// iconSrc: 'believing-in-the-process'
// },
{
text: 'Curious',
iconSrc: 'curious'
},
{
text: 'Achieving Mastery',
iconSrc: 'achieving-mastery'
},
{
text: 'Feeling Positive',
iconSrc: 'feeling-positive'
},
{
text: 'Reflecting',
iconSrc: 'reflecting'
},
{
text: 'Making progress',
iconSrc: 'progress'
},
{
text: 'Feeling Sad',
iconSrc: 'sad'
}
];
function selectMood(mood: Mood) {
$htmlBody.mood.text = mood.text;
$htmlBody.mood.iconSrc = mood.iconSrc;
$openModal.mood = false;
}
</script>
<Modal bind:open={$openModal.mood}>
<!-- header -->
<div class="flex justify-between">
<h1 class="text-sm font-semibold">Choose your Mood</h1>
<button
type="button"
on:click={() => ($openModal.mood = false)}
class="p-2 bg-[#F1F6FF] hover:scale-110 transition-all duration-300 rounded-full w-6"
>
<img src="/free-tools/progress-report/close-icon.svg" alt="Close icon" />
</button>
</div>
<!-- mapping -->
<div class="flex flex-wrap justify-between gap-y-3 mt-3">
{#each moods as mood}
<button
on:click={() => {
selectMood(mood);
}}
class="flex gap-3 items-center shadow-sm rounded-full px-4 py-1 bg-[#F1F6FF] hover:bg-[#0233BD] hover:text-white transition-all duration-700"
>
<p class="text-xs md:text-sm font-semibold">{mood.text}</p>
<img
src="https://assets.cdn.clsrio.com/progress-report/emojis/{mood.iconSrc}.png"
alt={mood.text}
class="w-4"
/>
</button>
{/each}
</div>
<!-- -->
</Modal>
@@ -0,0 +1,142 @@
<script lang="ts">
import { htmlBody, nodeStore, openModal } from './store';
import { derived } from 'svelte/store';
const colorMap = {
blue_tetiary_background: '#0542CC',
blue_secondary_background: '#0542CC',
blue_primary_background: '#0542CC',
purple_tetiary_background: '#9D4EDD',
purple_secondary_background: '#9D4EDD',
purple_primary_background: '#9D4EDD',
black_tetiary_background: '#252525',
black_secondary_background: '#252525',
black_primary_background: '#252525',
yellow_tetiary_background: '#FFBC42',
yellow_secondary_background: '#FFBC42',
yellow_primary_background: '#FFBC42'
};
type ColorMapKey = keyof typeof colorMap;
const backgroundColor = derived(htmlBody, ($htmlBody) => {
return colorMap[$htmlBody.background as ColorMapKey] || '#1e5ed2';
});
let node: any;
$: nodeStore.set(node);
$: rootBgImage = `https://assets.cdn.clsrio.com/progress-report/backgrounds/${
$htmlBody.background || 'blue_tetiary_background'
}.webp`;
</script>
<div
class="h-full min-h-[500px] max-h-[500px] md:h-[60vh] w-full border relative flex items-center justify-center"
style={`font-family: "Space Grotesk", sans-serif; background-image: url('${rootBgImage}'); background-size: cover; background-repeat: no-repeat`}
bind:this={node}
>
<!-- fullscreen button -->
{#if $openModal.showFullscreenButton}
{#if $openModal.fullscreen}
<button
type="button"
on:click={() => ($openModal.fullscreen = !$openModal.fullscreen)}
class="absolute top-4 right-4 p-1.5 rounded-full hover:scale-90 transition-all duration-300 bg-white"
>
<img src="/free-tools/progress-report/close-icon.svg" alt="" class="" />
</button>
{:else}
<button
type="button"
on:click={() => ($openModal.fullscreen = !$openModal.fullscreen)}
class="absolute top-4 right-4 w-7 hover:scale-90 transition-all duration-300"
>
<img src="/free-tools/progress-report/full-screen-icon.svg" alt="" class="" />
</button>
{/if}
{/if}
<div class="mt-20">
<!-- using both md & lg for the width here because the lib doesn't consider md to be medium screens and above, using md alone will make the output break on desktop so i had to also specify for large screens -->
<div
class="rounded-lg shadow-md bg-white border-4 border-gray-100 z-20 mx-auto flex justify-center items-center w-[16rem] h-[16rem]"
>
<div class="relative w-full h-full flex flex-col justify-center gap-3 items-center px-2">
<!-- avatar -->
<img
src={`https://assets.cdn.clsrio.com/progress-report/avatar/${
$htmlBody.avatar || 'avatar_a'
}.svg`}
alt=""
class="absolute top-0 w-[25%] h-[25%] ml-2 -mt-[2.2rem]"
/>
<!-- mood -->
{#if $htmlBody.name || $htmlBody.mood.text}
<div
style="background-color: {$backgroundColor}; color: white; display: flex;"
class="px-3 py-0.5 border border-[#EDEDED] rounded-2xl mt-6 md:mt-3 font-semibold text-[10px] md:text-[0.7rem] flex items-center justify-between"
>
<span class="mr-0.5">{$htmlBody.name}</span>
{#if $htmlBody.mood.text}
<span class="flex gap-1 items-center">
<p class="font-semibold text-[10px] md:text-[0.7rem]">
is {$htmlBody.mood.text}
</p>
<img
src="https://assets.cdn.clsrio.com/progress-report/emojis/{$htmlBody.mood
.iconSrc}.png"
alt={$htmlBody.mood.text}
class="w-3"
/>
</span>
{/if}
</div>
{:else}
<p
style="background-color: {$backgroundColor}"
class="px-3 py-0.5 border border-[#EDEDED] text-white rounded-2xl mt-3 font-medium text-[9px]"
>
Add your name
</p>
{/if}
<!-- learning -->
<span
class="w-full md:my-2 p-1 overflow-hidden text-[0.7rem] md:text-[0.8rem] font-normal text-center outline-none h-[10vh] md:h-[12vh]"
>
{$htmlBody.learning}
</span>
<!-- progress -->
<div class="flex flex-col justify-center items-center mb-2 md:mb-0">
<div class="border border-[#d9e0f5] w-full h-4 rounded-full p-0.5 bg-[#f1f6ff]">
<div
class="progress-range rounded-full w-full h-full"
style="background: {$backgroundColor}; width: {$htmlBody.progress}%"
/>
</div>
<p class="text-[12px] font-semibold mt-1">Progress Achieved: {$htmlBody.progress}%</p>
</div>
</div>
</div>
<div
style="display: flex;"
class="border mt-20 bottom-5 w-[50%] mx-auto flex gap-1 items-center justify-center bg-white rounded-md px-2 py-1"
>
<img src="/logo-192.png" alt="classroomio logo" class="w-4" />
<p class="text-[10px] font-bold">ClassroomIO.com</p>
</div>
<!-- -->
</div>
</div>
<!-- importing font family -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap');
</style>
@@ -0,0 +1,59 @@
import { derived, writable } from 'svelte/store';
import type { Writable } from 'svelte/store';
interface OpenModal {
avatar: boolean;
background: boolean;
fullscreen: boolean;
showFullscreenButton: boolean;
mood: boolean;
}
export interface HtmlBody {
name: string;
learning: string;
progress: number;
avatar: string;
background: string;
mood: {
text: string;
iconSrc: string;
};
}
// store for the node binded to the report component for the component to image conversion
export const nodeStore = writable(null);
export const openModal: Writable<OpenModal> = writable({
avatar: false,
background: false,
fullscreen: false,
showFullscreenButton: true,
mood: false
});
const placeholderLearning = 'Tell us what you are learning';
// report html
export const htmlBody: Writable<HtmlBody> = writable({
name: '',
learning: placeholderLearning,
progress: 10,
avatar: '',
background: '',
mood: {
text: '',
iconSrc: ''
}
});
// a mini validation for the htmlBody store that ensures all fields are filled
export const isFormComplete = derived(htmlBody, ($htmlBody) => {
return !!(
$htmlBody.name.trim() &&
$htmlBody.learning.trim() !== placeholderLearning &&
$htmlBody.progress > 0 &&
$htmlBody.mood.text.trim() &&
$htmlBody.mood.iconSrc.trim()
);
});