feat: add shareable lesson links with URL routing
- Add share button with SVG link icon in lesson title row - Create share dialog with copy URL functionality - Implement URL hash-based routing for lesson navigation - Support browser back/forward navigation - Add i18n translations for share dialog in all languages - Position share button between title and completion badge - Add RTL support for title row layout 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
164
src/app.js
164
src/app.js
@@ -3,6 +3,7 @@ import { CodeEditor } from "./impl/CodeEditor.js";
|
|||||||
import { renderLesson, renderModuleList, renderLevelIndicator, updateActiveLessonInSidebar } from "./helpers/renderer.js";
|
import { renderLesson, renderModuleList, renderLevelIndicator, updateActiveLessonInSidebar } from "./helpers/renderer.js";
|
||||||
import { loadModules } from "./config/lessons.js";
|
import { loadModules } from "./config/lessons.js";
|
||||||
import { initI18n, t, getLanguage, setLanguage, applyTranslations } from "./i18n.js";
|
import { initI18n, t, getLanguage, setLanguage, applyTranslations } from "./i18n.js";
|
||||||
|
import { parseHash, updateHash, replaceHash, getShareableUrl } from "./helpers/router.js";
|
||||||
|
|
||||||
// Simplified state - LessonEngine now manages lesson state and progress
|
// Simplified state - LessonEngine now manages lesson state and progress
|
||||||
const state = {
|
const state = {
|
||||||
@@ -28,6 +29,7 @@ const elements = {
|
|||||||
modulePill: document.getElementById("module-pill"),
|
modulePill: document.getElementById("module-pill"),
|
||||||
moduleName: document.querySelector(".module-name"),
|
moduleName: document.querySelector(".module-name"),
|
||||||
lessonTitle: document.getElementById("lesson-title"),
|
lessonTitle: document.getElementById("lesson-title"),
|
||||||
|
lessonTitleRow: document.querySelector(".lesson-title-row"),
|
||||||
lessonDescription: document.getElementById("lesson-description"),
|
lessonDescription: document.getElementById("lesson-description"),
|
||||||
taskInstruction: document.getElementById("task-instruction"),
|
taskInstruction: document.getElementById("task-instruction"),
|
||||||
codeInput: document.getElementById("code-input"),
|
codeInput: document.getElementById("code-input"),
|
||||||
@@ -71,7 +73,15 @@ const elements = {
|
|||||||
resetCodeDialogClose: document.getElementById("reset-code-dialog-close"),
|
resetCodeDialogClose: document.getElementById("reset-code-dialog-close"),
|
||||||
cancelResetCode: document.getElementById("cancel-reset-code"),
|
cancelResetCode: document.getElementById("cancel-reset-code"),
|
||||||
confirmResetCode: document.getElementById("confirm-reset-code"),
|
confirmResetCode: document.getElementById("confirm-reset-code"),
|
||||||
resetCodeDontShow: document.getElementById("reset-code-dont-show")
|
resetCodeDontShow: document.getElementById("reset-code-dont-show"),
|
||||||
|
|
||||||
|
// Share dialog
|
||||||
|
shareBtn: document.getElementById("share-btn"),
|
||||||
|
shareDialog: document.getElementById("share-dialog"),
|
||||||
|
shareDialogClose: document.getElementById("share-dialog-close"),
|
||||||
|
shareUrlInput: document.getElementById("share-url-input"),
|
||||||
|
copyUrlBtn: document.getElementById("copy-url-btn"),
|
||||||
|
copyFeedback: document.getElementById("copy-feedback")
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize the lesson engine - now the single source of truth
|
// Initialize the lesson engine - now the single source of truth
|
||||||
@@ -283,14 +293,22 @@ function initializeModules() {
|
|||||||
// Use the new renderModuleList function with both callbacks
|
// Use the new renderModuleList function with both callbacks
|
||||||
renderModuleList(elements.moduleList, modules, selectModule, selectLesson);
|
renderModuleList(elements.moduleList, modules, selectModule, selectLesson);
|
||||||
|
|
||||||
// Load saved progress and select appropriate module
|
// Check URL first for shareable links
|
||||||
const progressData = lessonEngine.loadUserProgress();
|
const urlState = parseHash();
|
||||||
const lastModuleId = progressData?.lastModuleId;
|
|
||||||
|
|
||||||
if (lastModuleId && modules.find((m) => m.id === lastModuleId)) {
|
if (urlState) {
|
||||||
selectModule(lastModuleId);
|
// URL takes priority - navigate to specified lesson
|
||||||
} else if (modules.length > 0) {
|
navigateToLesson(urlState.moduleId, urlState.lessonIndex, false);
|
||||||
selectModule(modules[0].id);
|
} else {
|
||||||
|
// No URL - use saved progress (existing logic)
|
||||||
|
const progressData = lessonEngine.loadUserProgress();
|
||||||
|
const lastModuleId = progressData?.lastModuleId;
|
||||||
|
|
||||||
|
if (lastModuleId && modules.find((m) => m.id === lastModuleId)) {
|
||||||
|
selectModule(lastModuleId);
|
||||||
|
} else if (modules.length > 0) {
|
||||||
|
selectModule(modules[0].id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProgressDisplay();
|
updateProgressDisplay();
|
||||||
@@ -307,6 +325,10 @@ function selectModule(moduleId) {
|
|||||||
const success = lessonEngine.setModuleById(moduleId);
|
const success = lessonEngine.setModuleById(moduleId);
|
||||||
if (!success) return;
|
if (!success) return;
|
||||||
|
|
||||||
|
// Update URL
|
||||||
|
const engineState = lessonEngine.getCurrentState();
|
||||||
|
updateHash(moduleId, engineState.lessonIndex);
|
||||||
|
|
||||||
// Update module list UI to highlight the active module
|
// Update module list UI to highlight the active module
|
||||||
const moduleItems = elements.moduleList.querySelectorAll(".module-header");
|
const moduleItems = elements.moduleList.querySelectorAll(".module-header");
|
||||||
moduleItems.forEach((item) => {
|
moduleItems.forEach((item) => {
|
||||||
@@ -332,6 +354,10 @@ function selectLesson(moduleId, lessonIndex) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lessonEngine.setLessonByIndex(lessonIndex);
|
lessonEngine.setLessonByIndex(lessonIndex);
|
||||||
|
|
||||||
|
// Update URL
|
||||||
|
updateHash(moduleId, lessonIndex);
|
||||||
|
|
||||||
loadCurrentLesson();
|
loadCurrentLesson();
|
||||||
|
|
||||||
// Close sidebar after selection on mobile
|
// Close sidebar after selection on mobile
|
||||||
@@ -463,7 +489,7 @@ function loadCurrentLesson() {
|
|||||||
const badge = document.createElement("span");
|
const badge = document.createElement("span");
|
||||||
badge.className = "completion-badge";
|
badge.className = "completion-badge";
|
||||||
badge.textContent = t("completed");
|
badge.textContent = t("completed");
|
||||||
elements.lessonTitle.appendChild(badge);
|
elements.lessonTitleRow.appendChild(badge);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show gradient border for completed lessons
|
// Show gradient border for completed lessons
|
||||||
@@ -556,9 +582,12 @@ function nextLesson() {
|
|||||||
const prevModuleId = lessonEngine.getCurrentState().module?.id;
|
const prevModuleId = lessonEngine.getCurrentState().module?.id;
|
||||||
const success = lessonEngine.nextLesson();
|
const success = lessonEngine.nextLesson();
|
||||||
if (success) {
|
if (success) {
|
||||||
const newModuleId = lessonEngine.getCurrentState().module?.id;
|
const newState = lessonEngine.getCurrentState();
|
||||||
if (newModuleId !== prevModuleId) {
|
// Update URL
|
||||||
updateModuleHighlight(newModuleId);
|
updateHash(newState.module.id, newState.lessonIndex);
|
||||||
|
|
||||||
|
if (newState.module.id !== prevModuleId) {
|
||||||
|
updateModuleHighlight(newState.module.id);
|
||||||
}
|
}
|
||||||
loadCurrentLesson();
|
loadCurrentLesson();
|
||||||
}
|
}
|
||||||
@@ -568,9 +597,12 @@ function prevLesson() {
|
|||||||
const prevModuleId = lessonEngine.getCurrentState().module?.id;
|
const prevModuleId = lessonEngine.getCurrentState().module?.id;
|
||||||
const success = lessonEngine.previousLesson();
|
const success = lessonEngine.previousLesson();
|
||||||
if (success) {
|
if (success) {
|
||||||
const newModuleId = lessonEngine.getCurrentState().module?.id;
|
const newState = lessonEngine.getCurrentState();
|
||||||
if (newModuleId !== prevModuleId) {
|
// Update URL
|
||||||
updateModuleHighlight(newModuleId);
|
updateHash(newState.module.id, newState.lessonIndex);
|
||||||
|
|
||||||
|
if (newState.module.id !== prevModuleId) {
|
||||||
|
updateModuleHighlight(newState.module.id);
|
||||||
}
|
}
|
||||||
loadCurrentLesson();
|
loadCurrentLesson();
|
||||||
}
|
}
|
||||||
@@ -636,7 +668,7 @@ function runCode() {
|
|||||||
const badge = document.createElement("span");
|
const badge = document.createElement("span");
|
||||||
badge.className = "completion-badge";
|
badge.className = "completion-badge";
|
||||||
badge.textContent = t("completed");
|
badge.textContent = t("completed");
|
||||||
elements.lessonTitle.appendChild(badge);
|
elements.lessonTitleRow.appendChild(badge);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add success visual indicators
|
// Add success visual indicators
|
||||||
@@ -748,6 +780,93 @@ function handleResetCodeClick() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================= SHARE DIALOG =================
|
||||||
|
|
||||||
|
function showShareDialog() {
|
||||||
|
const engineState = lessonEngine.getCurrentState();
|
||||||
|
if (engineState.module && engineState.lesson !== null) {
|
||||||
|
const shareUrl = getShareableUrl(engineState.module.id, engineState.lessonIndex);
|
||||||
|
elements.shareUrlInput.value = shareUrl;
|
||||||
|
elements.copyFeedback.hidden = true;
|
||||||
|
}
|
||||||
|
elements.shareDialog.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeShareDialog() {
|
||||||
|
elements.shareDialog.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyShareUrl() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(elements.shareUrlInput.value);
|
||||||
|
elements.copyFeedback.hidden = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
elements.copyFeedback.hidden = true;
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback for older browsers
|
||||||
|
elements.shareUrlInput.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
elements.copyFeedback.hidden = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
elements.copyFeedback.hidden = true;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= URL ROUTING =================
|
||||||
|
|
||||||
|
function initRouter() {
|
||||||
|
// Handle browser back/forward
|
||||||
|
window.addEventListener("popstate", handlePopState);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePopState() {
|
||||||
|
const parsed = parseHash();
|
||||||
|
if (parsed) {
|
||||||
|
navigateToLesson(parsed.moduleId, parsed.lessonIndex, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateToLesson(moduleId, lessonIndex, shouldUpdateUrl = true) {
|
||||||
|
// Validate moduleId exists
|
||||||
|
const module = lessonEngine.modules.find((m) => m.id === moduleId);
|
||||||
|
if (!module) {
|
||||||
|
// Invalid module - fallback to first module
|
||||||
|
const fallbackModule = lessonEngine.modules[0];
|
||||||
|
if (fallbackModule) {
|
||||||
|
replaceHash(fallbackModule.id, 0);
|
||||||
|
lessonEngine.setModuleById(fallbackModule.id);
|
||||||
|
lessonEngine.setLessonByIndex(0);
|
||||||
|
loadCurrentLesson();
|
||||||
|
updateModuleHighlight(fallbackModule.id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate lessonIndex is in bounds
|
||||||
|
if (lessonIndex < 0 || lessonIndex >= module.lessons.length) {
|
||||||
|
// Invalid lesson - go to first lesson of module
|
||||||
|
replaceHash(moduleId, 0);
|
||||||
|
lessonEngine.setModuleById(moduleId);
|
||||||
|
lessonEngine.setLessonByIndex(0);
|
||||||
|
loadCurrentLesson();
|
||||||
|
updateModuleHighlight(moduleId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid navigation
|
||||||
|
lessonEngine.setModuleById(moduleId);
|
||||||
|
lessonEngine.setLessonByIndex(lessonIndex);
|
||||||
|
|
||||||
|
if (shouldUpdateUrl) {
|
||||||
|
updateHash(moduleId, lessonIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadCurrentLesson();
|
||||||
|
updateModuleHighlight(moduleId);
|
||||||
|
}
|
||||||
|
|
||||||
// ================= INITIALIZATION =================
|
// ================= INITIALIZATION =================
|
||||||
|
|
||||||
function initCodeEditor() {
|
function initCodeEditor() {
|
||||||
@@ -788,6 +907,9 @@ function init() {
|
|||||||
// Load modules after editor is ready
|
// Load modules after editor is ready
|
||||||
initializeModules();
|
initializeModules();
|
||||||
|
|
||||||
|
// Initialize URL router for shareable links
|
||||||
|
initRouter();
|
||||||
|
|
||||||
// Sidebar controls
|
// Sidebar controls
|
||||||
elements.menuBtn.addEventListener("click", openSidebar);
|
elements.menuBtn.addEventListener("click", openSidebar);
|
||||||
elements.closeSidebar.addEventListener("click", closeSidebar);
|
elements.closeSidebar.addEventListener("click", closeSidebar);
|
||||||
@@ -797,7 +919,9 @@ function init() {
|
|||||||
elements.logoLink.addEventListener("click", (e) => {
|
elements.logoLink.addEventListener("click", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
lessonEngine.setModuleById("welcome");
|
lessonEngine.setModuleById("welcome");
|
||||||
|
updateHash("welcome", 0);
|
||||||
loadCurrentLesson();
|
loadCurrentLesson();
|
||||||
|
updateModuleHighlight("welcome");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Language select
|
// Language select
|
||||||
@@ -820,6 +944,7 @@ function init() {
|
|||||||
if (codeEditor) codeEditor.redo();
|
if (codeEditor) codeEditor.redo();
|
||||||
});
|
});
|
||||||
elements.resetCodeBtn.addEventListener("click", handleResetCodeClick);
|
elements.resetCodeBtn.addEventListener("click", handleResetCodeClick);
|
||||||
|
elements.shareBtn.addEventListener("click", showShareDialog);
|
||||||
|
|
||||||
// Dialogs
|
// Dialogs
|
||||||
elements.helpBtn.addEventListener("click", showHelp);
|
elements.helpBtn.addEventListener("click", showHelp);
|
||||||
@@ -841,6 +966,13 @@ function init() {
|
|||||||
elements.cancelResetCode.addEventListener("click", closeResetCodeDialog);
|
elements.cancelResetCode.addEventListener("click", closeResetCodeDialog);
|
||||||
elements.confirmResetCode.addEventListener("click", handleResetCodeConfirm);
|
elements.confirmResetCode.addEventListener("click", handleResetCodeConfirm);
|
||||||
|
|
||||||
|
// Share dialog
|
||||||
|
elements.shareDialogClose.addEventListener("click", closeShareDialog);
|
||||||
|
elements.shareDialog.addEventListener("click", (e) => {
|
||||||
|
if (e.target === elements.shareDialog) closeShareDialog();
|
||||||
|
});
|
||||||
|
elements.copyUrlBtn.addEventListener("click", copyShareUrl);
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
elements.disableFeedbackToggle.addEventListener("change", (e) => {
|
elements.disableFeedbackToggle.addEventListener("change", (e) => {
|
||||||
state.userSettings.disableFeedbackErrors = !e.target.checked;
|
state.userSettings.disableFeedbackErrors = !e.target.checked;
|
||||||
|
|||||||
57
src/helpers/router.js
Normal file
57
src/helpers/router.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* URL Router for Code Crispies
|
||||||
|
* Handles hash-based routing for shareable lesson links
|
||||||
|
* Format: #module-id/lesson-index (e.g., #flexbox/2)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse current URL hash into module and lesson info
|
||||||
|
* @returns {{ moduleId: string, lessonIndex: number } | null}
|
||||||
|
*/
|
||||||
|
export function parseHash() {
|
||||||
|
const hash = window.location.hash.slice(1); // Remove '#'
|
||||||
|
if (!hash) return null;
|
||||||
|
|
||||||
|
const parts = hash.split("/");
|
||||||
|
if (parts.length !== 2) return null;
|
||||||
|
|
||||||
|
const moduleId = parts[0];
|
||||||
|
const lessonIndex = parseInt(parts[1], 10);
|
||||||
|
|
||||||
|
if (!moduleId || isNaN(lessonIndex) || lessonIndex < 0) return null;
|
||||||
|
|
||||||
|
return { moduleId, lessonIndex };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update URL hash with history entry (for navigation)
|
||||||
|
* @param {string} moduleId
|
||||||
|
* @param {number} lessonIndex
|
||||||
|
*/
|
||||||
|
export function updateHash(moduleId, lessonIndex) {
|
||||||
|
const newHash = `#${moduleId}/${lessonIndex}`;
|
||||||
|
if (window.location.hash !== newHash) {
|
||||||
|
history.pushState(null, "", newHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace URL hash without history entry (for invalid URL fallbacks)
|
||||||
|
* @param {string} moduleId
|
||||||
|
* @param {number} lessonIndex
|
||||||
|
*/
|
||||||
|
export function replaceHash(moduleId, lessonIndex) {
|
||||||
|
const newHash = `#${moduleId}/${lessonIndex}`;
|
||||||
|
history.replaceState(null, "", newHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build full shareable URL for current lesson
|
||||||
|
* @param {string} moduleId
|
||||||
|
* @param {number} lessonIndex
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getShareableUrl(moduleId, lessonIndex) {
|
||||||
|
const base = window.location.origin + window.location.pathname;
|
||||||
|
return `${base}#${moduleId}/${lessonIndex}`;
|
||||||
|
}
|
||||||
42
src/i18n.js
42
src/i18n.js
@@ -99,6 +99,13 @@ const translations = {
|
|||||||
dontShowAgain: "Don't show this again",
|
dontShowAgain: "Don't show this again",
|
||||||
reset: "Reset",
|
reset: "Reset",
|
||||||
|
|
||||||
|
// Share dialog
|
||||||
|
shareDialogTitle: "Share Lesson",
|
||||||
|
shareDialogText: "Copy this URL to share the current lesson:",
|
||||||
|
shareTitle: "Share lesson",
|
||||||
|
copyUrl: "Copy",
|
||||||
|
urlCopied: "URL copied to clipboard!",
|
||||||
|
|
||||||
// Dynamic content
|
// Dynamic content
|
||||||
loadingFallbackText: "Could not load lesson. Please select one from the menu or check the help.",
|
loadingFallbackText: "Could not load lesson. Please select one from the menu or check the help.",
|
||||||
completed: "Completed",
|
completed: "Completed",
|
||||||
@@ -207,6 +214,13 @@ const translations = {
|
|||||||
dontShowAgain: "Nicht mehr anzeigen",
|
dontShowAgain: "Nicht mehr anzeigen",
|
||||||
reset: "Zurücksetzen",
|
reset: "Zurücksetzen",
|
||||||
|
|
||||||
|
// Share dialog
|
||||||
|
shareDialogTitle: "Lektion teilen",
|
||||||
|
shareDialogText: "Kopiere diese URL, um die aktuelle Lektion zu teilen:",
|
||||||
|
shareTitle: "Lektion teilen",
|
||||||
|
copyUrl: "Kopieren",
|
||||||
|
urlCopied: "URL in die Zwischenablage kopiert!",
|
||||||
|
|
||||||
// Dynamic content
|
// Dynamic content
|
||||||
loadingFallbackText: "Lektion konnte nicht geladen werden. Bitte wähle eine aus dem Menü oder prüfe die Hilfe.",
|
loadingFallbackText: "Lektion konnte nicht geladen werden. Bitte wähle eine aus dem Menü oder prüfe die Hilfe.",
|
||||||
completed: "Erledigt",
|
completed: "Erledigt",
|
||||||
@@ -315,6 +329,13 @@ const translations = {
|
|||||||
dontShowAgain: "Nie pokazuj ponownie",
|
dontShowAgain: "Nie pokazuj ponownie",
|
||||||
reset: "Resetuj",
|
reset: "Resetuj",
|
||||||
|
|
||||||
|
// Share dialog
|
||||||
|
shareDialogTitle: "Udostępnij lekcję",
|
||||||
|
shareDialogText: "Skopiuj ten URL, aby udostępnić bieżącą lekcję:",
|
||||||
|
shareTitle: "Udostępnij lekcję",
|
||||||
|
copyUrl: "Kopiuj",
|
||||||
|
urlCopied: "URL skopiowany do schowka!",
|
||||||
|
|
||||||
// Dynamic content
|
// Dynamic content
|
||||||
loadingFallbackText: "Nie można załadować lekcji. Wybierz jedną z menu lub sprawdź pomoc.",
|
loadingFallbackText: "Nie można załadować lekcji. Wybierz jedną z menu lub sprawdź pomoc.",
|
||||||
completed: "Ukończono",
|
completed: "Ukończono",
|
||||||
@@ -424,6 +445,13 @@ const translations = {
|
|||||||
dontShowAgain: "No mostrar de nuevo",
|
dontShowAgain: "No mostrar de nuevo",
|
||||||
reset: "Reiniciar",
|
reset: "Reiniciar",
|
||||||
|
|
||||||
|
// Share dialog
|
||||||
|
shareDialogTitle: "Compartir lección",
|
||||||
|
shareDialogText: "Copia esta URL para compartir la lección actual:",
|
||||||
|
shareTitle: "Compartir lección",
|
||||||
|
copyUrl: "Copiar",
|
||||||
|
urlCopied: "¡URL copiada al portapapeles!",
|
||||||
|
|
||||||
// Dynamic content
|
// Dynamic content
|
||||||
loadingFallbackText: "No se pudo cargar la lección. Selecciona una del menú o consulta la ayuda.",
|
loadingFallbackText: "No se pudo cargar la lección. Selecciona una del menú o consulta la ayuda.",
|
||||||
completed: "Completado",
|
completed: "Completado",
|
||||||
@@ -531,6 +559,13 @@ const translations = {
|
|||||||
dontShowAgain: "لا تظهر هذا مرة أخرى",
|
dontShowAgain: "لا تظهر هذا مرة أخرى",
|
||||||
reset: "إعادة تعيين",
|
reset: "إعادة تعيين",
|
||||||
|
|
||||||
|
// Share dialog
|
||||||
|
shareDialogTitle: "مشاركة الدرس",
|
||||||
|
shareDialogText: "انسخ هذا الرابط لمشاركة الدرس الحالي:",
|
||||||
|
shareTitle: "مشاركة الدرس",
|
||||||
|
copyUrl: "نسخ",
|
||||||
|
urlCopied: "تم نسخ الرابط إلى الحافظة!",
|
||||||
|
|
||||||
// Dynamic content
|
// Dynamic content
|
||||||
loadingFallbackText: "تعذر تحميل الدرس. اختر واحدًا من القائمة أو تحقق من المساعدة.",
|
loadingFallbackText: "تعذر تحميل الدرس. اختر واحدًا من القائمة أو تحقق من المساعدة.",
|
||||||
completed: "مكتمل",
|
completed: "مكتمل",
|
||||||
@@ -639,6 +674,13 @@ const translations = {
|
|||||||
dontShowAgain: "Більше не показувати",
|
dontShowAgain: "Більше не показувати",
|
||||||
reset: "Скинути",
|
reset: "Скинути",
|
||||||
|
|
||||||
|
// Share dialog
|
||||||
|
shareDialogTitle: "Поділитися уроком",
|
||||||
|
shareDialogText: "Скопіюйте цю URL-адресу, щоб поділитися поточним уроком:",
|
||||||
|
shareTitle: "Поділитися уроком",
|
||||||
|
copyUrl: "Копіювати",
|
||||||
|
urlCopied: "URL-адресу скопійовано до буфера обміну!",
|
||||||
|
|
||||||
// Dynamic content
|
// Dynamic content
|
||||||
loadingFallbackText: "Не вдалося завантажити урок. Виберіть один з меню або перевірте допомогу.",
|
loadingFallbackText: "Не вдалося завантажити урок. Виберіть один з меню або перевірте допомогу.",
|
||||||
completed: "Завершено",
|
completed: "Завершено",
|
||||||
|
|||||||
@@ -34,7 +34,15 @@
|
|||||||
<!-- Left Panel: Instructions + Editor -->
|
<!-- Left Panel: Instructions + Editor -->
|
||||||
<div class="left-panel">
|
<div class="left-panel">
|
||||||
<section class="instructions">
|
<section class="instructions">
|
||||||
<h2 id="lesson-title"></h2>
|
<div class="lesson-title-row">
|
||||||
|
<h2 id="lesson-title"></h2>
|
||||||
|
<button id="share-btn" class="share-btn" data-i18n-title="shareTitle" title="Share lesson" aria-label="Share lesson">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
||||||
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="task-instruction" id="task-instruction"></div>
|
<div class="task-instruction" id="task-instruction"></div>
|
||||||
<div class="lesson-description" id="lesson-description"></div>
|
<div class="lesson-description" id="lesson-description"></div>
|
||||||
</section>
|
</section>
|
||||||
@@ -257,6 +265,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<!-- Share Dialog -->
|
||||||
|
<dialog id="share-dialog" class="dialog">
|
||||||
|
<div class="dialog-header">
|
||||||
|
<h3 data-i18n="shareDialogTitle">Share Lesson</h3>
|
||||||
|
<button id="share-dialog-close" class="dialog-close" aria-label="Close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-content">
|
||||||
|
<p data-i18n="shareDialogText">Copy this URL to share the current lesson:</p>
|
||||||
|
<div class="share-url-container">
|
||||||
|
<input type="text" id="share-url-input" class="share-url-input" readonly />
|
||||||
|
<button id="copy-url-btn" class="btn btn-primary" data-i18n="copyUrl">Copy</button>
|
||||||
|
</div>
|
||||||
|
<p id="copy-feedback" class="copy-feedback" data-i18n="urlCopied" hidden>URL copied to clipboard!</p>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="app.js"></script>
|
<script type="module" src="app.js"></script>
|
||||||
|
|||||||
128
src/main.css
128
src/main.css
@@ -257,7 +257,9 @@ kbd {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--light-text);
|
color: var(--light-text);
|
||||||
transition: color 0.2s, border-color 0.2s;
|
transition:
|
||||||
|
color 0.2s,
|
||||||
|
border-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.help-toggle:hover {
|
.help-toggle:hover {
|
||||||
@@ -356,15 +358,47 @@ kbd {
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lesson-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
#lesson-title {
|
#lesson-title {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
color: var(--primary-dark);
|
color: var(--primary-dark);
|
||||||
margin-bottom: var(--spacing-sm);
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--light-text);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
transition: color 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-btn:hover {
|
||||||
|
color: var(--primary-color);
|
||||||
|
background: var(--primary-bg-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-btn svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.completion-badge {
|
.completion-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 0.5rem;
|
|
||||||
padding: 0.15rem 0.5rem;
|
padding: 0.15rem 0.5rem;
|
||||||
background: linear-gradient(135deg, #9b59b6, #e040fb, #00bcd4, #7c4dff);
|
background: linear-gradient(135deg, #9b59b6, #e040fb, #00bcd4, #7c4dff);
|
||||||
color: white;
|
color: white;
|
||||||
@@ -584,14 +618,7 @@ kbd {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: var(--spacing-md);
|
inset: var(--spacing-md);
|
||||||
border-radius: var(--border-radius-md);
|
border-radius: var(--border-radius-md);
|
||||||
background: conic-gradient(
|
background: conic-gradient(from var(--border-angle), #9b59b6, #e040fb, #00bcd4, #7c4dff, #9b59b6);
|
||||||
from var(--border-angle),
|
|
||||||
#9b59b6,
|
|
||||||
#e040fb,
|
|
||||||
#00bcd4,
|
|
||||||
#7c4dff,
|
|
||||||
#9b59b6
|
|
||||||
);
|
|
||||||
filter: blur(30px);
|
filter: blur(30px);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
animation: spin-glow 3s ease-out forwards;
|
animation: spin-glow 3s ease-out forwards;
|
||||||
@@ -681,14 +708,7 @@ kbd {
|
|||||||
border: 6px solid transparent;
|
border: 6px solid transparent;
|
||||||
background:
|
background:
|
||||||
linear-gradient(var(--panel-bg), var(--panel-bg)) padding-box,
|
linear-gradient(var(--panel-bg), var(--panel-bg)) padding-box,
|
||||||
conic-gradient(
|
conic-gradient(from 0deg, #9b59b6, #e040fb, #00bcd4, #7c4dff, #9b59b6) border-box;
|
||||||
from 0deg,
|
|
||||||
#9b59b6,
|
|
||||||
#e040fb,
|
|
||||||
#00bcd4,
|
|
||||||
#7c4dff,
|
|
||||||
#9b59b6
|
|
||||||
) border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-wrapper.matched {
|
.preview-wrapper.matched {
|
||||||
@@ -696,19 +716,11 @@ kbd {
|
|||||||
border: 6px solid transparent;
|
border: 6px solid transparent;
|
||||||
background:
|
background:
|
||||||
linear-gradient(var(--panel-bg), var(--panel-bg)) padding-box,
|
linear-gradient(var(--panel-bg), var(--panel-bg)) padding-box,
|
||||||
conic-gradient(
|
conic-gradient(from var(--border-angle), #9b59b6, #e040fb, #00bcd4, #7c4dff, #9b59b6) border-box;
|
||||||
from var(--border-angle),
|
|
||||||
#9b59b6,
|
|
||||||
#e040fb,
|
|
||||||
#00bcd4,
|
|
||||||
#7c4dff,
|
|
||||||
#9b59b6
|
|
||||||
) border-box;
|
|
||||||
animation: spin-border 3s ease-out forwards;
|
animation: spin-border 3s ease-out forwards;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Animated CRISPY speech bubble with SVG tail */
|
/* Animated CRISPY speech bubble with SVG tail */
|
||||||
.preview-wrapper.matched::after {
|
.preview-wrapper.matched::after {
|
||||||
content: var(--crispy-quote, "Crispyyyyyy!");
|
content: var(--crispy-quote, "Crispyyyyyy!");
|
||||||
@@ -716,7 +728,10 @@ kbd {
|
|||||||
left: 55%;
|
left: 55%;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
transform: translateX(-50%) translateY(100%) scale(0.5);
|
transform: translateX(-50%) translateY(100%) scale(0.5);
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
font-family:
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
sans-serif;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
@@ -1080,7 +1095,10 @@ button.lesson-list-item {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: var(--font-main);
|
font-family: var(--font-main);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
transition: background 0.2s, color 0.2s, border-color 0.2s;
|
transition:
|
||||||
|
background 0.2s,
|
||||||
|
color 0.2s,
|
||||||
|
border-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:hover {
|
.btn:hover {
|
||||||
@@ -1319,7 +1337,9 @@ input:checked + .toggle-slider::before {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transition: background 0.2s, color 0.2s;
|
transition:
|
||||||
|
background 0.2s,
|
||||||
|
color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-close:hover {
|
.dialog-close:hover {
|
||||||
@@ -1370,6 +1390,39 @@ input:checked + .toggle-slider::before {
|
|||||||
margin-top: var(--spacing-lg);
|
margin-top: var(--spacing-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Share Dialog */
|
||||||
|
.share-url-container {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
margin: var(--spacing-md) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-url-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
font-family: var(--font-code);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
background: var(--code-bg);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-url-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-feedback {
|
||||||
|
color: var(--success-color);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-top: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir="rtl"] .share-url-container {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
/* Project Cards in Help Dialog */
|
/* Project Cards in Help Dialog */
|
||||||
.project-cards {
|
.project-cards {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1386,7 +1439,11 @@ input:checked + .toggle-slider::before {
|
|||||||
border: 1px solid var(--primary-bg-medium);
|
border: 1px solid var(--primary-bg-medium);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
transition: background 0.2s, border-color 0.2s, transform 0.2s, box-shadow 0.2s;
|
transition:
|
||||||
|
background 0.2s,
|
||||||
|
border-color 0.2s,
|
||||||
|
transform 0.2s,
|
||||||
|
box-shadow 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-card:hover {
|
.project-card:hover {
|
||||||
@@ -1706,10 +1763,9 @@ input:checked + .toggle-slider::before {
|
|||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* RTL: Completion badge spacing */
|
/* RTL: Lesson title row */
|
||||||
[dir="rtl"] .completion-badge {
|
[dir="rtl"] .lesson-title-row {
|
||||||
margin-left: 0;
|
flex-direction: row-reverse;
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* RTL: Lists - bullets/numbers on right side */
|
/* RTL: Lists - bullets/numbers on right side */
|
||||||
|
|||||||
Reference in New Issue
Block a user