Merged in feature/training-content (pull request #95)
ENCOA-69: Training update, most of the styles in the old tips were standardized, before all the styles were hardcoded into the tip
This commit is contained in:
@@ -1,39 +1,47 @@
|
||||
import { useCallback } from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import { HighlightConfig, HighlightTarget } from "@/training/TrainingInterfaces";
|
||||
|
||||
const HighlightContent: React.FC<{
|
||||
html: string;
|
||||
highlightPhrases: string[],
|
||||
firstOccurence?: boolean
|
||||
}> = ({
|
||||
interface HighlightedContentProps {
|
||||
html: string;
|
||||
highlightConfigs: HighlightConfig[];
|
||||
contentType: HighlightTarget;
|
||||
currentSegmentIndex?: number;
|
||||
}
|
||||
|
||||
const HighlightedContent: React.FC<HighlightedContentProps> = ({
|
||||
html,
|
||||
highlightPhrases,
|
||||
firstOccurence = false
|
||||
highlightConfigs,
|
||||
contentType,
|
||||
currentSegmentIndex
|
||||
}) => {
|
||||
|
||||
const createHighlightedContent = useCallback(() => {
|
||||
if (highlightPhrases.length === 0) {
|
||||
return { __html: html };
|
||||
}
|
||||
|
||||
const escapeRegExp = (string: string) => {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
};
|
||||
|
||||
const regex = new RegExp(`(${highlightPhrases.map(escapeRegExp).join('|')})`, 'i');
|
||||
const globalRegex = new RegExp(`(${highlightPhrases.map(escapeRegExp).join('|')})`, 'gi');
|
||||
|
||||
let highlightedHtml = html;
|
||||
highlightConfigs.forEach(config => {
|
||||
if (config.targets.includes(contentType) || config.targets.includes('all')) {
|
||||
const escapeRegExp = (string: string) => {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
};
|
||||
|
||||
if (firstOccurence) {
|
||||
highlightedHtml = html.replace(regex, (match) => `<span style="background-color: yellow;">${match}</span>`);
|
||||
} else {
|
||||
highlightedHtml = html.replace(globalRegex, (match) => `<span style="background-color: yellow;">${match}</span>`);
|
||||
}
|
||||
const regex = new RegExp(config.phrases.map(escapeRegExp).join('|'), 'g');
|
||||
|
||||
if (contentType === 'segment' && currentSegmentIndex !== undefined) {
|
||||
const segments = highlightedHtml.split('</div>');
|
||||
segments[currentSegmentIndex] = segments[currentSegmentIndex].replace(regex, (match) => {
|
||||
return `<span style="background-color: #FFFACD;">${match}</span>`;
|
||||
});
|
||||
highlightedHtml = segments.join('</div>');
|
||||
} else {
|
||||
highlightedHtml = highlightedHtml.replace(regex, (match) => {
|
||||
return `<span style="background-color: #FFFACD;">${match}</span>`;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { __html: highlightedHtml };
|
||||
}, [html, highlightPhrases, firstOccurence]);
|
||||
}, [html, highlightConfigs, contentType, currentSegmentIndex]);
|
||||
|
||||
return <div dangerouslySetInnerHTML={createHighlightedContent()} />;
|
||||
};
|
||||
|
||||
export default HighlightContent;
|
||||
export default HighlightedContent;
|
||||
@@ -1,91 +1,43 @@
|
||||
import React, { useState, useCallback } from "react";
|
||||
import React from "react";
|
||||
import ExerciseWalkthrough from "@/training/ExerciseWalkthrough";
|
||||
import { ITrainingTip, WalkthroughConfigs } from "./TrainingInterfaces";
|
||||
import formatTip from "./FormatTip";
|
||||
|
||||
|
||||
// This wrapper is just to test new exercises from the handbook, will be removed when all the tips and exercises are in firestore
|
||||
const TrainingExercise: React.FC<ITrainingTip> = (trainingTip: ITrainingTip) => {
|
||||
const leftText = "<div class=\"container mx-auto px-4 overflow-x-auto\"><table class=\"min-w-full bg-white border border-gray-300\"><thead><tr class=\"bg-gray-100\"><th class=\"py-2 px-4 border-b font-semibold text-left\">Category</th><th class=\"py-2 px-4 border-b font-semibold text-left\">Option A</th><th class=\"py-2 px-4 border-b font-semibold text-left\">Option B</th></tr></thead><tbody><tr><td class=\"py-2 px-4 border-b font-medium\">Self</td><td class=\"py-2 px-4 border-b\">You need to take care of yourself and connect with the people around you.</td><td class=\"py-2 px-4 border-b\">Focus on your interests and talents and meet people who are like you.</td></tr><tr class=\"bg-gray-50\"><td class=\"py-2 px-4 border-b font-medium\">Home</td><td class=\"py-2 px-4 border-b\">It's a good idea to paint your living room yellow.</td><td class=\"py-2 px-4 border-b\">You should arrange your home so that it makes you feel happy.</td></tr><tr><td class=\"py-2 px-4 border-b font-medium\">Financial Life</td><td class=\"py-2 px-4 border-b\">You can be happy if you have enough money, but don't want money too much.</td><td class=\"py-2 px-4 border-b\">If you waste money on things you don't need, you won't have enough money for things that you do need.</td></tr><tr class=\"bg-gray-50\"><td class=\"py-2 px-4 border-b font-medium\">Social Life</td><td class=\"py-2 px-4 border-b\">A good group of friends can increase your happiness.</td><td class=\"py-2 px-4 border-b\">Researchers say that a happy friend can increase our mood by nine percent.</td></tr><tr><td class=\"py-2 px-4 border-b font-medium\">Workplace</td><td class=\"py-2 px-4 border-b\">You spend a lot of time at work, so you should like your workplace.</td><td class=\"py-2 px-4 border-b\">Your boss needs to be someone you enjoy working for.</td></tr><tr class=\"bg-gray-50\"><td class=\"py-2 px-4 border-b font-medium\">Community</td><td class=\"py-2 px-4 border-b\">The place where you live is more important for happiness than anything else.</td><td class=\"py-2 px-4 border-b\">Live around people who have the same amount of money as you do.</td></tr></tbody></table></div>";
|
||||
const tip = {
|
||||
category: "Strategy",
|
||||
body: "<p>Look for <b>clues to the main idea</b> in the first (and sometimes second) sentence of a paragraph.</p>"
|
||||
}
|
||||
const question = "<div class=\"container mx-auto px-4 py-8\"><h2 class=\"text-2xl font-bold mb-4\">Identifying Main Ideas</h2><p class=\"text-lg leading-relaxed mb-6\">Read the statements below. Circle the main idea in each pair of statements (a or b).</p></div>";
|
||||
const rightTextData: WalkthroughConfigs[] = [
|
||||
{
|
||||
"html": "<div class='bg-blue-100 p-4 rounded-lg mb-4'><h2 class='text-xl font-bold mb-2'>Identifying Main Ideas</h2><p class='text-gray-700 leading-relaxed'>Let's analyze each pair of statements to determine which one represents the main idea. We'll focus on which statement is more general and encompasses the overall concept.</p></div>",
|
||||
"wordDelay": 200,
|
||||
"holdDelay": 5000,
|
||||
"highlight": []
|
||||
},
|
||||
{
|
||||
"html": "<div class='bg-green-50 p-4 rounded-lg mb-4'><h3 class='text-lg font-semibold mb-2'>1. Self</h3><p class='text-gray-700 leading-relaxed'>Main idea: <b>A. You need to take care of yourself and connect with the people around you.</b></p><p class='mt-2'>This statement is more comprehensive, covering both self-care and social connections. Option B is more specific and could be considered a subset of A.</p></div>",
|
||||
"wordDelay": 200,
|
||||
"holdDelay": 8000,
|
||||
"highlight": ["You need to take care of yourself and connect with the people around you."]
|
||||
},
|
||||
{
|
||||
"html": "<div class='bg-yellow-50 p-4 rounded-lg mb-4'><h3 class='text-lg font-semibold mb-2'>2. Home</h3><p class='text-gray-700 leading-relaxed'>Main idea: <b>B. You should arrange your home so that it makes you feel happy.</b></p><p class='mt-2'>This statement is more general and applies to the entire home. Option A is a specific example that could fall under this broader concept.</p></div>",
|
||||
"wordDelay": 200,
|
||||
"holdDelay": 8000,
|
||||
"highlight": ["You should arrange your home so that it makes you feel happy."]
|
||||
},
|
||||
{
|
||||
"html": "<div class='bg-pink-50 p-4 rounded-lg mb-4'><h3 class='text-lg font-semibold mb-2'>3. Financial Life</h3><p class='text-gray-700 leading-relaxed'>Main idea: <b>A. You can be happy if you have enough money, but don't want money too much.</b></p><p class='mt-2'>This statement provides a balanced view of money's role in happiness. Option B is more specific and could be seen as a consequence of wanting money too much.</p></div>",
|
||||
"wordDelay": 200,
|
||||
"holdDelay": 8000,
|
||||
"highlight": ["You can be happy if you have enough money, but don't want money too much."]
|
||||
},
|
||||
{
|
||||
"html": "<div class='bg-purple-50 p-4 rounded-lg mb-4'><h3 class='text-lg font-semibold mb-2'>4. Social Life</h3><p class='text-gray-700 leading-relaxed'>Main idea: <b>A. A good group of friends can increase your happiness.</b></p><p class='mt-2'>This statement is more general about the impact of friendships. Option B provides a specific statistic that supports this main idea.</p></div>",
|
||||
"wordDelay": 200,
|
||||
"holdDelay": 8000,
|
||||
"highlight": ["A good group of friends can increase your happiness."]
|
||||
},
|
||||
{
|
||||
"html": "<div class='bg-indigo-50 p-4 rounded-lg mb-4'><h3 class='text-lg font-semibold mb-2'>5. Workplace</h3><p class='text-gray-700 leading-relaxed'>Main idea: <b>A. You spend a lot of time at work, so you should like your workplace.</b></p><p class='mt-2'>This statement covers the overall importance of workplace satisfaction. Option B focuses on one specific aspect (the boss) and is less comprehensive.</p></div>",
|
||||
"wordDelay": 200,
|
||||
"holdDelay": 8000,
|
||||
"highlight": ["You spend a lot of time at work, so you should like your workplace."]
|
||||
},
|
||||
{
|
||||
"html": "<div class='bg-red-50 p-4 rounded-lg mb-4'><h3 class='text-lg font-semibold mb-2'>6. Community</h3><p class='text-gray-700 leading-relaxed'>Main idea: <b>A. The place where you live is more important for happiness than anything else.</b></p><p class='mt-2'>While this statement might be debatable, it's more general and encompasses the overall importance of community. Option B is a specific suggestion about community demographics.</p></div>",
|
||||
"wordDelay": 200,
|
||||
"holdDelay": 8000,
|
||||
"highlight": ["The place where you live is more important for happiness than anything else."]
|
||||
},
|
||||
{
|
||||
"html": "<div class='bg-orange-50 p-4 rounded-lg mb-4'><h3 class='text-lg font-semibold mb-2'>Key Strategy</h3><p class='text-gray-700 leading-relaxed'>When identifying main ideas:</p><ul class='list-disc pl-5 space-y-2'><li>Look for broader, more encompassing statements</li><li>Consider which statement other ideas could fall under</li><li>Identify which statement provides a general principle rather than a specific example</li></ul></div>",
|
||||
"wordDelay": 200,
|
||||
"holdDelay": 8000,
|
||||
"highlight": []
|
||||
},
|
||||
{
|
||||
"html": "<div class='bg-teal-50 p-4 rounded-lg'><h3 class='text-lg font-semibold mb-2'>Helpful Tip</h3><p class='text-gray-700 leading-relaxed'>Remember to look for clues to the main idea in the first (and sometimes second) sentence of a paragraph. In this exercise, we applied this concept to pairs of statements. This approach can help you quickly identify the central theme or main point in various types of text.</p></div>",
|
||||
"wordDelay": 200,
|
||||
"holdDelay": 5000,
|
||||
"highlight": []
|
||||
const tip = {
|
||||
"category": "",
|
||||
"embedding": "",
|
||||
"text": "",
|
||||
"html": "",
|
||||
"id": "",
|
||||
"verified": true,
|
||||
"standalone": false,
|
||||
"exercise": {
|
||||
"question": "",
|
||||
"additional": "",
|
||||
"segments": []
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const mockTip: ITrainingTip = {
|
||||
id: "some random id",
|
||||
tipCategory: tip.category,
|
||||
tipHtml: tip.body,
|
||||
standalone: false,
|
||||
exercise: {
|
||||
question: question,
|
||||
highlightable: leftText,
|
||||
segments: rightTextData
|
||||
}
|
||||
}
|
||||
const mockTip: ITrainingTip = {
|
||||
id: "some random id",
|
||||
tipCategory: tip.category,
|
||||
tipHtml: tip.html,
|
||||
standalone: tip.standalone,
|
||||
exercise: {
|
||||
question: tip.exercise.question,
|
||||
additional: tip.exercise.additional,
|
||||
segments: tip.exercise.segments as WalkthroughConfigs[]
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col p-10">
|
||||
<ExerciseWalkthrough {...trainingTip}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const formattedTip = formatTip(mockTip);
|
||||
return (
|
||||
<ExerciseWalkthrough {...formatTip(trainingTip)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default TrainingExercise;
|
||||
@@ -1,19 +1,32 @@
|
||||
import React, {useState, useEffect, useRef, useCallback} from "react";
|
||||
import {animated} from "@react-spring/web";
|
||||
import {FaRegCirclePlay, FaRegCircleStop} from "react-icons/fa6";
|
||||
import HighlightContent from "../HighlightContent";
|
||||
import {ITrainingTip, SegmentRef, TimelineEvent} from "./TrainingInterfaces";
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { animated } from '@react-spring/web';
|
||||
import { FaRegCirclePlay, FaRegCircleStop } from "react-icons/fa6";
|
||||
import HighlightContent from '../HighlightContent';
|
||||
import { ITrainingTip, SegmentRef, TimelineEvent, HighlightConfig, InsertHtmlConfig } from './TrainingInterfaces';
|
||||
import Tip from './Tip';
|
||||
|
||||
interface HtmlState {
|
||||
question: string;
|
||||
additional: string;
|
||||
walkthrough: string;
|
||||
}
|
||||
|
||||
const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
|
||||
const [isAutoPlaying, setIsAutoPlaying] = useState<boolean>(false);
|
||||
const [currentTime, setCurrentTime] = useState<number>(0);
|
||||
const [walkthroughHtml, setWalkthroughHtml] = useState<string>("");
|
||||
const [highlightedPhrases, setHighlightedPhrases] = useState<string[]>([]);
|
||||
const [currentHighlightConfigs, setCurrentHighlightConfigs] = useState<HighlightConfig[]>([]);
|
||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||
const [currentSegmentIndex, setCurrentSegmentIndex] = useState<number>(0);
|
||||
const timelineRef = useRef<TimelineEvent[]>([]);
|
||||
const animationRef = useRef<number | null>(null);
|
||||
const segmentsRef = useRef<SegmentRef[]>([]);
|
||||
|
||||
const [questionHtml, setQuestionHtml] = useState(tip.exercise?.question || '');
|
||||
const [additionalHtml, setAdditionalHtml] = useState(tip.exercise?.additional || '');
|
||||
const [walkthroughHtml, setWalkthroughHtml] = useState<string>('');
|
||||
const [htmlStates, setHtmlStates] = useState<HtmlState[]>([]);
|
||||
const lastProcessedInsertTime = useRef<number>(-1);
|
||||
|
||||
const toggleAutoPlay = useCallback(() => {
|
||||
setIsAutoPlaying((prev) => {
|
||||
if (!prev && currentTime === getMaxTime()) {
|
||||
@@ -21,7 +34,6 @@ const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
|
||||
}
|
||||
return !prev;
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentTime]);
|
||||
|
||||
const handleAnimationComplete = useCallback(() => {
|
||||
@@ -33,23 +45,24 @@ const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
|
||||
}, []);
|
||||
|
||||
const getMaxTime = (): number => {
|
||||
return (
|
||||
tip.exercise?.segments.reduce((sum, segment) => sum + segment.wordDelay * segment.html.split(/\s+/).length + segment.holdDelay, 0) ?? 0
|
||||
);
|
||||
return tip.exercise?.segments.reduce((sum, segment) =>
|
||||
sum + segment.wordDelay * segment.html.split(/\s+/).length + segment.holdDelay, 0
|
||||
) ?? 0;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const timeline: TimelineEvent[] = [];
|
||||
let currentTimePosition = 0;
|
||||
segmentsRef.current = [];
|
||||
const newHtmlStates: HtmlState[] = [];
|
||||
|
||||
tip.exercise?.segments.forEach((segment, index) => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(segment.html, "text/html");
|
||||
const doc = parser.parseFromString(segment.html, 'text/html');
|
||||
const words: string[] = [];
|
||||
const walkTree = (node: Node) => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
words.push(...(node.textContent?.split(/\s+/).filter((word) => word.length > 0) || []));
|
||||
words.push(...(node.textContent?.split(/\s+/).filter(word => word.length > 0) || []));
|
||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
Array.from(node.childNodes).forEach(walkTree);
|
||||
}
|
||||
@@ -62,69 +75,116 @@ const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
|
||||
...segment,
|
||||
words: words,
|
||||
startTime: currentTimePosition,
|
||||
endTime: currentTimePosition + textDuration,
|
||||
endTime: currentTimePosition + textDuration
|
||||
});
|
||||
|
||||
timeline.push({
|
||||
type: "text",
|
||||
type: 'text',
|
||||
start: currentTimePosition,
|
||||
end: currentTimePosition + textDuration,
|
||||
segmentIndex: index,
|
||||
segmentIndex: index
|
||||
});
|
||||
|
||||
currentTimePosition += textDuration;
|
||||
|
||||
timeline.push({
|
||||
type: "highlight",
|
||||
type: 'highlight',
|
||||
start: currentTimePosition,
|
||||
end: currentTimePosition + segment.holdDelay,
|
||||
content: segment.highlight,
|
||||
segmentIndex: index,
|
||||
segmentIndex: index
|
||||
});
|
||||
|
||||
if (segment.insertHTML && segment.insertHTML.length > 0) {
|
||||
newHtmlStates.push({
|
||||
question: questionHtml,
|
||||
additional: additionalHtml,
|
||||
walkthrough: walkthroughHtml
|
||||
});
|
||||
timeline.push({
|
||||
type: 'insert',
|
||||
start: currentTimePosition,
|
||||
end: currentTimePosition + segment.holdDelay,
|
||||
segmentIndex: index,
|
||||
content: segment.insertHTML
|
||||
});
|
||||
}
|
||||
|
||||
currentTimePosition += segment.holdDelay;
|
||||
});
|
||||
|
||||
for (let i = 0; i < timeline.length; i++) {
|
||||
if (timeline[i].type === 'insert') {
|
||||
const nextInsertIndex = timeline.findIndex((event, index) => index > i && event.type === 'insert');
|
||||
if (nextInsertIndex !== -1) {
|
||||
timeline[i].end = timeline[nextInsertIndex].start;
|
||||
} else {
|
||||
timeline[i].end = currentTimePosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timelineRef.current = timeline;
|
||||
}, [tip.exercise?.segments]);
|
||||
setHtmlStates(newHtmlStates);
|
||||
}, [tip.exercise?.segments, questionHtml, additionalHtml, walkthroughHtml]);
|
||||
|
||||
const updateText = useCallback(() => {
|
||||
const currentEvent = timelineRef.current.find((event) => currentTime >= event.start && currentTime < event.end);
|
||||
const currentEvents = timelineRef.current.filter(
|
||||
event => currentTime >= event.start && currentTime <= event.end
|
||||
);
|
||||
|
||||
if (currentEvent) {
|
||||
if (currentEvent.type === "text") {
|
||||
if (currentTime < lastProcessedInsertTime.current) {
|
||||
const lastInsertEvent = timelineRef.current
|
||||
.filter(event => event.type === 'insert' && event.start <= currentTime)
|
||||
.pop();
|
||||
|
||||
if (lastInsertEvent) {
|
||||
const stateIndex = timelineRef.current.indexOf(lastInsertEvent);
|
||||
if (stateIndex >= 0 && stateIndex < htmlStates.length) {
|
||||
const previousState = htmlStates[stateIndex];
|
||||
setQuestionHtml(previousState.question);
|
||||
setAdditionalHtml(previousState.additional);
|
||||
setWalkthroughHtml(previousState.walkthrough);
|
||||
}
|
||||
} else {
|
||||
// If no previous insert event, revert to initial state
|
||||
setQuestionHtml(tip.exercise?.question || '');
|
||||
setAdditionalHtml(tip.exercise?.additional || '');
|
||||
setWalkthroughHtml('');
|
||||
}
|
||||
}
|
||||
|
||||
currentEvents.forEach(currentEvent => {
|
||||
if (currentEvent.type === 'text') {
|
||||
const segment = segmentsRef.current[currentEvent.segmentIndex];
|
||||
const elapsedTime = currentTime - currentEvent.start;
|
||||
const wordsToShow = Math.min(Math.floor(elapsedTime / segment.wordDelay), segment.words.length);
|
||||
|
||||
const previousSegmentsHtml = segmentsRef.current
|
||||
.slice(0, currentEvent.segmentIndex)
|
||||
.map((seg) => seg.html)
|
||||
.join("");
|
||||
.map(seg => seg.html)
|
||||
.join('');
|
||||
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(segment.html, "text/html");
|
||||
const doc = parser.parseFromString(segment.html, 'text/html');
|
||||
let wordCount = 0;
|
||||
const walkTree = (node: Node, action: (node: Node) => void): boolean => {
|
||||
if (node.nodeType === Node.TEXT_NODE && node.textContent) {
|
||||
const words = node.textContent.split(/(\s+)/).filter((word) => word.length > 0);
|
||||
if (wordCount + words.filter((w) => !/\s+/.test(w)).length <= wordsToShow) {
|
||||
const words = node.textContent.split(/(\s+)/).filter(word => word.length > 0);
|
||||
if (wordCount + words.filter(w => !/\s+/.test(w)).length <= wordsToShow) {
|
||||
action(node.cloneNode(true));
|
||||
wordCount += words.filter((w) => !/\s+/.test(w)).length;
|
||||
wordCount += words.filter(w => !/\s+/.test(w)).length;
|
||||
} else {
|
||||
const remainingWords = wordsToShow - wordCount;
|
||||
const newTextContent = words.reduce(
|
||||
(acc, word) => {
|
||||
if (!/\s+/.test(word) && acc.nonSpaceWords < remainingWords) {
|
||||
acc.text += word;
|
||||
acc.nonSpaceWords++;
|
||||
} else if (/\s+/.test(word) || acc.nonSpaceWords < remainingWords) {
|
||||
acc.text += word;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{text: "", nonSpaceWords: 0},
|
||||
).text;
|
||||
const newTextContent = words.reduce((acc, word) => {
|
||||
if (!/\s+/.test(word) && acc.nonSpaceWords < remainingWords) {
|
||||
acc.text += word;
|
||||
acc.nonSpaceWords++;
|
||||
} else if (/\s+/.test(word) || acc.nonSpaceWords < remainingWords) {
|
||||
acc.text += word;
|
||||
}
|
||||
return acc;
|
||||
}, { text: '', nonSpaceWords: 0 }).text;
|
||||
const newNode = node.cloneNode(false);
|
||||
newNode.textContent = newTextContent;
|
||||
action(newNode);
|
||||
@@ -133,38 +193,79 @@ const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
|
||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const clone = node.cloneNode(false);
|
||||
action(clone);
|
||||
Array.from(node.childNodes).some((child) => {
|
||||
return walkTree(child, (childNode) => (clone as Node).appendChild(childNode));
|
||||
Array.from(node.childNodes).some(child => {
|
||||
return walkTree(child, childNode => (clone as Node).appendChild(childNode));
|
||||
});
|
||||
}
|
||||
return wordCount >= wordsToShow;
|
||||
};
|
||||
const fragment = document.createDocumentFragment();
|
||||
walkTree(doc.body, (node) => fragment.appendChild(node));
|
||||
walkTree(doc.body, node => fragment.appendChild(node));
|
||||
|
||||
const serializer = new XMLSerializer();
|
||||
const currentSegmentHtml = Array.from(fragment.childNodes)
|
||||
.map((node) => serializer.serializeToString(node))
|
||||
.join("");
|
||||
.map(node => serializer.serializeToString(node))
|
||||
.join('');
|
||||
const newHtml = previousSegmentsHtml + currentSegmentHtml;
|
||||
|
||||
setWalkthroughHtml(newHtml);
|
||||
setHighlightedPhrases([]);
|
||||
} else if (currentEvent.type === "highlight") {
|
||||
setCurrentSegmentIndex(currentEvent.segmentIndex);
|
||||
setCurrentHighlightConfigs([]);
|
||||
} else if (currentEvent.type === 'highlight') {
|
||||
const newHtml = segmentsRef.current
|
||||
.slice(0, currentEvent.segmentIndex + 1)
|
||||
.map((seg) => seg.html)
|
||||
.join("");
|
||||
.map(seg => seg.html)
|
||||
.join('');
|
||||
setWalkthroughHtml(newHtml);
|
||||
setHighlightedPhrases(currentEvent.content || []);
|
||||
setCurrentSegmentIndex(currentEvent.segmentIndex);
|
||||
setCurrentHighlightConfigs(currentEvent.content as HighlightConfig[] || []);
|
||||
} else if (currentEvent.type === 'insert') {
|
||||
const insertConfigs = currentEvent.content as InsertHtmlConfig[];
|
||||
insertConfigs.forEach(config => {
|
||||
switch (config.target) {
|
||||
case 'question':
|
||||
setQuestionHtml(prevHtml => insertHtmlContent(prevHtml, config));
|
||||
break;
|
||||
case 'additional':
|
||||
setAdditionalHtml(prevHtml => insertHtmlContent(prevHtml, config));
|
||||
break;
|
||||
case 'segment':
|
||||
setWalkthroughHtml(prevHtml => insertHtmlContent(prevHtml, config));
|
||||
break;
|
||||
}
|
||||
});
|
||||
lastProcessedInsertTime.current = currentTime;
|
||||
}
|
||||
}
|
||||
}, [currentTime]);
|
||||
});
|
||||
}, [currentTime, htmlStates, tip.exercise?.question, tip.exercise?.additional]);
|
||||
|
||||
useEffect(() => {
|
||||
updateText();
|
||||
}, [currentTime, updateText]);
|
||||
|
||||
const insertHtmlContent = (prevHtml: string, config: InsertHtmlConfig): string => {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = prevHtml;
|
||||
|
||||
const targetElement = tempDiv.querySelector(`#${config.targetId}`);
|
||||
if (targetElement) {
|
||||
switch (config.position) {
|
||||
case 'append':
|
||||
targetElement.insertAdjacentHTML('beforeend', config.html);
|
||||
break;
|
||||
case 'prepend':
|
||||
targetElement.insertAdjacentHTML('afterbegin', config.html);
|
||||
break;
|
||||
case 'replace':
|
||||
targetElement.innerHTML = config.html;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return tempDiv.innerHTML;
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (isAutoPlaying) {
|
||||
const lastEvent = timelineRef.current[timelineRef.current.length - 1];
|
||||
@@ -219,62 +320,81 @@ const ExerciseWalkthrough: React.FC<ITrainingTip> = (tip: ITrainingTip) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (tip.standalone || !tip.exercise) {
|
||||
return (
|
||||
<div className="container mx-auto">
|
||||
<h1 className="text-xl font-bold text-red-600">The exercise for this tip is not available yet!</h1>
|
||||
<div className="tip-container bg-blue-100 p-4 rounded-lg shadow-md mb-4 mt-10">
|
||||
<h3 className="text-xl font-semibold text-blue-800 mb-2">{tip.tipCategory}</h3>
|
||||
<div className="text-gray-700" dangerouslySetInnerHTML={{__html: tip.tipHtml}} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto">
|
||||
<div className="tip-container bg-blue-100 p-4 rounded-lg shadow-md mb-4">
|
||||
<h3 className="text-xl font-semibold text-blue-800 mb-2">{tip.tipCategory}</h3>
|
||||
<div className="text-gray-700" dangerouslySetInnerHTML={{__html: tip.tipHtml}} />
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex flex-row items-center space-x-4 py-4">
|
||||
<button
|
||||
onClick={toggleAutoPlay}
|
||||
className="p-2 bg-blue-500 text-white rounded-full transition-colors duration-200 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"
|
||||
aria-label={isAutoPlaying ? "Pause" : "Play"}>
|
||||
{isAutoPlaying ? <FaRegCircleStop className="w-6 h-6" /> : <FaRegCirclePlay className="w-6 h-6" />}
|
||||
</button>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={timelineRef.current.length > 0 ? timelineRef.current[timelineRef.current.length - 1].end : 0}
|
||||
value={currentTime}
|
||||
onChange={handleSliderChange}
|
||||
onMouseDown={handleSliderMouseDown}
|
||||
onMouseUp={handleSliderMouseUp}
|
||||
onTouchStart={handleSliderMouseDown}
|
||||
onTouchEnd={handleSliderMouseUp}
|
||||
className="flex-grow"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4">
|
||||
<div className="flex-1 bg-white p-6 rounded-lg shadow">
|
||||
{/*<h2 className="text-xl font-bold mb-4">Question</h2>*/}
|
||||
<div className="mb-4" dangerouslySetInnerHTML={{__html: tip.exercise.question}} />
|
||||
<HighlightContent html={tip.exercise.highlightable} highlightPhrases={highlightedPhrases} />
|
||||
<div className="container mx-auto py-6">
|
||||
<Tip category={tip.tipCategory} html={tip.tipHtml} />
|
||||
{!tip.standalone && (
|
||||
<div className='flex flex-col space-y-4'>
|
||||
<div className='flex flex-row items-center space-x-4 py-4'>
|
||||
<button
|
||||
onClick={toggleAutoPlay}
|
||||
className="p-2 bg-blue-500 text-white rounded-full transition-colors duration-200 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"
|
||||
aria-label={isAutoPlaying ? 'Pause' : 'Play'}
|
||||
>
|
||||
{isAutoPlaying ? (
|
||||
<FaRegCircleStop className="w-6 h-6" />
|
||||
) : (
|
||||
<FaRegCirclePlay className="w-6 h-6" />
|
||||
)}
|
||||
</button>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={timelineRef.current.length > 0 ? timelineRef.current[timelineRef.current.length - 1].end : 0}
|
||||
value={currentTime}
|
||||
onChange={handleSliderChange}
|
||||
onMouseDown={handleSliderMouseDown}
|
||||
onMouseUp={handleSliderMouseUp}
|
||||
onTouchStart={handleSliderMouseDown}
|
||||
onTouchEnd={handleSliderMouseUp}
|
||||
className='flex-grow'
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="bg-gray-50 rounded-lg shadow">
|
||||
<div className="p-6 space-y-4">
|
||||
<animated.div dangerouslySetInnerHTML={{__html: walkthroughHtml}} />
|
||||
<div className='flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 w-full'>
|
||||
<div className='flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 w-full'>
|
||||
<div className='flex-1 bg-white p-6 rounded-lg shadow space-y-6'>
|
||||
<div className="container mx-auto px-4">
|
||||
<div id="question-container" className="border p-6 rounded-lg shadow-md">
|
||||
<HighlightContent
|
||||
html={questionHtml}
|
||||
highlightConfigs={currentHighlightConfigs}
|
||||
contentType="question"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{tip.exercise?.additional && (<div className="container mx-auto px-4">
|
||||
<div id="additional-container" className="border p-6 rounded-lg shadow-md">
|
||||
<HighlightContent
|
||||
html={additionalHtml}
|
||||
highlightConfigs={currentHighlightConfigs}
|
||||
contentType="additional"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<div className='bg-gray-50 rounded-lg shadow'>
|
||||
<div id="segment-container" className='p-6 space-y-4'>
|
||||
<animated.div>
|
||||
<HighlightContent
|
||||
html={walkthroughHtml}
|
||||
highlightConfigs={currentHighlightConfigs.filter(config =>
|
||||
config.targets.includes('segment') || config.targets.includes('all')
|
||||
)}
|
||||
contentType="segment"
|
||||
currentSegmentIndex={currentSegmentIndex}
|
||||
/>
|
||||
</animated.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExerciseWalkthrough;
|
||||
export default ExerciseWalkthrough;
|
||||
201
src/components/TrainingContent/FormatTip.ts
Normal file
201
src/components/TrainingContent/FormatTip.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { ITrainingTip, WalkthroughConfigs } from "./TrainingInterfaces";
|
||||
|
||||
const colorOptions = [
|
||||
'red', 'blue', 'green', 'purple', 'pink', 'indigo', 'teal', 'orange', 'cyan', 'emerald', 'sky', 'violet', 'fuchsia', 'rose', 'lime', 'slate'
|
||||
]
|
||||
|
||||
const getRandomColors = (count: number) => {
|
||||
const shuffled = [...colorOptions].sort(() => 0.5 - Math.random());
|
||||
return shuffled.slice(0, count);
|
||||
};
|
||||
|
||||
const classMap = {
|
||||
"mainDiv": {
|
||||
"tip": "flex-col gap-2",
|
||||
"question": "flex-col gap-2",
|
||||
"additional": "flex-col gap-8",
|
||||
"segment": "p-4 rounded-lg mb-4 flex flex-col gap-2"
|
||||
},
|
||||
"h2": {
|
||||
"tip": "mb-4 font-semibold text-lg",
|
||||
"question": "text-lg font-semibold mb-4",
|
||||
"additional": "text-2xl font-semibold mb-4",
|
||||
"segment": "text-xl font-semibold"
|
||||
}
|
||||
}
|
||||
|
||||
const setClass = (element: Element, style: string) => {
|
||||
element.setAttribute('class', style)
|
||||
}
|
||||
|
||||
|
||||
// DON'T OVERRIDE DIV AND SPAN STYLES
|
||||
const processHtml = (section: string, html: string, color: string) => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
const mainDiv = doc.body.firstElementChild;
|
||||
if (mainDiv && mainDiv.tagName === 'DIV') {
|
||||
if (section === "segment") {
|
||||
setClass(mainDiv, `bg-${color}-100 ${classMap["mainDiv"][section]}`);
|
||||
} else {
|
||||
setClass(mainDiv, classMap["mainDiv"][section as keyof typeof classMap["mainDiv"]]);
|
||||
}
|
||||
}
|
||||
|
||||
doc.querySelectorAll('h1').forEach(e => {
|
||||
if (section === "additional") {
|
||||
setClass(e, 'text-4xl font-bold mb-6')
|
||||
} else {
|
||||
setClass(e, 'text-xl font-semibold mb-4');
|
||||
}
|
||||
});
|
||||
|
||||
doc.querySelectorAll('h2').forEach(e => {
|
||||
setClass(e, classMap["h2"][section as keyof typeof classMap["h2"]])
|
||||
});
|
||||
|
||||
doc.querySelectorAll('h3').forEach(e => {
|
||||
e.setAttribute('class', 'text-lg font-semibold mb-4')
|
||||
})
|
||||
|
||||
doc.querySelectorAll('p').forEach(e => {
|
||||
if (section === "segment") {
|
||||
setClass(e, 'text-gray-700 leading-relaxed')
|
||||
} else {
|
||||
setClass(e, 'mb-4');
|
||||
}
|
||||
});
|
||||
|
||||
doc.querySelectorAll('label').forEach(e => {
|
||||
if (section === "additional") {
|
||||
setClass(e, 'font-semibold');
|
||||
} else {
|
||||
setClass(e, 'min-w-[16px] mr-1 font-semibold');
|
||||
}
|
||||
});
|
||||
|
||||
doc.querySelectorAll('ul').forEach(e => {
|
||||
const hasLabel = Array.from(e.querySelectorAll('li')).some(li => li.querySelector('label'));
|
||||
if (hasLabel) {
|
||||
e.setAttribute('class', 'list-none space-y-2');
|
||||
} else {
|
||||
e.setAttribute('class', `list-disc pl-5 space-y-2`);
|
||||
}
|
||||
});
|
||||
|
||||
doc.querySelectorAll('ol').forEach(e => {
|
||||
e.setAttribute('class', 'list-decimal pl-5 space-y-2');
|
||||
})
|
||||
|
||||
doc.querySelectorAll('hz-row').forEach(e => {
|
||||
e.setAttribute('class', `flex flex-row items-center mb-4 gap-2`);
|
||||
})
|
||||
|
||||
if (section === "segment") {
|
||||
doc.querySelectorAll('b').forEach(e => {
|
||||
e.setAttribute('class', `text-${color}-700`);
|
||||
});
|
||||
}
|
||||
|
||||
doc.querySelectorAll('section').forEach(e => {
|
||||
e.setAttribute('class', `mb-8`);
|
||||
});
|
||||
|
||||
doc.querySelectorAll('option-box').forEach(e => {
|
||||
e.setAttribute('class', `flex justify-center min-w-[32px] min-h-6 bg-gray-200 rounded`);
|
||||
});
|
||||
|
||||
doc.querySelectorAll('option-box-grow').forEach(e => {
|
||||
e.setAttribute('class', 'flex flex-grow ml-2 w-10 min-h-6 bg-gray-200 rounded px-4 py-2');
|
||||
})
|
||||
|
||||
doc.querySelectorAll('option-box-blank').forEach(e => {
|
||||
e.setAttribute('class', 'min-w-[32px] min-h-[32px] border border-gray-300 text-center mr-3 flex justify-center items-center');
|
||||
})
|
||||
|
||||
doc.querySelectorAll('option-card').forEach(e => {
|
||||
e.setAttribute('class', 'bg-gray-100 rounded-lg flex flex-col p-4')
|
||||
})
|
||||
|
||||
doc.querySelectorAll('footer').forEach(e => {
|
||||
e.setAttribute('class', `flex flex-col gap-2 text-sm`);
|
||||
});
|
||||
|
||||
doc.querySelectorAll('single-line').forEach(e => {
|
||||
e.setAttribute('class', `border-b border-black w-full h-4 inline-block`);
|
||||
})
|
||||
|
||||
doc.querySelectorAll('padded-line').forEach(e => {
|
||||
e.setAttribute('class', `my-2 inline-block w-full`);
|
||||
})
|
||||
|
||||
doc.querySelectorAll('table').forEach(table => {
|
||||
table.setAttribute('class', 'min-w-full bg-white border border-gray-300')
|
||||
|
||||
table.querySelectorAll('thead tr').forEach(tr => {
|
||||
tr.setAttribute('class', 'bg-gray-100');
|
||||
});
|
||||
|
||||
table.querySelectorAll('th').forEach(th => {
|
||||
th.setAttribute('class', 'py-2 px-4 border-b font-semibold text-left');
|
||||
});
|
||||
|
||||
table.querySelectorAll('tbody tr').forEach((tr, index) => {
|
||||
if (index % 2 === 1) {
|
||||
tr.setAttribute('class', 'bg-gray-50');
|
||||
}
|
||||
});
|
||||
|
||||
table.querySelectorAll('td').forEach(td => {
|
||||
if (td === td.parentElement?.firstElementChild) {
|
||||
td.setAttribute('class', 'py-2 px-4 border-b font-medium');
|
||||
} else {
|
||||
td.setAttribute('class', 'py-2 px-4 border-b');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
doc.querySelectorAll('blockquote').forEach(e => {
|
||||
setClass(e, `flex w-full justify-center ${section === "segment" ? "" : "mb-4"}`)
|
||||
})
|
||||
|
||||
doc.querySelectorAll('items-between').forEach(e => {
|
||||
setClass(e, 'flex flex-row justify-between mb-4')
|
||||
})
|
||||
|
||||
return doc.body.innerHTML;
|
||||
}
|
||||
|
||||
const formatTip = (tip: ITrainingTip): ITrainingTip => {
|
||||
if (tip.exercise && tip.exercise.segments) {
|
||||
const colors = getRandomColors(tip.exercise.segments.length);
|
||||
|
||||
const processedSegments: WalkthroughConfigs[] = tip.exercise.segments.map((segment, index) => ({
|
||||
...segment,
|
||||
html: processHtml("segment", segment.html, colors[index])
|
||||
}));
|
||||
|
||||
return {
|
||||
id: tip.id,
|
||||
tipCategory: tip.tipCategory,
|
||||
tipHtml: processHtml("tip", tip.tipHtml, ""),
|
||||
standalone: tip.standalone,
|
||||
exercise: {
|
||||
question: processHtml("question", tip.exercise.question, ""),
|
||||
additional: tip.exercise.additional ? processHtml("additional", tip.exercise.additional, "") : undefined,
|
||||
segments: processedSegments
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: tip.id,
|
||||
tipCategory: tip.tipCategory,
|
||||
tipHtml: processHtml("tip", tip.tipHtml, ""),
|
||||
standalone: tip.standalone,
|
||||
exercise: undefined
|
||||
};
|
||||
};
|
||||
|
||||
export default formatTip;
|
||||
83
src/components/TrainingContent/Tip.tsx
Normal file
83
src/components/TrainingContent/Tip.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { FaChessKnight, FaLink, FaPen } from 'react-icons/fa';
|
||||
import { IoLanguage } from 'react-icons/io5';
|
||||
import { MdOutlineCategory } from 'react-icons/md';
|
||||
import { GiSkills } from 'react-icons/gi';
|
||||
import { BiBookReader } from 'react-icons/bi';
|
||||
|
||||
type CategoryConfig = {
|
||||
[key: string]: {
|
||||
headerColor: string;
|
||||
bodyColor: string;
|
||||
textColor: string;
|
||||
icon: any;
|
||||
}
|
||||
};
|
||||
|
||||
const categoryConfig : CategoryConfig = {
|
||||
'Strategy': {
|
||||
headerColor: 'bg-yellow-400',
|
||||
bodyColor: 'bg-yellow-100',
|
||||
textColor: 'text-yellow-900',
|
||||
icon: FaChessKnight
|
||||
},
|
||||
'Word Partners': {
|
||||
headerColor: 'bg-purple-700',
|
||||
bodyColor: 'bg-purple-200',
|
||||
textColor: 'text-purple-900',
|
||||
icon: MdOutlineCategory
|
||||
},
|
||||
'Word Link': {
|
||||
headerColor: 'bg-green-600',
|
||||
bodyColor: 'bg-green-100',
|
||||
textColor: 'text-green-900',
|
||||
icon: FaLink
|
||||
},
|
||||
'CT Focus': {
|
||||
headerColor: 'bg-purple-700',
|
||||
bodyColor: 'bg-purple-200',
|
||||
textColor: 'text-purple-900',
|
||||
icon: GiSkills
|
||||
},
|
||||
'Reading Skill': {
|
||||
headerColor: 'bg-orange-200',
|
||||
bodyColor: 'bg-orange-100',
|
||||
textColor: 'text-orange-900',
|
||||
icon: BiBookReader
|
||||
},
|
||||
'Language for Writing': {
|
||||
headerColor: 'bg-orange-200',
|
||||
bodyColor: 'bg-orange-100',
|
||||
textColor: 'text-orange-900',
|
||||
icon: IoLanguage
|
||||
},
|
||||
'Writing Skill': {
|
||||
headerColor: 'bg-orange-200',
|
||||
bodyColor: 'bg-orange-100',
|
||||
textColor: 'text-orange-900',
|
||||
icon: FaPen
|
||||
}
|
||||
};
|
||||
|
||||
const Tip: React.FC<{ category: string; html: string }> = ({ category, html }) => {
|
||||
|
||||
const { headerColor, bodyColor, textColor, icon: Icon } = useMemo(() =>
|
||||
categoryConfig[category] || categoryConfig['Strategy'], [category]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg overflow-hidden shadow-md mb-4">
|
||||
<div className={`px-4 py-3 ${headerColor}`}>
|
||||
<h2 className="font-bold text-white text-xl flex items-center">
|
||||
<Icon className="ml-2 mr-2" size={24} />
|
||||
{category === "CT Focus" ? "Critical Thinking" : category}
|
||||
</h2>
|
||||
</div>
|
||||
<div className={`p-6 ${bodyColor}`}>
|
||||
<p className={`text-lg ${textColor}`} dangerouslySetInnerHTML={{ __html: html }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tip;
|
||||
@@ -29,7 +29,7 @@ export interface ITrainingTip {
|
||||
standalone: boolean;
|
||||
exercise?: {
|
||||
question: string;
|
||||
highlightable: string;
|
||||
additional?: string;
|
||||
segments: WalkthroughConfigs[]
|
||||
}
|
||||
}
|
||||
@@ -38,16 +38,31 @@ export interface WalkthroughConfigs {
|
||||
html: string;
|
||||
wordDelay: number;
|
||||
holdDelay: number;
|
||||
highlight: string[];
|
||||
highlight?: HighlightConfig[];
|
||||
insertHTML?: InsertHtmlConfig[];
|
||||
}
|
||||
|
||||
export type HighlightTarget = 'question' | 'additional' | 'segment' | 'all';
|
||||
|
||||
export interface HighlightConfig {
|
||||
targets: HighlightTarget[];
|
||||
phrases: string[];
|
||||
}
|
||||
|
||||
export interface InsertHtmlConfig {
|
||||
target: 'question' | 'additional' | 'segment';
|
||||
targetId: string;
|
||||
html: string;
|
||||
position: 'append' | 'prepend' | 'replace';
|
||||
}
|
||||
|
||||
|
||||
export interface TimelineEvent {
|
||||
type: 'text' | 'highlight';
|
||||
type: 'text' | 'highlight' | 'insert';
|
||||
start: number;
|
||||
end: number;
|
||||
segmentIndex: number;
|
||||
content?: string[];
|
||||
content?: HighlightConfig[] | InsertHtmlConfig[];
|
||||
}
|
||||
|
||||
export interface SegmentRef extends WalkthroughConfigs {
|
||||
|
||||
@@ -10,6 +10,7 @@ import clsx from "clsx";
|
||||
import Exercise from "@/training/Exercise";
|
||||
import TrainingScore from "@/training/TrainingScore";
|
||||
import {ITrainingContent, ITrainingTip} from "@/training/TrainingInterfaces";
|
||||
import formatTip from "@/training/FormatTip";
|
||||
import {Stat, User} from "@/interfaces/user";
|
||||
import Head from "next/head";
|
||||
import Layout from "@/components/High/Layout";
|
||||
@@ -105,7 +106,9 @@ const TrainingContent: React.FC<{user: User}> = ({user}) => {
|
||||
params: {ids: trainingContent.tip_ids},
|
||||
paramsSerializer: (params) => qs.stringify(params, {arrayFormat: "repeat"}),
|
||||
});
|
||||
setTrainingTips(tips.data);
|
||||
|
||||
const processedTips = tips.data.map(formatTip);
|
||||
setTrainingTips(processedTips);
|
||||
setTrainingContent(withExamsStats);
|
||||
} catch (error) {
|
||||
router.push("/training");
|
||||
@@ -338,7 +341,7 @@ const TrainingContent: React.FC<{user: User}> = ({user}) => {
|
||||
</div>
|
||||
<div className="flex -md:hidden">
|
||||
<div className="rounded-3xl p-6 shadow-training-inset w-full">
|
||||
<div className="flex flex-col p-10">
|
||||
<div className="flex flex-col">
|
||||
<Exercise key={currentTipIndex} {...trainingTips[currentTipIndex]} />
|
||||
</div>
|
||||
<div className="self-end flex justify-between w-full gap-8 bottom-8 left-0 px-8">
|
||||
|
||||
Reference in New Issue
Block a user