Table of Contents
Miscellaneous
<pre><xmp>
<?php require_once __DIR__ . '/../partials/header.php'; ?>
<?php require_once __DIR__ . '/../partials/sidebar.php'; ?>
<link href="https://cdn.jsdelivr.net/npm/prismjs/themes/prism-okaidia.min.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Hind+Siliguri:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/prismjs/prism.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs/components/prism-java.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs/components/prism-php.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs/components/prism-javascript.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs/components/prism-markup.min.js"></script>
<script>if (window.Prism) {
Prism.highlightAll();
}</script>
<style>
:root{
--presentation-sidebar-width: 62px;
--presentation-panel-bg: var(--card-bg, var(--bs-body-bg, #ffffff));
--presentation-page-bg: var(--bg-color, var(--bs-body-bg, #f6f8fb));
--presentation-text: var(--text-color, var(--bs-body-color, #1f2937));
--presentation-text-soft: var(--text-color-secondary, #6b7280);
--presentation-border: var(--border-color, rgba(0,0,0,0.08));
--presentation-shadow: 0 12px 35px rgba(0,0,0,0.08);
--presentation-primary: var(--link-color, #0d6efd);
--presentation-primary-hover: var(--link-hover-color, #0b5ed7);
--presentation-sidebar-bg: var(--header-footer-bg, #111827);
--presentation-sidebar-text: #ffffff;
--presentation-success-bg: rgba(25, 135, 84, 0.10);
--presentation-danger-bg: rgba(220, 53, 69, 0.10);
--presentation-warning-bg: rgba(255, 193, 7, 0.12);
/*--presentation-gradient-1: linear-gradient(135deg, rgba(13,110,253,0.10), rgba(111,66,193,0.12));*/
--presentation-gradient-2: linear-gradient(135deg, rgb(63 100 53 / 92%), rgb(91 103 9 / 88%));
--presentation-font-scale: 1;
}
.presentation-stage-title-wrap h2,
.presentation-stage-subtitle,
.presentation-pointer-title,
.presentation-explanation-editor,
.presentation-mobile-sheet-body {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
}
.presentation-pointer-title pre,
.presentation-explanation-editor pre,
.presentation-mobile-sheet-body pre {
margin: 12px 0 0;
padding: 16px 18px;
border-radius: 14px;
overflow-x: auto;
border: 1px solid rgba(255,255,255,0.08);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
}
.presentation-pointer-title code,
.presentation-explanation-editor code,
.presentation-mobile-sheet-body code {
font-family: Consolas, Monaco, 'Fira Code', monospace;
font-size: 0.92rem;
line-height: 1.7;
}
.presentation-explanation-editor {
overflow: auto;
}
.presentation-explanation-editor pre[class*="language-"] {
margin: 0;
}
.presentation-pointer-title pre[class*="language-"],
.presentation-explanation-editor pre[class*="language-"],
.presentation-mobile-sheet-body pre[class*="language-"] {
background: transparent !important;
}
.presentation-stage::after {
content: "";
position: absolute;
inset: 0;
border-radius: 22px;
box-shadow: 0 0 0 1px rgba(13,110,253,0.08),
0 20px 60px rgba(13,110,253,0.12);
pointer-events: none;
}
.presentation-pointer-item.show {
animation: slideFadeIn 0.5s ease forwards;
}
@keyframes slideFadeIn {
from {
opacity: 0;
transform: translateY(25px) scale(0.96);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.presentation-pointer-list.focus-mode .presentation-pointer-item {
opacity: 0.3;
transform: scale(0.98);
}
.presentation-pointer-list.focus-mode .presentation-pointer-item.active {
opacity: 1;
transform: scale(1.02);
background: rgba(13,110,253,0.12);
}
.presentation-page-shell {
padding: 18px;
background: var(--presentation-page-bg);
color: var(--presentation-text);
min-height: calc(100vh - 100px);
}
.presentation-stage-actions {
position: sticky;
bottom: 10px;
background: rgba(255,255,255,0.85);
backdrop-filter: blur(10px);
border-radius: 14px;
padding: 8px 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.12);
display: none;
gap: 6px;
align-items: center;
flex-wrap: wrap;
justify-content: flex-end;
margin-left: auto;
}
.presentation-top-card {
background: var(--presentation-panel-bg);
border: 1px solid var(--presentation-border);
border-radius: 22px;
box-shadow: var(--presentation-shadow);
padding: 18px;
margin-bottom: 16px;
}
.presentation-page-shell.fullscreen-mode .presentation-top-card {
display: none !important;
}
.presentation-top-grid {
display: grid;
grid-template-columns: 1.5fr 1fr;
gap: 16px;
align-items: center;
}
.presentation-title {
font-size: 1.35rem;
font-weight: 800;
margin: 0 0 6px 0;
color: var(--presentation-text);
}
.presentation-subtitle {
margin: 0;
color: var(--presentation-text-soft);
font-size: 0.95rem;
line-height: 1.75;
}
.presentation-upload-wrap {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
justify-content: flex-end;
}
.presentation-file-input {
min-width: 220px;
max-width: 100%;
padding: 11px 12px;
border-radius: 12px;
border: 1px solid var(--presentation-border);
background: var(--presentation-panel-bg);
color: var(--presentation-text);
outline: none;
}
.presentation-btn {
border: 0;
outline: none;
border-radius: 12px;
padding: 11px 16px;
font-weight: 700;
cursor: pointer;
transition: all 0.22s ease;
box-shadow: 0 8px 18px rgba(0,0,0,0.06);
}
.presentation-btn-primary {
background: var(--presentation-primary);
color: #fff;
}
.presentation-btn-primary:hover {
background: var(--presentation-primary-hover);
transform: translateY(-1px);
}
.presentation-btn-light {
background: var(--presentation-panel-bg);
color: var(--presentation-text);
border: 1px solid var(--presentation-border);
}
.presentation-btn-light:hover {
transform: translateY(-1px);
}
.presentation-template-note {
margin-top: 10px;
color: var(--presentation-text-soft);
font-size: 0.86rem;
line-height: 1.7;
}
.presentation-feedback {
display: none;
margin-top: 12px;
border-radius: 12px;
padding: 11px 13px;
font-size: 0.9rem;
border: 1px solid transparent;
}
.presentation-feedback.show {
display: block;
}
.presentation-feedback.success {
background: var(--presentation-success-bg);
border-color: rgba(25, 135, 84, 0.18);
}
.presentation-feedback.error {
background: var(--presentation-danger-bg);
border-color: rgba(220, 53, 69, 0.18);
}
.presentation-feedback.warning {
background: var(--presentation-warning-bg);
border-color: rgba(255, 193, 7, 0.18);
}
.presentation-main {
display: grid;
grid-template-columns: var(--presentation-sidebar-width) minmax(0, 1fr);
gap: 14px;
align-items: stretch;
}
.presentation-sidebar {
background: var(--presentation-sidebar-bg);
color: var(--presentation-sidebar-text);
border-radius: 18px;
padding: 10px 8px;
min-height: 620px;
box-shadow: var(--presentation-shadow);
position: sticky;
top: 88px;
z-index: 9;
}
.presentation-sidebar-title {
text-align: center;
font-size: 0.72rem;
opacity: 0.8;
margin-bottom: 8px;
font-weight: 700;
letter-spacing: 0.3px;
}
.presentation-slide-nav {
display: flex;
flex-direction: column;
gap: 8px;
max-height: calc(100vh - 160px);
overflow-y: auto;
padding-right: 1px;
}
.presentation-slide-nav::-webkit-scrollbar {
width: 4px;
}
.presentation-slide-nav::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.18);
border-radius: 999px;
}
.presentation-slide-nav-btn {
width: 100%;
min-height: 38px;
border: 1px solid var(--border-color);
border-radius: 10px;
background: var(--hover-bg);
color: var(--text-color);
font-weight: 700;
font-size: 0.82rem;
padding: 8px 6px;
cursor: pointer;
transition: all 0.25s ease;
}
/* Hover + Active */
.presentation-slide-nav-btn:hover,
.presentation-slide-nav-btn.active {
background: var(--link-color);
color: #fff;
border-color: var(--link-color);
transform: translateY(-1px);
box-shadow: 0 4px 12px var(--shadow-color);
}
.presentation-stage-frame {
min-width: 0;
}
.presentation-workspace {
display: grid;
grid-template-columns: minmax(0, 1.42fr) minmax(320px, 0.58fr);
gap: 14px;
}
.presentation-stage,
.presentation-explanation-panel {
background: var(--presentation-panel-bg);
border: 1px solid var(--presentation-border);
border-radius: 22px;
box-shadow: var(--presentation-shadow);
}
.presentation-stage {
padding: 18px;
min-height: 620px;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
justify-content: flex-start;
background:
radial-gradient(circle at top right, rgba(13,110,253,0.10), transparent 35%),
radial-gradient(circle at bottom left, rgba(34,197,94,0.08), transparent 35%),
var(--presentation-panel-bg);
}
.presentation-stage::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 160px;
background: var(--presentation-gradient-1);
pointer-events: none;
}
.presentation-stage > * {
position: relative;
z-index: 1;
}
.presentation-stage-header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12px;
align-items: start;
margin-bottom: 30px;
padding-bottom: 22px;
border-bottom: 1px solid var(--presentation-border);
}
.presentation-stage-title-wrap {
min-width: 0;
}
.presentation-stage-title-wrap h2 {
font-size: 1.85rem;
margin: 0 0 1px 0;
font-weight: 900;
color: var(--presentation-text);
line-height: 1.25;
word-break: break-word;
letter-spacing: -0.02em;
}
.presentation-stage-subtitle {
color: var(--presentation-text-soft);
font-size: 1.08rem;
line-height: 1.1;
margin-bottom: 1px;
word-break: break-word;
font-weight: 600;
}
.presentation-stage-meta {
color: var(--presentation-text-soft);
font-size: 0.84rem;
}
.presentation-presentation-info {
display: none;
/*background: var(--presentation-gradient-2);*/
color: #183d22;
border-radius: 18px;
padding: 16px 18px;
margin-bottom: 16px;
box-shadow: 0 10px 24px rgba(13,110,253,0.15);
}
.presentation-presentation-info.show {
display: block;
}
.presentation-presentation-title {
font-size: 1.12rem;
font-weight: 900;
margin-bottom: 10px;
letter-spacing: 0.01em;
padding: 120px 90px 80px
}
.presentation-presentation-meta-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
position: relative;
z-index: 2;
}
.presentation-presentation-meta-card {
/*background: rgba(255,255,255,0.12);*/
/*border: 1px solid rgba(255,255,255,0.18);*/
border-radius: 14px;
padding: 10px 12px;
min-height: 68px;
backdrop-filter: blur(12px);
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.15);
}
.presentation-presentation-meta-label {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.08em;
opacity: 0.86;
margin-bottom: 4px;
font-weight: 700;
}
.presentation-presentation-meta-value {
font-size: 0.96rem;
line-height: 1.55;
font-weight: 700;
word-break: break-word;
}
.presentation-stage-actions.show {
display: flex;
}
.presentation-icon-btn {
width: 30px;
height: 30px;
min-width: 30px;
border-radius: 8px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.86rem;
border: 1px solid var(--presentation-border);
background: var(--presentation-panel-bg);
color: var(--presentation-text);
cursor: pointer;
transition: all 0.18s ease;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
.presentation-icon-btn:hover {
transform: translateY(-1px);
border-color: rgba(13,110,253,0.22);
color: var(--presentation-primary);
}
.presentation-icon-btn.primary {
background: var(--presentation-primary);
border-color: var(--presentation-primary);
color: #fff;
}
.presentation-icon-btn.primary:hover {
background: var(--presentation-primary-hover);
color: #fff;
}
.presentation-layout-mini-group {
display: none;
gap: 6px;
align-items: center;
margin-right: 4px;
}
.presentation-layout-mini-group.show {
display: flex;
}
.presentation-layout-mini-btn {
border: 1px solid var(--presentation-border);
background: var(--presentation-panel-bg);
color: var(--presentation-text);
border-radius: 8px;
width: 30px;
height: 30px;
min-width: 30px;
padding: 0;
font-size: 0.72rem;
font-weight: 900;
cursor: pointer;
transition: all 0.18s ease;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
.presentation-layout-mini-btn.active,
.presentation-layout-mini-btn:hover {
background: var(--presentation-primary);
color: #fff;
border-color: var(--presentation-primary);
}
.presentation-content-area {
display: flex;
flex-direction: column;
gap: 18px;
flex: 1;
min-height: 320px;
justify-content: flex-start;
}
.presentation-active-view {
display: grid;
gap: 14px;
min-height: 0;
flex: 1;
}
.presentation-active-view.layout-top {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.presentation-active-view.layout-left {
grid-template-columns: minmax(260px, 42%) minmax(0, 1fr);
grid-template-rows: 1fr;
align-items: start;
}
.presentation-active-view.layout-right {
grid-template-columns: minmax(0, 1fr) minmax(260px, 42%);
grid-template-rows: 1fr;
align-items: start;
}
/* =========================================
SPOT MODE: IMAGE LEFT + POINTER RIGHT
========================================= */
.presentation-active-view.spotlight-mode {
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(360px, 0.85fr) !important;
grid-template-rows: 1fr !important;
gap: 20px;
min-height: 560px;
align-items: stretch;
}
.presentation-active-view.spotlight-mode .presentation-active-media-box {
display: flex !important;
height: 100%;
min-height: 520px;
max-height: none;
background:
radial-gradient(circle at top right, rgba(13,110,253,0.10), transparent 35%),
linear-gradient(135deg, rgba(15,23,42,0.04), rgba(34,197,94,0.06));
border-radius: 28px;
padding: 16px;
border: 1px solid var(--presentation-border);
}
.presentation-active-view.spotlight-mode .presentation-active-media-box img {
width: 100%;
height: 100%;
object-fit: contain;
filter: none !important;
opacity: 1 !important;
}
.presentation-active-view.spotlight-mode .presentation-pointer-list {
width: 100% !important;
display: flex !important;
/*align-items: center;*/
justify-content: center;
padding: 0;
align-items: flex-start !important;
justify-content: center;
padding-top: 455px;
}
.presentation-active-view.spotlight-mode .presentation-pointer-item.show {
display: none !important;
}
.presentation-active-view.spotlight-mode .presentation-pointer-item.active {
display: flex !important;
width: 100%;
min-height: 150px;
opacity: 1 !important;
transform: none !important;
background:
linear-gradient(135deg, rgba(255,255,255,0.98), rgba(245,248,255,0.96));
border: 1px solid rgba(13,110,253,0.22);
border-left: 8px solid var(--presentation-primary);
border-radius: 28px;
padding: 26px 28px;
box-shadow:
0 22px 55px rgba(15,23,42,0.18),
inset 0 1px 0 rgba(255,255,255,0.8);
}
.presentation-active-view.spotlight-mode .presentation-pointer-item.active .presentation-pointer-index {
width: 76px;
height: 76px;
min-width: 76px;
border-radius: 24px;
font-size: 1.7rem;
}
.presentation-active-view.spotlight-mode .presentation-pointer-item.active .presentation-pointer-title {
font-size: calc(1.35rem * var(--presentation-font-scale));
font-weight: 900;
line-height: 1.65;
color: var(--presentation-text);
}
/* SPOT mode: hide empty image area */
.presentation-active-view.spotlight-mode.no-active-image .presentation-active-media-box {
display: none !important;
}
/* SPOT mode: hide empty pointer area */
.presentation-active-view.spotlight-mode.no-active-pointer .presentation-pointer-list {
display: none !important;
}
/* only image exists */
.presentation-active-view.spotlight-mode.no-active-pointer:not(.no-active-image) {
grid-template-columns: 1fr !important;
}
/* only pointer exists */
.presentation-active-view.spotlight-mode.no-active-image:not(.no-active-pointer) {
grid-template-columns: 1fr !important;
}
/* nothing exists */
.presentation-active-view.spotlight-mode.no-active-image.no-active-pointer {
display: none !important;
}
/* Fullscreen spot mode */
.presentation-page-shell.fullscreen-mode .presentation-active-view.spotlight-mode {
height: calc(100vh - 155px);
min-height: 0;
}
.presentation-page-shell.fullscreen-mode .presentation-active-view.spotlight-mode .presentation-active-media-box {
min-height: 0;
height: 100%;
}
.presentation-page-shell.spot-mode-active .presentation-camera-overlay {
z-index: 99999;
}
/* =========================================
SPOT MODE WITHOUT ACTIVE POINTER
========================================= */
/*.presentation-active-view.spotlight-mode.no-active-pointer {*/
/* grid-template-columns: 1fr !important;*/
/*}*/
/*.presentation-active-view.spotlight-mode.no-active-pointer .presentation-pointer-list {*/
/* display: none !important;*/
/*}*/
/* DO NOT force controls visible in spot mode */
.presentation-page-shell.spot-mode-active .presentation-camera-overlay:not(:hover) .presentation-camera-controls {
opacity: 0;
visibility: hidden;
transform: translateY(6px);
}
/* Mobile fallback */
@media (max-width: 991px) {
.presentation-active-view.spotlight-mode {
grid-template-columns: 1fr !important;
grid-template-rows: minmax(0, 1fr) auto !important;
}
.presentation-page-shell.spot-mode-active .presentation-camera-overlay {
top: auto !important;
right: 14px !important;
bottom: 80px !important;
transform: none !important;
--camera-size: 180px;
}
}
/*full fullscreen-mode*/
.presentation-page-shell.fullscreen-mode .presentation-active-view.spotlight-mode {
height: calc(100vh - 155px);
min-height: 0;
}
.presentation-page-shell.fullscreen-mode .presentation-active-view.spotlight-mode .presentation-active-media-box {
min-height: 0;
height: 100%;
}
/* =========================================
FULLSCREEN IMAGE MODE FIXES
========================================= */
.presentation-layout-mini-btn.active {
background: var(--presentation-primary);
color: #fff;
border-color: var(--presentation-primary);
}
.presentation-page-shell.fullscreen-mode .presentation-active-view.image-mode-background {
min-height: calc(100vh - 180px);
}
.presentation-page-shell.fullscreen-mode .presentation-active-view.image-mode-background .presentation-active-media-box {
inset: 0;
max-height: none;
height: 100%;
}
.presentation-page-shell.fullscreen-mode .presentation-active-view.image-mode-focus .presentation-active-media-box {
max-height: calc(100vh - 210px);
}
.presentation-page-shell.fullscreen-mode .presentation-active-view.image-mode-hide {
grid-template-rows: 1fr;
}
.presentation-active-media-box {
display: none;
position: relative;
width: 100%;
max-width: 100%;
min-height: 280px;
max-height: calc(100vh - 220px);
border-radius: 24px;
padding: 18px;
margin: 0;
overflow: hidden;
align-items: center;
justify-content: center;
background:
radial-gradient(circle at center, rgba(255,255,255,0.18), transparent 35%),
linear-gradient(135deg, rgba(15,23,42,0.08), rgba(13,110,253,0.08));
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.18),
0 24px 55px rgba(15,23,42,0.16);
}
.presentation-active-media-box.show {
display: flex;
}
.presentation-active-media-box img {
max-width: 100%;
max-height: calc(100vh - 260px);
width: auto;
height: auto;
display: block;
margin: 0 auto;
border-radius: 22px;
object-fit: contain;
opacity: 0;
transform: scale(0.96) translateY(12px);
transition:
opacity 0.7s ease,
transform 0.7s ease,
filter 0.7s ease;
box-shadow:
0 28px 65px rgba(0,0,0,0.26),
0 0 80px rgba(13,110,253,0.16);
position: relative;
z-index: 2;
}
.presentation-active-media-box img.show {
opacity: 1;
transform: scale(1) translateY(0);
}
.presentation-active-media-box.has-image::before {
content: "";
position: absolute;
inset: -35px;
background-image: var(--active-slide-image);
background-size: cover;
background-position: center;
filter: blur(36px);
opacity: 0.22;
transform: scale(1.12);
z-index: 0;
}
.presentation-active-media-box.has-image::after {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(180deg, rgba(255,255,255,0.12), rgba(255,255,255,0.02)),
radial-gradient(circle at top right, rgba(13,110,253,0.18), transparent 32%);
z-index: 1;
pointer-events: none;
}
.presentation-active-media-box.cinematic img.show {
animation: cinematicImageZoom 16s ease-in-out infinite alternate;
}
@keyframes cinematicImageZoom {
from {
transform: scale(1) translateY(0);
}
to {
transform: scale(1.035) translateY(-4px);
}
}
.presentation-magnifier-lens.active {
display: block;
}
.presentation-magnifier-enabled {
cursor: none;
}
.presentation-magnifier-lens {
position: fixed;
width: 180px;
height: 180px;
border-radius: 50%;
overflow: hidden;
display: none;
z-index: 99999;
border: 3px solid #fff;
box-shadow: 0 10px 25px rgba(0,0,0,0.3);
/*background: #000;*/
pointer-events: none;
}
.presentation-magnifier-content {
position: absolute;
top: 0;
left: 0;
transform-origin: top left;
}
.presentation-magnifier-lens.active {
display: block;
}
.presentation-magnifier-content {
position: absolute;
top: 0;
left: 0;
transform-origin: top left;
will-change: transform;
}
.presentation-magnifier-enabled {
cursor: none;
}
.presentation-pointer-list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 10px;
align-content: start;
}
.presentation-pointer-item {
display: flex;
gap: calc(10px * var(--presentation-font-scale, 1));
align-items: center;
background: rgba(255,255,255,0.76);
border: 1px solid var(--presentation-border);
border-radius: 16px;
padding: calc(5px * var(--presentation-font-scale, 1)) calc(10px * var(--presentation-font-scale, 1));
cursor: pointer;
transition: all 0.28s ease;
opacity: 0;
transform: translateY(14px) scale(0.98);
}
.presentation-pointer-item.show {
opacity: 1;
transform: translateY(0) scale(1);
}
.presentation-pointer-item:hover {
transform: translateY(-1px);
box-shadow: 0 10px 18px rgba(0,0,0,0.05);
}
.presentation-pointer-item.active {
border-color: rgba(13,110,253,0.38);
box-shadow:
0 14px 30px rgba(13,110,253,0.16),
inset 0 1px 0 rgba(255,255,255,0.45);
background:
linear-gradient(135deg, rgba(13,110,253,0.10), rgba(255,255,255,0.82));
transform: scale(1.015);
}
.presentation-pointer-list.has-active .presentation-pointer-item.show {
opacity: 0.45;
}
.presentation-pointer-list.has-active .presentation-pointer-item.active {
opacity: 1;
}
.presentation-pointer-index {
width: calc(56px * var(--presentation-font-scale, 1));
height: calc(56px * var(--presentation-font-scale, 1));
min-width: calc(56px * var(--presentation-font-scale, 1));
border-radius: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
color: #ffffff;
font-size: calc(1.35rem * var(--presentation-font-scale));
margin-top: 2px;
flex-shrink: 0;
background:
linear-gradient(
135deg,
rgba(34, 197, 94, 0.95),
rgba(22, 163, 74, 0.92)
);
border: 1px solid rgba(255,255,255,0.16);
box-shadow:
0 10px 25px rgba(34,197,94,0.28),
inset 0 1px 0 rgba(255,255,255,0.18);
backdrop-filter: blur(10px);
transition:
transform 0.25s ease,
box-shadow 0.25s ease,
filter 0.25s ease;
}
/* glossy shine */
.presentation-pointer-index::before {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(
135deg,
rgba(255,255,255,0.30),
transparent 40%
);
pointer-events: none;
}
/* icon */
.presentation-pointer-index i {
font-size: calc(1.45rem * var(--presentation-font-scale));
position: relative;
z-index: 2;
}
/* hover effect */
.presentation-pointer-item:hover .presentation-pointer-index {
transform: translateY(-2px) scale(1.06);
box-shadow:
0 16px 35px rgba(34,197,94,0.38),
inset 0 1px 0 rgba(255,255,255,0.24);
filter: brightness(1.05);
}
/* active effect */
.presentation-pointer-item.active .presentation-pointer-index {
background:
linear-gradient(
135deg,
rgba(13,110,253,0.96),
rgba(99,102,241,0.92)
);
box-shadow:
0 18px 40px rgba(13,110,253,0.34),
inset 0 1px 0 rgba(255,255,255,0.24);
}
.presentation-icon-btn.active {
background: var(--link-color);
color: #fff;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.presentation-pointer-text {
flex: 1;
min-width: 0;
}
.presentation-pointer-title {
font-weight: 700;
line-height: 1.7;
color: var(--presentation-text);
word-break: break-word;
font-size: calc(1rem * var(--presentation-font-scale));
}
.presentation-stage,
.presentation-stage :not(i):not(.fa):not(.fa-solid):not(.fa-regular):not(.fa-brands),
.presentation-explanation-panel,
.presentation-explanation-panel :not(i):not(.fa):not(.fa-solid):not(.fa-regular):not(.fa-brands),
.presentation-mobile-sheet,
.presentation-mobile-sheet :not(i):not(.fa):not(.fa-solid):not(.fa-regular):not(.fa-brands) {
font-family:
'Hind Siliguri',
'Segoe UI',
Arial,
sans-serif !important;
letter-spacing: 0 !important;
}
.presentation-pointer-index i,
.presentation-stage i.fa,
.presentation-stage i.fa-solid,
.presentation-stage i.fa-regular,
.presentation-stage i.fa-brands {
font-family: "Font Awesome 6 Free" !important;
font-weight: 900 !important;
}
.presentation-explanation-panel {
padding: 16px;
min-height: 620px;
display: flex;
flex-direction: column;
}
.presentation-explanation-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid var(--presentation-border);
}
.presentation-explanation-title {
font-size: 1rem;
font-weight: 800;
color: var(--presentation-text);
}
.presentation-explanation-actions {
display: flex;
align-items: center;
gap: 6px;
}
.presentation-save-indicator {
font-size: 0.78rem;
color: var(--presentation-text-soft);
}
.presentation-explanation-editor {
flex: 1;
width: 100%;
min-height: 260px;
border: 1px solid var(--presentation-border);
border-radius: 14px;
padding: 12px;
outline: none;
resize: vertical;
background: var(--presentation-panel-bg);
color: var(--presentation-text);
line-height: 1.8;
font-size: 0.95rem;
}
.presentation-progress {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 16px;
padding-top: 14px;
border-top: 1px solid var(--presentation-border);
}
.presentation-progress-bar-wrap {
flex: 1;
height: 8px;
background: rgba(0,0,0,0.06);
border-radius: 999px;
overflow: hidden;
}
.presentation-progress-bar {
width: 0%;
height: 100%;
background: linear-gradient(90deg, var(--presentation-primary), var(--presentation-primary-hover));
transition: width 0.3s ease;
}
.presentation-progress-text {
font-size: 0.84rem;
color: var(--presentation-text-soft);
white-space: nowrap;
margin-right: auto;
font-weight: 700;
}
.presentation-mobile-hint {
display: none;
margin-top: 10px;
font-size: 0.84rem;
color: var(--presentation-text-soft);
}
.presentation-mobile-explanation-modal {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.62);
z-index: 9999;
display: none;
align-items: flex-end;
justify-content: center;
padding: 12px;
}
.presentation-mobile-explanation-modal.show {
display: flex;
}
.presentation-mobile-sheet {
width: 100%;
max-width: 700px;
background: var(--presentation-panel-bg);
border-radius: 22px 22px 0 0;
padding: 16px;
max-height: 82vh;
overflow: auto;
box-shadow: 0 -10px 34px rgba(0,0,0,0.16);
}
.presentation-mobile-sheet-handle {
width: 64px;
height: 5px;
background: rgba(0,0,0,0.14);
border-radius: 999px;
margin: 0 auto 12px auto;
}
.presentation-mobile-sheet-title {
font-size: 0.98rem;
font-weight: 800;
margin-bottom: 9px;
color: var(--presentation-text);
}
.presentation-mobile-sheet-body {
color: var(--presentation-text);
line-height: 1.85;
white-space: pre-wrap;
}
.presentation-mobile-sheet-close {
margin-top: 14px;
width: 100%;
}
.presentation-page-shell.fullscreen-mode {
padding: 0;
min-height: 100vh;
width: 100vw;
height: 100vh;
overflow: hidden;
}
.presentation-page-shell.fullscreen-mode .presentation-main {
gap: 0;
grid-template-columns: var(--presentation-sidebar-width) minmax(0, 1fr);
min-height: 100vh;
height: 100vh;
}
.presentation-page-shell.fullscreen-mode .presentation-sidebar {
position: sticky;
top: 0;
border-radius: 0;
min-height: 100vh;
height: 100vh;
box-shadow: none;
}
.presentation-page-shell.fullscreen-mode .presentation-slide-nav-btn {
min-height: 42px;
font-size: 0.9rem;
}
.presentation-page-shell.fullscreen-mode .presentation-stage-frame {
min-height: 100vh;
height: 100vh;
}
.presentation-page-shell.fullscreen-mode .presentation-workspace {
grid-template-columns: minmax(0, 1fr);
min-height: 100vh;
height: 100vh;
gap: 0;
}
.presentation-page-shell.fullscreen-mode .presentation-stage {
border-radius: 0;
border: none;
min-height: 100vh;
height: 100vh;
box-shadow: none;
padding: 14px 18px 12px 18px;
}
.presentation-page-shell.fullscreen-mode .presentation-content-area {
flex: 1;
min-height: 0;
overflow: hidden;
padding-right: 0;
}
.presentation-page-shell.fullscreen-mode .presentation-explanation-panel {
display: none !important;
}
.presentation-stage-title-wrap .presentation-presentation-info {
margin-top: 14px;
}
.presentation-page-shell.fullscreen-mode .presentation-stage-title-wrap .presentation-presentation-info {
margin-top: 12px;
}
.presentation-presentation-meta-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 10px;
}
@media (max-width: 1199px) {
.presentation-workspace {
grid-template-columns: 1fr;
}
.presentation-explanation-panel {
min-height: 260px;
}
.presentation-active-view.layout-left,
.presentation-active-view.layout-right {
grid-template-columns: 1fr;
}
.presentation-presentation-meta-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.presentation-page-shell.fullscreen-mode .presentation-workspace {
grid-template-columns: 1fr;
}
}
@media (max-width: 991px) {
.presentation-top-grid {
grid-template-columns: 1fr;
}
.presentation-upload-wrap {
justify-content: flex-start;
}
.presentation-main {
grid-template-columns: 54px minmax(0, 1fr);
}
.presentation-sidebar {
top: 78px;
}
.presentation-page-shell.fullscreen-mode .presentation-main {
grid-template-columns: 54px minmax(0, 1fr);
}
.presentation-stage-title-wrap h2 {
font-size: calc(1.85rem * var(--presentation-font-scale));
}
.presentation-stage-subtitle {
font-size: calc(1.08rem * var(--presentation-font-scale));
}
}
@media (max-width: 767px) {
.presentation-page-shell {
padding: 12px;
}
.presentation-main {
grid-template-columns: 1fr;
}
.presentation-sidebar {
position: static;
min-height: auto;
padding: 10px;
border-radius: 14px;
}
.presentation-slide-nav {
flex-direction: row;
overflow-x: auto;
overflow-y: hidden;
max-height: unset;
padding-bottom: 3px;
}
.presentation-slide-nav-btn {
min-width: 42px;
min-height: 32px;
font-size: 0.78rem;
}
.presentation-stage,
.presentation-explanation-panel {
min-height: auto;
}
.presentation-explanation-panel {
display: none;
}
.presentation-mobile-hint {
display: block;
}
.presentation-stage-header {
grid-template-columns: 1fr;
}
.presentation-active-view.layout-left,
.presentation-active-view.layout-right {
grid-template-columns: 1fr;
}
.presentation-page-shell.fullscreen-mode .presentation-main {
grid-template-columns: 1fr;
}
.presentation-page-shell.fullscreen-mode .presentation-sidebar {
position: static;
min-height: auto;
height: auto;
}
}
</style>
<div class="dashboard-main">
<div class="presentation-page-shell" id="presentationPageShell">
<div class="presentation-top-card" id="presentationTopCard">
<div class="presentation-top-grid">
<div>
<h1 class="presentation-title">Excel Presentation Viewer</h1>
<p class="presentation-subtitle">
Upload your Excel file, reveal pointers one by one, show point images properly, and detach explanation into a separate window for your second monitor.
</p>
<div class="presentation-template-note">
Optional first row:
<strong>Presentation Title:</strong>, value,
<strong>Presented by:</strong>, value,
<strong>Organized By:</strong>, value,
<strong>Date:</strong>, value.
<br>
Slide columns:
<strong>Slide Number</strong>,
<strong>Slide Title</strong>,
<strong>Slide Subtitle</strong>,
<strong>Image</strong>,
<strong>Pointers</strong>,
<strong>Explanation in Details</strong>.
</div>
<p><a href="/user/documents/presentation-template.xlsx"> Download template</a></p>
</div>
<div>
<!--<div class="presentation-upload-wrap">-->
<!-- <input type="file" id="presentationExcelFile" class="presentation-file-input" accept=".xlsx,.xls,.csv">-->
<!-- <button type="button" class="presentation-btn presentation-btn-primary" id="presentationLoadBtn">Load File</button>-->
<!-- <button type="button" class="presentation-btn presentation-btn-light" id="presentationResetBtn">Reset</button>-->
<!--</div>-->
<!--<div class="presentation-upload-wrap">-->
<!-- <input type="file" id="presentationExcelFile" class="presentation-file-input" accept=".xlsx,.xls,.csv">-->
<!-- <button type="button" class="presentation-btn presentation-btn-primary" id="presentationLoadBtn">Load File</button>-->
<!-- <button type="button" class="presentation-btn presentation-btn-light" id="presentationSaveToServerBtn">Save to Server</button>-->
<!-- <button type="button" class="presentation-btn presentation-btn-light" id="presentationResetBtn">Reset</button>-->
<!--</div>-->
<div class="presentation-upload-wrap">
<input type="file" id="presentationExcelFile" class="presentation-file-input" accept=".xlsx,.xls,.csv">
<button type="button" class="presentation-btn presentation-btn-primary" id="presentationLoadBtn">Load File</button>
<button type="button" class="presentation-btn presentation-btn-light" id="presentationSaveToServerBtn">Save to Server</button>
<button type="button" class="presentation-btn presentation-btn-light" id="presentationLoadLatestBtn">Load Latest</button>
<button type="button" class="presentation-btn presentation-btn-light" id="presentationAllExcelsBtn">All Excels</button>
<button type="button" class="presentation-btn presentation-btn-light" id="presentationResetBtn">Reset</button>
</div>
<div class="presentation-feedback" id="presentationFeedback"></div>
<!-- All Excels Modal -->
<div id="presentationExcelModal" style="
display:none; position:fixed; inset:0; z-index:99999;
background:rgba(15,23,42,0.55); align-items:center; justify-content:center; padding:16px;">
<div style="
background:var(--presentation-panel-bg,#fff);
border-radius:22px; width:100%; max-width:620px;
max-height:80vh; display:flex; flex-direction:column;
box-shadow:0 24px 60px rgba(0,0,0,0.22);
border:1px solid var(--presentation-border,rgba(0,0,0,0.08));">
<div style="
display:flex; align-items:center; justify-content:space-between;
padding:18px 20px 14px; border-bottom:1px solid var(--presentation-border,rgba(0,0,0,0.08));">
<div style="font-size:1.05rem; font-weight:800; color:var(--presentation-text,#1f2937);">
Excel Files on Server
</div>
<button type="button" id="presentationExcelModalClose" style="
border:none; background:none; font-size:1.3rem;
cursor:pointer; color:var(--presentation-text-soft,#6b7280);
line-height:1; padding:4px 8px; border-radius:8px;">✕</button>
</div>
<div id="presentationExcelModalBody" style="
flex:1; overflow-y:auto; padding:14px 20px 20px;">
<div style="color:var(--presentation-text-soft,#6b7280); font-size:0.9rem;">Loading...</div>
</div>
</div>
</div>
<div class="presentation-feedback" id="presentationFeedback"></div>
<div class="presentation-feedback" id="presentationFeedback"></div>
</div>
</div>
</div>
<div class="presentation-main">
<aside class="presentation-sidebar">
<div class="presentation-sidebar-title">SLD</div>
<div class="presentation-slide-nav" id="presentationSlideNav"></div>
</aside>
<div class="presentation-stage-frame" id="presentationStageFrame">
<div class="presentation-workspace">
<style>
.presentation-camera-overlay.hidden {
display: none !important;
}
/*.presentation-camera-overlay {*/
/* position: fixed;*/
/* left: 14px;*/
/* bottom: 14px;*/
/* width: auto;*/
/* z-index: 9999;*/
/* display: flex;*/
/* flex-direction: column;*/
/* align-items: center;*/
/* gap: 6px;*/
/* --camera-size: 200px;*/
/*}*/
.presentation-camera-overlay {
position: fixed;
top: 2px;
left: calc(55% - 100px);
width: auto;
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
--camera-size: 200px;
cursor: grab;
}
.presentation-camera-overlay video {
width: var(--camera-size);
height: var(--camera-size);
border-radius: 50%;
object-fit: cover;
background: #000;
border: 3px solid rgba(255,255,255,0.92);
box-shadow: 0 10px 24px rgba(0,0,0,0.24);
}
.presentation-camera-controls {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 6px;
border-radius: 999px;
background: rgba(0,0,0,0.58);
backdrop-filter: blur(8px);
box-shadow: 0 8px 18px rgba(0,0,0,0.18);
flex-wrap: wrap;
justify-content: center;
max-width: calc(var(--camera-size) + 20px);
}
.presentation-camera-select {
/*max-width: 90px;*/
max-width: calc(var(--camera-size) + 20px);
min-width: 70px;
border: 1px solid rgba(255,255,255,0.12);
background: rgba(255,255,255,0.14);
color: #fff;
border-radius: 999px;
padding: 4px 8px;
font-size: 10px;
outline: none;
}
.presentation-camera-select option {
color: #111;
}
.presentation-camera-btn {
width: 26px;
height: 26px;
min-width: 26px;
border: none;
border-radius: 50%;
background: rgba(255,255,255,0.14);
color: #fff;
font-size: 11px;
font-weight: 700;
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.18s ease;
}
.presentation-camera-btn:hover {
transform: translateY(-1px);
background: rgba(255,255,255,0.24);
}
.presentation-camera-btn.primary {
background: var(--presentation-primary);
}
.presentation-camera-btn.danger {
background: #dc3545;
}
.presentation-camera-btn.active {
box-shadow: 0 0 0 2px rgba(255,255,255,0.18) inset;
}
.presentation-page-shell.fullscreen-mode .presentation-camera-overlay {
left: 18px;
bottom: 18px;
}
@media (max-width: 767px) {
.presentation-camera-overlay {
--camera-size: 180px;
left: 10px;
bottom: 10px;
}
.presentation-camera-btn {
width: 24px;
height: 24px;
min-width: 24px;
font-size: 10px;
}
.presentation-camera-select {
max-width: 78px;
font-size: 9px;
padding: 3px 7px;
}
}
.presentation-camera-overlay {
cursor: grab;
}
.presentation-camera-overlay.dragging {
cursor: grabbing;
}
/* Controls hidden by default */
.presentation-camera-controls {
opacity: 0;
visibility: hidden;
transform: translateY(6px);
transition: all 0.25s ease;
}
/* Show on hover */
.presentation-camera-overlay:hover .presentation-camera-controls {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
/* Also show when active via JS */
.presentation-camera-controls.show {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.presentation-camera-overlay.shape-circle video {
border-radius: 50%;
width: var(--camera-size);
height: var(--camera-size);
}
.presentation-camera-overlay.shape-square video {
border-radius: 18px;
width: var(--camera-size);
height: var(--camera-size);
}
.presentation-camera-overlay.shape-horizontal video {
border-radius: 18px;
width: calc(var(--camera-size) * 1.6);
height: var(--camera-size);
}
.presentation-camera-overlay.shape-vertical video {
border-radius: 18px;
width: var(--camera-size);
height: calc(var(--camera-size) * 1.45);
}
</style>
<div class="presentation-camera-overlay" id="presentationCameraOverlay">
<video id="presentationCameraVideo" autoplay playsinline muted></video>
<div class="presentation-camera-controls">
<select id="presentationCameraSelect" class="presentation-camera-select"></select>
<button type="button" class="presentation-camera-btn" id="presentationCameraSmallerBtn" title="Smaller">−</button>
<button type="button" class="presentation-camera-btn" id="presentationCameraLargerBtn" title="Larger">+</button>
<button type="button" class="presentation-camera-btn" id="presentationCameraShapeBtn" title="Change Shape">▣</button>
<button type="button" class="presentation-camera-btn primary" id="presentationStartCameraBtn" title="Start">▶</button>
<button type="button" class="presentation-camera-btn danger" id="presentationStopCameraBtn" title="Stop">■</button>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const cameraVideo = document.getElementById('presentationCameraVideo');
const startCameraBtn = document.getElementById('presentationStartCameraBtn');
const stopCameraBtn = document.getElementById('presentationStopCameraBtn');
const cameraSelect = document.getElementById('presentationCameraSelect');
const cameraOverlay = document.getElementById('presentationCameraOverlay');
const cameraSmallerBtn = document.getElementById('presentationCameraSmallerBtn');
const cameraLargerBtn = document.getElementById('presentationCameraLargerBtn');
const cameraShapeBtn = document.getElementById('presentationCameraShapeBtn');
let currentCameraSize = 200;
let currentCameraStream = null;
// const cameraOverlay = document.getElementById('presentationCameraOverlay');
let isDragging = false;
let offsetX = 0;
let offsetY = 0;
const controls = document.querySelector('.presentation-camera-controls');
let hideTimeout;
// const cameraOverlay = document.getElementById('presentationCameraOverlay');
const cameraToggleBtn = document.getElementById('presentationCameraToggleBtn');
const CAMERA_ENABLED_KEY = 'presentation_camera_enabled';
let isCameraEnabled = localStorage.getItem(CAMERA_ENABLED_KEY) !== '0';
document.addEventListener('keydown', function (e) {
// ignore typing inside input/textarea/contenteditable
const tag = document.activeElement.tagName;
if (
tag === 'INPUT' ||
tag === 'TEXTAREA' ||
document.activeElement.isContentEditable
) {
return;
}
// Press S
if (e.key.toLowerCase() === 's') {
e.preventDefault();
const spotlightBtn = document.getElementById('presentationSpotlightBtn');
if (spotlightBtn) {
spotlightBtn.click();
}
}
});
function applyCameraVisibility() {
if (!cameraOverlay) return;
if (isCameraEnabled) {
cameraOverlay.classList.remove('hidden');
cameraToggleBtn.classList.add('active');
cameraToggleBtn.title = 'Hide Camera';
} else {
cameraOverlay.classList.add('hidden');
cameraToggleBtn.classList.remove('active');
cameraToggleBtn.title = 'Show Camera';
if (typeof stopCamera === 'function') {
stopCamera();
}
}
}
cameraToggleBtn.addEventListener('click', function () {
isCameraEnabled = !isCameraEnabled;
localStorage.setItem(CAMERA_ENABLED_KEY, isCameraEnabled ? '1' : '0');
applyCameraVisibility();
});
// Show controls
function showControls() {
controls.classList.add('show');
clearTimeout(hideTimeout);
hideTimeout = setTimeout(() => {
controls.classList.remove('show');
}, 100);
}
// Show on interactions
cameraOverlay.addEventListener('mouseenter', showControls);
cameraOverlay.addEventListener('mousemove', showControls);
// Optional: show when camera starts
startCameraBtn.addEventListener('click', showControls);
// Optional: show when size changes
cameraLargerBtn.addEventListener('click', showControls);
cameraSmallerBtn.addEventListener('click', showControls);
// Initial show (first load)
showControls();
// Mouse down
cameraOverlay.addEventListener('mousedown', function (e) {
isDragging = true;
const rect = cameraOverlay.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
cameraOverlay.classList.add('dragging');
});
// Mouse move
document.addEventListener('mousemove', function (e) {
if (!isDragging) return;
let left = e.clientX - offsetX;
let top = e.clientY - offsetY;
// Keep inside screen
left = Math.max(0, Math.min(window.innerWidth - cameraOverlay.offsetWidth, left));
top = Math.max(0, Math.min(window.innerHeight - cameraOverlay.offsetHeight, top));
cameraOverlay.style.left = left + 'px';
cameraOverlay.style.top = top + 'px';
// Remove bottom/right so it doesn't conflict
cameraOverlay.style.right = 'auto';
cameraOverlay.style.bottom = 'auto';
cameraOverlay.style.transform = 'none';
});
// Mouse up
document.addEventListener('mouseup', function () {
isDragging = false;
cameraOverlay.classList.remove('dragging');
});
const CAMERA_MIN_SIZE = 90;
const CAMERA_MAX_SIZE = 1000;
const CAMERA_STEP = 25;
const CAMERA_SHAPES = ['shape-circle', 'shape-square', 'shape-horizontal', 'shape-vertical'];
let currentCameraShapeIndex = 0;
function applyCameraShape() {
cameraOverlay.classList.remove(...CAMERA_SHAPES);
cameraOverlay.classList.add(CAMERA_SHAPES[currentCameraShapeIndex]);
keepCameraInsideScreen();
}
cameraShapeBtn.addEventListener('click', function () {
currentCameraShapeIndex = (currentCameraShapeIndex + 1) % CAMERA_SHAPES.length;
applyCameraShape();
});
function keepCameraInsideScreen() {
if (!cameraOverlay) return;
const rect = cameraOverlay.getBoundingClientRect();
let left = rect.left;
let top = rect.top;
if (left + cameraOverlay.offsetWidth > window.innerWidth) {
left = Math.max(0, window.innerWidth - cameraOverlay.offsetWidth);
}
if (top + cameraOverlay.offsetHeight > window.innerHeight) {
top = Math.max(0, window.innerHeight - cameraOverlay.offsetHeight);
}
cameraOverlay.style.left = left + 'px';
cameraOverlay.style.top = top + 'px';
cameraOverlay.style.right = 'auto';
cameraOverlay.style.bottom = 'auto';
cameraOverlay.style.transform = 'none';
}
function applyCameraSize() {
currentCameraSize = Math.max(
CAMERA_MIN_SIZE,
Math.min(CAMERA_MAX_SIZE, currentCameraSize)
);
cameraOverlay.style.setProperty('--camera-size', currentCameraSize + 'px');
keepCameraInsideScreen();
}
cameraLargerBtn.addEventListener('click', function () {
currentCameraSize += CAMERA_STEP;
applyCameraSize();
});
cameraSmallerBtn.addEventListener('click', function () {
currentCameraSize -= CAMERA_STEP;
applyCameraSize();
});
async function loadCameraList() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(device => device.kind === 'videoinput');
cameraSelect.innerHTML = '';
videoDevices.forEach((device, index) => {
const option = document.createElement('option');
option.value = device.deviceId;
option.textContent = device.label || ('Camera ' + (index + 1));
cameraSelect.appendChild(option);
});
} catch (error) {
console.error('Could not load camera devices:', error);
}
}
async function startCamera() {
try {
if (currentCameraStream) {
currentCameraStream.getTracks().forEach(track => track.stop());
}
let constraints;
if (cameraSelect.value) {
constraints = {
video: { deviceId: { exact: cameraSelect.value } },
audio: false
};
} else {
constraints = {
video: true, // fallback (IMPORTANT)
audio: false
};
}
currentCameraStream = await navigator.mediaDevices.getUserMedia(constraints);
cameraVideo.srcObject = currentCameraStream;
await loadCameraList();
} catch (error) {
console.error('Camera start failed:', error);
// alert("Camera Error: " + error.message); // 🔥 show real error
if (error.name === 'NotReadableError' || /device in use/i.test(error.message)) {
alert('Your camera is already being used by another app or tab. Please close that app or tab and try again.');
} else if (error.name === 'NotAllowedError') {
alert('Camera permission is blocked. Please allow camera access in your browser.');
} else if (error.name === 'NotFoundError') {
alert('No camera was found on this device.');
} else {
alert('Camera Error: ' + error.name + ' - ' + error.message);
}
}
}
function stopCamera() {
if (currentCameraStream) {
currentCameraStream.getTracks().forEach(track => track.stop());
currentCameraStream = null;
}
cameraVideo.srcObject = null;
}
startCameraBtn.addEventListener('click', startCamera);
stopCameraBtn.addEventListener('click', stopCamera);
cameraSelect.addEventListener('change', function () {
if (cameraVideo.srcObject) {
startCamera();
}
});
if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
loadCameraList();
}
applyCameraVisibility();
applyCameraShape();
});
</script>
<section class="presentation-stage">
<div class="presentation-stage-header">
<div class="presentation-stage-title-wrap">
<h2 id="presentationStageTitle">Slide title will appear here</h2>
<div class="presentation-stage-subtitle" id="presentationStageSubtitle">Slide subtitle will appear here</div>
<div class="presentation-stage-meta" id="presentationStageMeta">0 pointer(s) in this slide</div>
<style>
.presentation-presentation-info {
display: none;
position: relative;
overflow: hidden;
width: min(100%, 1280px);
margin: 12px auto 0;
border-radius: 32px;
padding: 28px 30px 26px;
/*color: #ffffff;*/
/*background:*/
/* radial-gradient(circle at top right, rgba(255,255,255,0.18), transparent 28%),*/
/* radial-gradient(circle at bottom left, rgba(255,255,255,0.10), transparent 26%),*/
/* linear-gradient(135deg, rgba(22, 101, 52, 0.96), rgba(21, 128, 61, 0.92), rgba(101, 163, 13, 0.90));*/
border: 1px solid rgba(255,255,255,0.16);
box-shadow:
0 18px 45px rgba(0,0,0,0.16),
inset 0 1px 0 rgba(255,255,255,0.12);
backdrop-filter: blur(10px);
transform: translateY(10px) scale(0.98);
opacity: 0;
transition: opacity 0.35s ease, transform 0.35s ease;
}
.presentation-presentation-info.show {
display: block;
opacity: 1;
transform: translateY(0) scale(1);
}
.presentation-presentation-info::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
/*background:*/
/* linear-gradient(135deg, rgba(255,255,255,0.08), transparent 35%),*/
/* linear-gradient(315deg, rgba(255,255,255,0.06), transparent 42%);*/
}
.presentation-presentation-info > * {
position: relative;
z-index: 1;
}
.presentation-presentation-hero-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.presentation-presentation-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 15px;
border-radius: 999px;
background: rgb(96 126 87 / 14%);
border: 1px solid rgba(255,255,255,0.16);
font-size: 0.86rem;
font-weight: 700;
line-height: 1;
white-space: nowrap;
}
.presentation-presentation-title {
font-size: clamp(2rem, 4.2vw, 4.8rem);
font-weight: 900;
line-height: 1.05;
margin: 0 0 10px 0;
letter-spacing: -0.04em;
word-break: break-word;
max-width: 980px;
}
.presentation-presentation-subline {
font-size: 1.05rem;
line-height: 1.7;
opacity: 0.9;
margin: 0 0 8px 0;
}
.presentation-presentation-meta-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
align-items: stretch;
}
.presentation-presentation-meta-card {
display: flex;
align-items: flex-start;
gap: 14px;
min-height: 104px;
padding: 18px 18px;
border-radius: 22px;
background: rgb(98 129 93 / 12%);
border: 1px solid rgba(255,255,255,0.14);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.06);
backdrop-filter: blur(8px);
transition: transform 0.22s ease, box-shadow 0.22s ease, background 0.22s ease;
}
.presentation-presentation-meta-card:hover {
transform: translateY(-2px);
background: rgba(255,255,255,0.16);
box-shadow:
0 14px 28px rgba(0,0,0,0.10),
inset 0 1px 0 rgba(255,255,255,0.08);
}
.presentation-presentation-meta-icon {
width: 50px;
height: 50px;
min-width: 50px;
border-radius: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1.15rem;
line-height: 1;
background: rgba(255,255,255,0.16);
border: 1px solid rgba(255,255,255,0.18);
}
.presentation-presentation-meta-content {
flex: 1;
min-width: 0;
}
.presentation-presentation-meta-label {
font-size: 0.76rem;
text-transform: uppercase;
letter-spacing: 0.09em;
line-height: 1.4;
opacity: 0.78;
margin-bottom: 6px;
font-weight: 700;
}
.presentation-presentation-meta-value {
font-size: 1.12rem;
line-height: 1.5;
font-weight: 800;
word-break: break-word;
}
.presentation-page-shell.fullscreen-mode .presentation-presentation-info {
width: min(100%, 1420px);
margin: 10px auto 0;
padding: 38px 40px 34px;
border-radius: 36px;
}
.presentation-page-shell.fullscreen-mode .presentation-presentation-title {
font-size: clamp(2.4rem, 5vw, 5.4rem);
}
.presentation-page-shell.fullscreen-mode .presentation-presentation-meta-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 18px;
}
@media (max-width: 991px) {
.presentation-presentation-info {
width: 100%;
padding: 22px 20px 20px;
border-radius: 24px;
}
.presentation-presentation-title {
font-size: clamp(1.8rem, 5vw, 3rem);
}
.presentation-presentation-meta-grid {
grid-template-columns: 1fr;
}
}
.presentation-content-area {
display: flex;
flex-direction: column;
gap: 18px;
flex: 1;
min-height: 320px;
justify-content: flex-start;
}
.presentation-page-shell.intro-visible .presentation-content-area {
justify-content: center;
}
.presentation-page-shell.intro-visible .presentation-active-view {
min-height: 0;
}
.presentation-page-shell.intro-visible .presentation-stage {
justify-content: flex-start;
}
/* =========================
BACKGROUND DECOR SYSTEM
========================= */
.presentation-presentation-info {
position: relative;
overflow: hidden;
}
/* container */
.presentation-bg-decor {
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
}
/* base circle */
.bg-circle {
position: absolute;
border-radius: 50%;
filter: blur(60px);
opacity: 0.55;
animation: floatMove 12s ease-in-out infinite alternate;
}
/* individual circles */
.circle-1 {
width: 260px;
height: 260px;
background: #22c55e;
top: -180px;
left: -60px;
}
.circle-2 {
width: 120px;
height: 220px;
background: #84cc16;
bottom: -180px;
right: -40px;
animation-delay: 2s;
}
.circle-3 {
width: 1180px;
height: 180px;
background: #4ade80;
top: 50%;
left: 40%;
transform: translate(-50%, -50%);
animation-delay: 4s;
}
.circle-4 {
width: 1140px;
height: 140px;
background: #bbf7d0;
bottom: 20%;
left: 20%;
animation-delay: 6s;
}
/* animation */
@keyframes floatMove {
0% {
transform: translateY(0px) scale(1);
}
100% {
transform: translateY(-25px) scale(1.05);
}
}
</style>
</div>
</div>
<div class="presentation-content-area">
<div class="presentation-presentation-info" id="presentationInfoBox">
<div class="presentation-bg-decor">
<span class="bg-circle circle-1"></span>
<span class="bg-circle circle-2"></span>
<span class="bg-circle circle-3"></span>
<span class="bg-circle circle-4"></span>
</div>
<div class="presentation-presentation-hero-row">
<div class="presentation-presentation-badge">
<span>🎤</span>
<span>Presentation Mode</span>
</div>
<div class="presentation-presentation-badge" id="presentationStartHint">
<span>✨</span>
<span>Start your journey with me</span>
</div>
</div>
<div class="presentation-presentation-header">
<div class="presentation-presentation-title" id="presentationMainTitle">Presentation</div>
<div class="presentation-presentation-subline">Presentation overview</div>
</div>
<div class="presentation-presentation-meta-grid" id="presentationMetaGrid">
<div class="presentation-presentation-meta-card" id="presentationPresentedByCard">
<div class="presentation-presentation-meta-icon">👤</div>
<div class="presentation-presentation-meta-content">
<div class="presentation-presentation-meta-label">Presented By</div>
<div class="presentation-presentation-meta-value" id="presentationPresentedBy">-</div>
</div>
</div>
<div class="presentation-presentation-meta-card" id="presentationOrganizedByCard">
<div class="presentation-presentation-meta-icon">🏛️</div>
<div class="presentation-presentation-meta-content">
<div class="presentation-presentation-meta-label">Organized By</div>
<div class="presentation-presentation-meta-value" id="presentationOrganizedBy">-</div>
</div>
</div>
<div class="presentation-presentation-meta-card" id="presentationDateCard">
<div class="presentation-presentation-meta-icon">📅</div>
<div class="presentation-presentation-meta-content">
<div class="presentation-presentation-meta-label">Date</div>
<div class="presentation-presentation-meta-value" id="presentationDate">-</div>
</div>
</div>
<div class="presentation-presentation-meta-card" id="presentationCurrentSlideCard">
<div class="presentation-presentation-meta-icon">🖥️</div>
<div class="presentation-presentation-meta-content">
<div class="presentation-presentation-meta-label">Current Slide</div>
<div class="presentation-presentation-meta-value" id="presentationCurrentSlideLabel">-</div>
</div>
</div>
</div>
</div>
<div class="presentation-active-view layout-top" id="presentationActiveView">
<div class="presentation-bg-decor">
<!--<span class="bg-circle circle-1"></span>-->
<span class="bg-circle circle-2"></span>
<span class="bg-circle circle-3"></span>
<span class="bg-circle circle-4"></span>
</div>
<div class="presentation-empty-state" id="presentationEmptyState">
</div>
<div class="presentation-active-media-box" id="presentationActiveMediaBox"></div>
<ul class="presentation-pointer-list" id="presentationPointerList"></ul>
</div>
</div>
<div class="presentation-mobile-hint">
On mobile, tap any visible pointer to open its explanation.
</div>
<div class="presentation-progress">
<div class="presentation-progress-bar-wrap">
<div class="presentation-progress-bar" id="presentationProgressBar"></div>
</div>
<div class="presentation-progress-text" id="presentationProgressText">0 / 0 points visible</div>
<div class="presentation-stage-actions" id="presentationStageActions">
<div class="presentation-layout-mini-group" id="presentationLayoutMiniGroup">
<button type="button" class="presentation-layout-mini-btn active" data-layout="top" title="Image Top">T</button>
<button type="button" class="presentation-layout-mini-btn" data-layout="left" title="Image Left">L</button>
<button type="button" class="presentation-layout-mini-btn" data-layout="right" title="Image Right">R</button>
<button type="button" class="presentation-layout-mini-btn" id="presentationSpotlightBtn" title="Spotlight Mode">SPOT</button>
</div>
<!-- Font Size Controls -->
<div class="presentation-layout-mini-group show" id="presentationFontSizeGroup">
<button type="button" class="presentation-icon-btn" id="presentationMagnifierBtn" title="Magnifier">🔍</button>
<button type="button" class="presentation-layout-mini-btn" id="presentationFontDecreaseBtn" title="Decrease Text Size">−</button>
<button type="button" class="presentation-layout-mini-btn" id="presentationFontIncreaseBtn" title="Increase Text Size">+</button>
</div>
<button type="button" class="presentation-icon-btn" id="presentationHtmlToggleBtn" title="Toggle HTML Rendering">Code</button>
<button type="button" class="presentation-icon-btn" id="presentationPrevBtn" title="Previous Point">⟨</button>
<button type="button" class="presentation-icon-btn primary" id="presentationNextBtn" title="Next Point">⟩</button>
<button type="button" class="presentation-icon-btn" id="presentationReplayBtn" title="Replay Slide">↺</button>
<button type="button" class="presentation-icon-btn" id="presentationCameraToggleBtn" title="Toggle Camera">📷</button>
<button type="button" class="presentation-icon-btn" id="presentationDetachBtn" title="Detach Explanation">⧉</button>
<button type="button" class="presentation-icon-btn" id="presentationFullscreenBtn" title="Fullscreen">⛶</button>
</div>
</div>
</section>
<aside class="presentation-explanation-panel" id="presentationExplanationPanel">
<div class="presentation-explanation-title-row">
<div class="presentation-explanation-title">Explanation</div>
<div class="presentation-explanation-actions">
<span class="presentation-save-indicator" id="presentationSaveIndicator">Auto save off</span>
</div>
</div>
<div id="presentationExplanationEditor" class="presentation-explanation-editor" contenteditable="true"></div>
<!--<textarea id="presentationExplanationEditor" class="presentation-explanation-editor" placeholder="Explanation will appear here and you can edit it directly."></textarea>-->
</aside>
</div>
</div>
</div>
</div>
<div class="presentation-mobile-explanation-modal" id="presentationMobileModal">
<div class="presentation-mobile-sheet">
<div class="presentation-mobile-sheet-handle"></div>
<div class="presentation-mobile-sheet-title" id="presentationMobileSheetTitle">Explanation</div>
<div class="presentation-mobile-sheet-body" id="presentationMobileSheetBody"></div>
<button type="button" class="presentation-btn presentation-btn-primary presentation-mobile-sheet-close" id="presentationMobileCloseBtn">Close</button>
</div>
</div>
<div id="presentationMagnifierLens" class="presentation-magnifier-lens">
<div id="presentationMagnifierContent" class="presentation-magnifier-content"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/xlsx/dist/xlsx.full.min.js"></script>
<script>
(function () {
'use strict';
const LOCAL_STORAGE_KEY = 'presentation_excel_viewer_state_v6';
const SESSION_KEY_STORAGE = 'presentation_excel_viewer_session_key_v6';
const LAYOUT_STORAGE_KEY = 'presentation_excel_viewer_layout_v2';
const emptyState = document.getElementById('presentationEmptyState');
const magnifierBtn = document.getElementById('presentationMagnifierBtn');
const magnifierLens = document.getElementById('presentationMagnifierLens');
const magnifierContent = document.getElementById('presentationMagnifierContent');
// const magnifierTarget = document.querySelector('.presentation-stage');
// const magnifierTarget = document.body;
let magnifierEnabled = false;
const zoom = 2;
let lensSize = 180;
function toggleMagnifier() {
if (magnifierEnabled) {
disableMagnifier();
return;
}
magnifierEnabled = true;
attachLensToCorrectParent();
applyMagnifierSize();
buildClone();
magnifierLens.classList.add('active');
magnifierLens.style.display = 'block';
magnifierBtn.classList.add('active');
document.body.classList.add('presentation-magnifier-enabled');
}
function disableMagnifier() {
magnifierEnabled = false;
magnifierLens.classList.remove('active');
magnifierLens.style.display = 'none';
magnifierLens.style.left = '-9999px';
magnifierLens.style.top = '-9999px';
magnifierBtn.classList.remove('active');
document.body.classList.remove('presentation-magnifier-enabled');
magnifierContent.innerHTML = '';
}
function buildClone() {
magnifierContent.innerHTML = '';
const target = getMagnifierTarget();
if (!target) return;
const clone = target.cloneNode(true);
clone.style.pointerEvents = 'none';
clone.style.margin = '0';
clone.style.position = 'absolute';
clone.style.top = '0';
clone.style.left = '0';
// ✅ IMPORTANT FIX
clone.style.width = document.documentElement.scrollWidth + 'px';
clone.style.height = document.documentElement.scrollHeight + 'px';
magnifierContent.appendChild(clone);
}
function applyMagnifierSize() {
magnifierLens.style.width = lensSize + 'px';
magnifierLens.style.height = lensSize + 'px';
}
document.addEventListener('wheel', function (event) {
if (!magnifierEnabled) return;
event.preventDefault();
if (event.deltaY < 0) {
lensSize = Math.min(400, lensSize + 20); // scroll up = bigger
} else {
lensSize = Math.max(100, lensSize - 20); // scroll down = smaller
}
applyMagnifierSize();
}, { passive: false });
function moveMagnifier(e) {
if (!magnifierEnabled) return;
const target = getMagnifierTarget();
if (!target) return;
const rect = target.getBoundingClientRect();
const x = e.clientX;
const y = e.clientY;
magnifierLens.style.left = (x - lensSize / 2) + 'px';
magnifierLens.style.top = (y - lensSize / 2) + 'px';
let relX;
let relY;
if (document.fullscreenElement) {
relX = x - rect.left;
relY = y - rect.top;
} else {
relX = x + window.scrollX;
relY = y + window.scrollY;
}
magnifierContent.style.transform =
`translate(${-relX * zoom + lensSize / 2}px, ${-relY * zoom + lensSize / 2}px) scale(${zoom})`;
}
document.addEventListener('fullscreenchange', function () {
if (!magnifierEnabled) return;
// Small delay ensures DOM is ready
setTimeout(() => {
attachLensToCorrectParent();
buildClone();
}, 100);
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && magnifierEnabled) {
disableMagnifier();
}
});
function attachLensToCorrectParent() {
const target = getMagnifierTarget();
if (!target) return;
// Always move lens into correct container
target.appendChild(magnifierLens);
}
function getMagnifierTarget() {
return document.fullscreenElement || document.documentElement;
}
magnifierBtn.addEventListener('click', toggleMagnifier);
document.addEventListener('mousemove', moveMagnifier);
const presentationState = {
slides: {},
slideKeys: [],
currentSlideKey: null,
currentPointerIndex: 0,
visiblePointerCount: 0,
activeVisiblePointerIndex: null,
sessionKey: null,
currentLayout: localStorage.getItem(LAYOUT_STORAGE_KEY) || 'top',
renderHtml: false,
fontScale: 1,
presentationMeta: {
title: '',
presentedBy: '',
organizedBy: '',
date: ''
}
};
let detachedWindow = null;
let saveIndicatorTimeout = null;
let syncInterval = null;
const pageShell = document.getElementById('presentationPageShell');
const fileInput = document.getElementById('presentationExcelFile');
const loadBtn = document.getElementById('presentationLoadBtn');
const resetBtn = document.getElementById('presentationResetBtn');
const prevBtn = document.getElementById('presentationPrevBtn');
const nextBtn = document.getElementById('presentationNextBtn');
const replayBtn = document.getElementById('presentationReplayBtn');
const detachBtn = document.getElementById('presentationDetachBtn');
const fullscreenBtn = document.getElementById('presentationFullscreenBtn');
const stageActions = document.getElementById('presentationStageActions');
const layoutMiniGroup = document.getElementById('presentationLayoutMiniGroup');
const feedbackBox = document.getElementById('presentationFeedback');
const slideNav = document.getElementById('presentationSlideNav');
const stageTitle = document.getElementById('presentationStageTitle');
const stageSubtitle = document.getElementById('presentationStageSubtitle');
const stageMeta = document.getElementById('presentationStageMeta');
const pointerList = document.getElementById('presentationPointerList');
const explanationEditor = document.getElementById('presentationExplanationEditor');
const saveIndicator = document.getElementById('presentationSaveIndicator');
const progressBar = document.getElementById('presentationProgressBar');
const progressText = document.getElementById('presentationProgressText');
const explanationPanel = document.getElementById('presentationExplanationPanel');
const activeView = document.getElementById('presentationActiveView');
const spotlightBtn = document.getElementById('presentationSpotlightBtn');
let spotlightModeEnabled = false;
let previousLayoutBeforeSpot = null;
let previousCameraStateBeforeSpot = null;
function saveCameraStateBeforeSpot() {
const cameraOverlay = document.getElementById('presentationCameraOverlay');
if (!cameraOverlay) return null;
return {
left: cameraOverlay.style.left,
right: cameraOverlay.style.right,
top: cameraOverlay.style.top,
bottom: cameraOverlay.style.bottom,
transform: cameraOverlay.style.transform,
cameraSize: cameraOverlay.style.getPropertyValue('--camera-size')
};
}
function restoreCameraStateAfterSpot() {
const cameraOverlay = document.getElementById('presentationCameraOverlay');
if (!cameraOverlay || !previousCameraStateBeforeSpot) return;
cameraOverlay.style.left = previousCameraStateBeforeSpot.left;
cameraOverlay.style.right = previousCameraStateBeforeSpot.right;
cameraOverlay.style.top = previousCameraStateBeforeSpot.top;
cameraOverlay.style.bottom = previousCameraStateBeforeSpot.bottom;
cameraOverlay.style.transform = previousCameraStateBeforeSpot.transform;
if (previousCameraStateBeforeSpot.cameraSize) {
cameraOverlay.style.setProperty('--camera-size', previousCameraStateBeforeSpot.cameraSize);
} else {
cameraOverlay.style.removeProperty('--camera-size');
}
}
if (spotlightBtn) {
spotlightBtn.addEventListener('click', function (event) {
event.stopPropagation();
spotlightModeEnabled = !spotlightModeEnabled;
if (spotlightModeEnabled) {
previousLayoutBeforeSpot = presentationState.currentLayout || 'top';
previousCameraStateBeforeSpot = saveCameraStateBeforeSpot();
applySpotlightMode();
moveCameraToSpotPosition();
} else {
applySpotlightMode();
restoreCameraStateAfterSpot();
setLayout(previousLayoutBeforeSpot || 'top');
}
persistLocalState();
});
}
function moveCameraToSpotPosition() {
const cameraOverlay = document.getElementById('presentationCameraOverlay');
if (!cameraOverlay) return;
const size = 460;
cameraOverlay.style.setProperty('--camera-size', size + 'px');
// right side, like your screenshot
cameraOverlay.style.left = 'auto';
cameraOverlay.style.right = '250px';
// little above pointer
cameraOverlay.style.top = '170px';
cameraOverlay.style.bottom = 'auto';
cameraOverlay.style.transform = 'none';
}
function applySpotlightMode() {
if (!activeView || !spotlightBtn) return;
activeView.classList.toggle('spotlight-mode', spotlightModeEnabled);
if (pageShell) {
pageShell.classList.toggle('spot-mode-active', spotlightModeEnabled);
}
const activePointer = document.querySelector(
'.presentation-pointer-item.active'
);
if (spotlightModeEnabled) {
updateSpotModeEmptyState();
} else {
activeView.classList.remove('no-active-pointer', 'no-active-image');
}
if (spotlightModeEnabled) {
if (!activePointer) {
activeView.classList.add('no-active-pointer');
} else {
activeView.classList.remove('no-active-pointer');
}
} else {
activeView.classList.remove('no-active-pointer');
}
spotlightBtn.classList.toggle('active', spotlightModeEnabled);
spotlightBtn.textContent = spotlightModeEnabled ? 'FOCUS' : 'SPOT';
spotlightBtn.title = spotlightModeEnabled ? 'Focus Mode On' : 'Focus Mode Off';
document.querySelectorAll('.presentation-layout-mini-btn[data-layout]').forEach(function (btn) {
btn.classList.toggle(
'active',
!spotlightModeEnabled && btn.getAttribute('data-layout') === presentationState.currentLayout
);
});
}
function updateSpotModeEmptyState() {
if (!activeView) return;
const activePointer = activeView.querySelector('.presentation-pointer-item.active');
const visiblePointer = activeView.querySelector('.presentation-pointer-item.show');
const hasPointer = !!(activePointer || visiblePointer);
const hasImage =
activeMediaBox &&
activeMediaBox.classList.contains('show') &&
activeMediaBox.querySelector('img');
activeView.classList.toggle('no-active-pointer', !hasPointer);
activeView.classList.toggle('no-active-image', !hasImage);
}
const activeMediaBox = document.getElementById('presentationActiveMediaBox');
function applyCinematicImageEffect(imageUrl) {
if (!activeMediaBox) return;
if (spotlightModeEnabled) {
updateSpotModeEmptyState();
}
if (imageUrl) {
activeMediaBox.classList.add('has-image', 'cinematic');
activeMediaBox.style.setProperty('--active-slide-image', `url("${imageUrl}")`);
} else {
activeMediaBox.classList.remove('has-image', 'cinematic');
activeMediaBox.style.removeProperty('--active-slide-image');
}
}
const presentationInfoBox = document.getElementById('presentationInfoBox');
const presentationMainTitle = document.getElementById('presentationMainTitle');
const presentationPresentedBy = document.getElementById('presentationPresentedBy');
const presentationOrganizedBy = document.getElementById('presentationOrganizedBy');
const presentationDate = document.getElementById('presentationDate');
const presentationCurrentSlideLabel = document.getElementById('presentationCurrentSlideLabel');
const mobileModal = document.getElementById('presentationMobileModal');
const mobileSheetTitle = document.getElementById('presentationMobileSheetTitle');
const mobileSheetBody = document.getElementById('presentationMobileSheetBody');
const mobileCloseBtn = document.getElementById('presentationMobileCloseBtn');
const fontIncreaseBtn = document.getElementById('presentationFontIncreaseBtn');
const fontDecreaseBtn = document.getElementById('presentationFontDecreaseBtn');
const presentationPresentedByCard = document.getElementById('presentationPresentedByCard');
const presentationOrganizedByCard = document.getElementById('presentationOrganizedByCard');
const presentationDateCard = document.getElementById('presentationDateCard');
const presentationCurrentSlideCard = document.getElementById('presentationCurrentSlideCard');
const htmlToggleBtn = document.getElementById('presentationHtmlToggleBtn');
htmlToggleBtn.setAttribute('data-bs-toggle', 'tooltip');
htmlToggleBtn.setAttribute('data-bs-placement', 'top');
function updateHtmlToggleUi() {
if (presentationState.renderHtml) {
htmlToggleBtn.textContent = 'LIVE';
htmlToggleBtn.title = 'HTML Rendering ON';
htmlToggleBtn.classList.add('active');
} else {
htmlToggleBtn.textContent = '<>';
htmlToggleBtn.title = 'HTML Rendering OFF';
htmlToggleBtn.classList.remove('active');
}
}
function containsBangla(text) {
return /[\u0980-\u09FF]/.test(text);
}
function renderContentBySetting(value) {
const text = value == null ? '' : String(value);
const isBangla = containsBangla(text);
if (presentationState.renderHtml) {
if (isBangla) {
return '<span class="bangla-text">' + text + '</span>';
}
return text;
}
const escaped = escapeHtml(text);
if (isBangla) {
return '<span class="bangla-text">' + escaped + '</span>';
}
return escaped;
}
presentationFontIncreaseBtn.addEventListener('click', function () {
if (presentationState.fontScale < 2.5) {
presentationState.fontScale = +(presentationState.fontScale + 0.1).toFixed(2);
applyFontSize();
}
});
presentationFontDecreaseBtn.addEventListener('click', function () {
if (presentationState.fontScale > 0.6) {
presentationState.fontScale = +(presentationState.fontScale - 0.1).toFixed(2);
applyFontSize();
}
});
function applyFontSize() {
const scale = presentationState.fontScale || 1;
document.documentElement.style.setProperty('--presentation-font-scale', scale);
stageTitle.style.fontSize = (1.85 * scale) + 'rem';
stageSubtitle.style.fontSize = (1.08 * scale) + 'rem';
document.querySelectorAll('.presentation-pointer-title').forEach(function (el) {
el.style.fontSize = (1.0 * scale) + 'rem';
});
}
fontIncreaseBtn.addEventListener('click', function () {
if (presentationState.fontScale < 2.2) {
presentationState.fontScale = +(presentationState.fontScale + 0.1).toFixed(2);
applyFontSize();
}
});
fontDecreaseBtn.addEventListener('click', function () {
if (presentationState.fontScale > 0.7) {
presentationState.fontScale = +(presentationState.fontScale - 0.1).toFixed(2);
applyFontSize();
}
});
function formatPresentationDate(value) {
const raw = normalizeString(value);
if (!raw) {
return '';
}
if (/^\d{2}-\d{2}-\d{4}$/.test(raw)) {
return raw;
}
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
const parts = raw.split('-');
return parts[2] + '-' + parts[1] + '-' + parts[0];
}
if (/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(raw)) {
const parts = raw.split('/');
const dd = parts[0].padStart(2, '0');
const mm = parts[1].padStart(2, '0');
const yyyy = parts[2];
return dd + '-' + mm + '-' + yyyy;
}
if (!isNaN(raw) && Number(raw) > 20000) {
const excelEpoch = new Date(Date.UTC(1899, 11, 30));
const jsDate = new Date(excelEpoch.getTime() + Number(raw) * 86400000);
if (!isNaN(jsDate.getTime())) {
const dd = String(jsDate.getUTCDate()).padStart(2, '0');
const mm = String(jsDate.getUTCMonth() + 1).padStart(2, '0');
const yyyy = jsDate.getUTCFullYear();
return dd + '-' + mm + '-' + yyyy;
}
}
const parsed = new Date(raw);
if (!isNaN(parsed.getTime())) {
const dd = String(parsed.getDate()).padStart(2, '0');
const mm = String(parsed.getMonth() + 1).padStart(2, '0');
const yyyy = parsed.getFullYear();
return dd + '-' + mm + '-' + yyyy;
}
return raw;
}
function isFirstSlide(slideKey) {
return presentationState.slideKeys.length > 0 && presentationState.slideKeys[0] === slideKey;
}
function shouldShowPresentationInfo() {
return (
isFirstSlide(presentationState.currentSlideKey) &&
presentationState.visiblePointerCount === 0 &&
(
normalizeString(presentationState.presentationMeta.title) ||
normalizeString(presentationState.presentationMeta.presentedBy) ||
normalizeString(presentationState.presentationMeta.organizedBy) ||
normalizeString(presentationState.presentationMeta.date)
)
);
}
function showFeedback(message, type) {
feedbackBox.className = 'presentation-feedback show ' + (type || 'success');
feedbackBox.textContent = message || '';
}
function clearFeedback() {
feedbackBox.className = 'presentation-feedback';
feedbackBox.textContent = '';
}
function escapeHtml(value) {
const div = document.createElement('div');
div.textContent = value == null ? '' : String(value);
return div.innerHTML;
}
function normalizeString(value) {
return value == null ? '' : String(value).trim();
}
function isMobileView() {
return window.innerWidth < 768;
}
function ensureSessionKey() {
if (presentationState.sessionKey) {
return presentationState.sessionKey;
}
let savedSessionKey = localStorage.getItem(SESSION_KEY_STORAGE);
if (!savedSessionKey) {
savedSessionKey = 'presentation_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
localStorage.setItem(SESSION_KEY_STORAGE, savedSessionKey);
}
presentationState.sessionKey = savedSessionKey;
return savedSessionKey;
}
function resetSessionKey() {
const newKey = 'presentation_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
presentationState.sessionKey = newKey;
localStorage.setItem(SESSION_KEY_STORAGE, newKey);
}
function getSlideObject(slideKey) {
return presentationState.slides[slideKey] || null;
}
function getSlideItems(slideKey) {
return getSlideObject(slideKey)?.items || [];
}
function getCurrentItemByVisibleIndex(index) {
const items = getSlideItems(presentationState.currentSlideKey);
if (index == null || index < 0 || index >= items.length) {
return null;
}
return items[index];
}
function setSaveIndicator(text) {
saveIndicator.textContent = text;
if (saveIndicatorTimeout) {
clearTimeout(saveIndicatorTimeout);
}
saveIndicatorTimeout = setTimeout(function () {
saveIndicator.textContent = 'Auto saved';
}, 900);
}
function persistLocalState() {
const payload = {
slides: presentationState.slides,
slideKeys: presentationState.slideKeys,
currentSlideKey: presentationState.currentSlideKey,
currentPointerIndex: presentationState.currentPointerIndex,
visiblePointerCount: presentationState.visiblePointerCount,
activeVisiblePointerIndex: presentationState.activeVisiblePointerIndex,
sessionKey: presentationState.sessionKey,
currentLayout: presentationState.currentLayout,
presentationMeta: presentationState.presentationMeta,
renderHtml: presentationState.renderHtml
};
try {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(payload));
localStorage.setItem(LAYOUT_STORAGE_KEY, presentationState.currentLayout);
setSaveIndicator('Saving...');
} catch (error) {
console.error(error);
saveIndicator.textContent = 'Save failed';
}
}
function clearPersistedState() {
try {
localStorage.removeItem(LOCAL_STORAGE_KEY);
} catch (error) {
console.error(error);
}
}
function updateProgress() {
const total = getSlideItems(presentationState.currentSlideKey).length;
const visible = presentationState.visiblePointerCount;
const percentage = total > 0 ? (visible / total) * 100 : 0;
progressBar.style.width = percentage + '%';
progressText.textContent = visible + ' / ' + total + ' points visible';
const slide = getSlideObject(presentationState.currentSlideKey);
const totalPointers = slide?.items?.length || 0;
stageMeta.textContent = totalPointers + ' pointer(s) in this slide';
// ✅ ADD THIS HERE
const emptyState = document.getElementById('presentationEmptyState');
if (visible > 0) {
emptyState.style.display = 'none';
} else {
emptyState.style.display = 'flex';
}
renderPresentationMeta();
}
// function setExplanationValue(text) {
// explanationEditor.value = text || '';
// syncDetachedWindow();
// }
function setExplanationValue(text) {
if (presentationState.renderHtml) {
explanationEditor.innerHTML = text || '';
} else {
explanationEditor.textContent = text || '';
}
syncDetachedWindow();
Prism.highlightAll(); // 👈 ADD HERE
}
explanationEditor.addEventListener('input', function () {
const item = getCurrentItemByVisibleIndex(presentationState.activeVisiblePointerIndex);
if (!item) return;
item.explanation = presentationState.renderHtml
? explanationEditor.innerHTML
: explanationEditor.textContent;
persistLocalState();
syncServerState();
syncDetachedWindow();
});
htmlToggleBtn.addEventListener('click', function () {
presentationState.renderHtml = !presentationState.renderHtml;
updateHtmlToggleUi();
renderVisiblePointers();
const activeItem = getCurrentItemByVisibleIndex(presentationState.activeVisiblePointerIndex);
setExplanationValue(activeItem?.explanation || '');
persistLocalState();
});
function openMobileExplanation(title, body) {
mobileSheetTitle.textContent = title || 'Explanation';
// mobileSheetBody.textContent = body || '';
if (presentationState.renderHtml) {
mobileSheetBody.innerHTML = body || '';
} else {
mobileSheetBody.textContent = body || '';
}
mobileModal.classList.add('show');
document.body.style.overflow = 'hidden';
}
function closeMobileExplanation() {
mobileModal.classList.remove('show');
document.body.style.overflow = '';
}
function setLayout(layoutName) {
const validLayouts = ['top', 'left', 'right'];
const layout = validLayouts.includes(layoutName) ? layoutName : 'top';
presentationState.currentLayout = layout;
activeView.className = 'presentation-active-view layout-' + layout;
applySpotlightMode();
localStorage.setItem(LAYOUT_STORAGE_KEY, layout);
document.querySelectorAll('.presentation-layout-mini-btn[data-layout]').forEach(function (btn) {
btn.classList.toggle('active', btn.getAttribute('data-layout') === layout);
});
}
function updateLayoutVisibility() {
const currentItem = getCurrentItemByVisibleIndex(presentationState.activeVisiblePointerIndex);
const hasImage = !!normalizeString(currentItem?.image || '');
layoutMiniGroup.classList.toggle('show', hasImage);
if (!hasImage) {
activeMediaBox.className = 'presentation-active-media-box';
activeMediaBox.innerHTML = '';
}
applySpotlightMode();
}
function renderPresentationMeta() {
const meta = presentationState.presentationMeta || {
title: '',
presentedBy: '',
organizedBy: '',
date: ''
};
const formattedDate = formatPresentationDate(meta.date);
presentationMainTitle.textContent = normalizeString(meta.title) || 'Presentation';
presentationPresentedBy.textContent = normalizeString(meta.presentedBy) || '-';
presentationOrganizedBy.textContent = normalizeString(meta.organizedBy) || '-';
presentationDate.textContent = formattedDate || '-';
const currentSlide = getSlideObject(presentationState.currentSlideKey);
presentationCurrentSlideLabel.textContent = currentSlide?.slideNumber || '-';
presentationPresentedByCard.style.display = normalizeString(meta.presentedBy) ? '' : 'none';
presentationOrganizedByCard.style.display = normalizeString(meta.organizedBy) ? '' : 'none';
presentationDateCard.style.display = formattedDate ? '' : 'none';
presentationCurrentSlideCard.style.display = 'none';
if (shouldShowPresentationInfo()) {
presentationInfoBox.classList.add('show');
pageShell.classList.add('intro-visible');
} else {
presentationInfoBox.classList.remove('show');
pageShell.classList.remove('intro-visible');
}
}
function hidePresentationIntroIfVisible() {
if (presentationInfoBox.classList.contains('show')) {
presentationInfoBox.classList.remove('show');
}
}
let lastRenderedImageUrl = '';
function renderActivePointImage(item) {
const imageUrl = normalizeString(item?.image || '');
if (imageUrl === '') {
lastRenderedImageUrl = '';
activeMediaBox.className = 'presentation-active-media-box';
activeMediaBox.innerHTML = '';
applyCinematicImageEffect('');
updateLayoutVisibility();
return;
}
// Same image: do not reload, do not fade again
if (imageUrl === lastRenderedImageUrl && activeMediaBox.querySelector('img')) {
updateLayoutVisibility();
return;
}
lastRenderedImageUrl = imageUrl;
activeMediaBox.className = 'presentation-active-media-box show';
activeMediaBox.innerHTML = '';
const img = document.createElement('img');
img.alt = 'Point image';
applyCinematicImageEffect(imageUrl);
img.addEventListener('load', function () {
requestAnimationFrame(function () {
img.classList.add('show');
});
});
img.src = imageUrl;
activeMediaBox.appendChild(img);
updateLayoutVisibility();
}
function setActivePointer(element, visibleIndex) {
pointerList.querySelectorAll('.presentation-pointer-item').forEach(function (item) {
item.classList.remove('active');
});
if (element) {
element.classList.add('active');
pointerList.classList.add('has-active');
} else {
pointerList.classList.remove('has-active');
}
presentationState.activeVisiblePointerIndex = visibleIndex;
const currentItem = getCurrentItemByVisibleIndex(visibleIndex);
renderActivePointImage(currentItem);
persistLocalState();
syncServerState();
}
function renderSlideNav() {
slideNav.innerHTML = '';
presentationState.slideKeys.forEach(function (slideKey, index) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'presentation-slide-nav-btn' + (slideKey === presentationState.currentSlideKey ? ' active' : '');
button.textContent = index + 1;
button.title = slideKey;
button.addEventListener('click', function () {
loadSlideByKey(slideKey, true);
});
slideNav.appendChild(button);
});
}
function renderSlideMeta(slideKey) {
const slide = getSlideObject(slideKey);
const items = slide ? slide.items : [];
stageTitle.textContent = slide?.title || slideKey || 'Untitled Slide';
stageSubtitle.textContent = slide?.subtitle || '';
stageMeta.textContent = items.length + ' pointer(s) in this slide';
presentationCurrentSlideLabel.textContent = slideKey || '-';
}
function buildPointerElement(item, visibleIndex) {
const li = document.createElement('li');
li.className = 'presentation-pointer-item';
const pointerTitle = normalizeString(item.pointer) || ('Point ' + (visibleIndex + 1));
// li.innerHTML =
// '<div class="presentation-pointer-index">' + (visibleIndex + 1) + '</div>' +
// '<div class="presentation-pointer-text">' +
// '<div class="presentation-pointer-title">' + renderContentBySetting(pointerTitle) + '</div>' +
// '</div>';
const iconValue = normalizeString(item.icon);
let iconHtml = '';
if (iconValue) {
iconHtml = renderExcelIcon(iconValue); // show icon
} else {
iconHtml = (visibleIndex + 1); // fallback number
}
li.innerHTML =
'<div class="presentation-pointer-index">' + iconHtml + '</div>' +
'<div class="presentation-pointer-text">' +
'<div class="presentation-pointer-title">' + renderContentBySetting(pointerTitle) + '</div>' +
'</div>';
li.addEventListener('click', function () {
setActivePointer(li, visibleIndex);
const currentItem = getCurrentItemByVisibleIndex(visibleIndex);
if (isMobileView()) {
openMobileExplanation(pointerTitle, currentItem?.explanation || 'Explanation not available.');
} else {
setExplanationValue(currentItem?.explanation || '');
}
});
requestAnimationFrame(function () {
li.classList.add('show');
});
return li;
}
function renderExcelIcon(iconValue) {
const icon = normalizeString(iconValue);
if (!icon) return '';
// Emoji support
if (/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/u.test(icon)) {
return icon;
}
// Raw HTML
if (icon.includes('<') && icon.includes('>')) {
return icon;
}
// Font Awesome
if (icon.includes('fa-')) {
return '<i class="' + icon + '"></i>';
}
return icon;
}
function renderVisiblePointers() {
pointerList.innerHTML = '';
const items = getSlideItems(presentationState.currentSlideKey);
const visibleCount = presentationState.visiblePointerCount;
for (let i = 0; i < visibleCount; i++) {
const li = buildPointerElement(items[i], i);
li.classList.add('show');
pointerList.appendChild(li);
}
if (visibleCount > 0) {
const activeIndex = presentationState.activeVisiblePointerIndex != null
? Math.min(presentationState.activeVisiblePointerIndex, visibleCount - 1)
: visibleCount - 1;
const pointerElements = pointerList.querySelectorAll('.presentation-pointer-item');
const activeElement = pointerElements[activeIndex];
const activeItem = items[activeIndex];
if (activeElement) {
activeElement.classList.add('active');
}
renderActivePointImage(activeItem);
if (!isMobileView()) {
setExplanationValue(activeItem?.explanation || '');
}
} else {
setExplanationValue('');
renderActivePointImage(null);
}
updateProgress();
Prism.highlightAll();
}
function showStageControls() {
stageActions.classList.add('show');
}
function hideStageControls() {
stageActions.classList.remove('show');
}
function loadSlideByKey(slideKey, resetVisibleState) {
if (!presentationState.slides[slideKey]) {
return;
}
presentationState.currentSlideKey = slideKey;
if (resetVisibleState === true) {
presentationState.currentPointerIndex = 0;
presentationState.visiblePointerCount = 0;
presentationState.activeVisiblePointerIndex = null;
} else {
const total = getSlideItems(slideKey).length;
presentationState.currentPointerIndex = Math.min(presentationState.currentPointerIndex, total);
presentationState.visiblePointerCount = Math.min(presentationState.visiblePointerCount, total);
if (presentationState.visiblePointerCount === 0) {
presentationState.activeVisiblePointerIndex = null;
} else {
presentationState.activeVisiblePointerIndex = Math.min(
presentationState.activeVisiblePointerIndex ?? (presentationState.visiblePointerCount - 1),
presentationState.visiblePointerCount - 1
);
}
}
renderSlideNav();
renderSlideMeta(slideKey);
renderPresentationMeta();
renderVisiblePointers();
persistLocalState();
syncServerState();
syncDetachedWindow();
updateProgress(); // added
}
function revealNextPointer() {
const slideKey = presentationState.currentSlideKey;
const items = getSlideItems(slideKey);
if (!slideKey || !items.length) {
return false;
}
if (presentationState.currentPointerIndex >= items.length) {
return false;
}
const visibleIndex = presentationState.currentPointerIndex;
const item = items[visibleIndex];
const li = buildPointerElement(item, visibleIndex);
pointerList.appendChild(li);
presentationState.currentPointerIndex += 1;
presentationState.visiblePointerCount += 1;
presentationState.activeVisiblePointerIndex = visibleIndex;
pointerList.querySelectorAll('.presentation-pointer-item').forEach(function (node) {
node.classList.remove('active');
});
li.classList.add('active');
renderActivePointImage(item);
if (!isMobileView()) {
setExplanationValue(item.explanation || '');
}
updateProgress();
persistLocalState();
syncServerState();
syncDetachedWindow();
clearFeedback();
return true;
}
function goToNextSlideAndRevealFirstPoint() {
const currentIndex = presentationState.slideKeys.indexOf(presentationState.currentSlideKey);
if (currentIndex < presentationState.slideKeys.length - 1) {
const nextSlideKey = presentationState.slideKeys[currentIndex + 1];
loadSlideByKey(nextSlideKey, true);
revealNextPointer();
return true;
}
showFeedback('You are already on the last slide.', 'warning');
return false;
}
function nextAction() {
const revealed = revealNextPointer();
if (!revealed) {
goToNextSlideAndRevealFirstPoint();
}
}
function hideLastPointer() {
const visibleItems = pointerList.querySelectorAll('.presentation-pointer-item');
if (!visibleItems.length) {
const currentIndex = presentationState.slideKeys.indexOf(presentationState.currentSlideKey);
if (currentIndex > 0) {
const previousSlideKey = presentationState.slideKeys[currentIndex - 1];
loadSlideByKey(previousSlideKey, false);
const total = getSlideItems(previousSlideKey).length;
presentationState.currentPointerIndex = total;
presentationState.visiblePointerCount = total;
presentationState.activeVisiblePointerIndex = total > 0 ? total - 1 : null;
renderVisiblePointers();
persistLocalState();
syncServerState();
syncDetachedWindow();
return;
}
showFeedback('No visible pointer to remove.', 'error');
return;
}
visibleItems[visibleItems.length - 1].remove();
presentationState.currentPointerIndex = Math.max(0, presentationState.currentPointerIndex - 1);
presentationState.visiblePointerCount = Math.max(0, presentationState.visiblePointerCount - 1);
if (presentationState.visiblePointerCount > 0) {
presentationState.activeVisiblePointerIndex = presentationState.visiblePointerCount - 1;
const items = getSlideItems(presentationState.currentSlideKey);
const activeItem = items[presentationState.activeVisiblePointerIndex] || null;
const remaining = pointerList.querySelectorAll('.presentation-pointer-item');
remaining.forEach(function (node) {
node.classList.remove('active');
});
if (remaining.length) {
remaining[remaining.length - 1].classList.add('active');
}
renderActivePointImage(activeItem);
if (!isMobileView()) {
setExplanationValue(activeItem?.explanation || '');
}
} else {
presentationState.activeVisiblePointerIndex = null;
setExplanationValue('');
renderActivePointImage(null);
}
updateProgress();
persistLocalState();
syncServerState();
syncDetachedWindow();
clearFeedback();
}
function replayCurrentSlide() {
if (!presentationState.currentSlideKey) {
showFeedback('Please load a presentation first.', 'error');
return;
}
presentationState.currentPointerIndex = 0;
presentationState.visiblePointerCount = 0;
presentationState.activeVisiblePointerIndex = null;
renderVisiblePointers();
persistLocalState();
syncServerState();
syncDetachedWindow();
clearFeedback();
}
function tryParsePresentationMetaRow(row) {
if (!row || !row.length) {
return null;
}
const firstCell = normalizeString(row[0]).toLowerCase();
if (firstCell !== 'presentation title:') {
return null;
}
return {
title: normalizeString(row[1]),
presentedBy: normalizeString(row[3]),
organizedBy: normalizeString(row[5]),
date: normalizeString(row[7])
};
}
function parseRowsToSlides(rows) {
const slides = {};
let meta = {
title: '',
presentedBy: '',
organizedBy: '',
date: ''
};
if (!rows || !rows.length) {
return { slides, meta };
}
let headerRowIndex = 0;
const maybeMeta = tryParsePresentationMetaRow(rows[0]);
if (maybeMeta) {
meta = maybeMeta;
headerRowIndex = 1;
}
let lastSlideNumber = '';
let lastSlideTitle = '';
let lastSlideSubtitle = '';
rows.forEach(function (row, index) {
if (index <= headerRowIndex) return;
const slideNumberCell = normalizeString(row[0]);
const slideTitleCell = normalizeString(row[1]);
const slideSubtitleCell = normalizeString(row[2]);
// const imageCell = normalizeString(row[3]);
// const pointerCell = normalizeString(row[4]);
// const explanationCell = normalizeString(row[5]);
const imageCell = normalizeString(row[3]);
const iconCell = normalizeString(row[4]); // ✅ ADD THIS
const pointerCell = normalizeString(row[5]); // shifted
const explanationCell = normalizeString(row[6]); // shifted
const slideNumber = slideNumberCell !== '' ? slideNumberCell : lastSlideNumber;
const slideTitle = slideTitleCell !== '' ? slideTitleCell : lastSlideTitle;
const slideSubtitle = slideSubtitleCell !== '' ? slideSubtitleCell : lastSlideSubtitle;
if (!slideNumber) return;
lastSlideNumber = slideNumber;
lastSlideTitle = slideTitle;
lastSlideSubtitle = slideSubtitle;
if (!slides[slideNumber]) {
slides[slideNumber] = {
title: slideTitle || slideNumber,
subtitle: slideSubtitle || '',
items: []
};
} else {
if (slideTitle) slides[slideNumber].title = slideTitle;
if (slideSubtitle) slides[slideNumber].subtitle = slideSubtitle;
}
if (pointerCell === '' && explanationCell === '' && imageCell === '') {
return;
}
// slides[slideNumber].items.push({
// pointer: pointerCell,
// explanation: explanationCell,
// image: imageCell
// });
slides[slideNumber].items.push({
icon: iconCell, // ✅ ADD THIS
pointer: pointerCell,
explanation: explanationCell,
image: imageCell
});
});
return { slides, meta };
}
function sortSlideKeys(slideObject) {
return Object.keys(slideObject).sort(function (a, b) {
const aNum = parseInt(String(a).replace(/[^\d]/g, ''), 10);
const bNum = parseInt(String(b).replace(/[^\d]/g, ''), 10);
if (!isNaN(aNum) && !isNaN(bNum) && aNum !== bNum) {
return aNum - bNum;
}
return String(a).localeCompare(String(b), undefined, {
numeric: true,
sensitivity: 'base'
});
});
}
function handleWorkbook(workbook) {
const firstSheetName = workbook.SheetNames[0];
if (!firstSheetName) {
showFeedback('No worksheet found in the uploaded file.', 'error');
return;
}
const worksheet = workbook.Sheets[firstSheetName];
const rows = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: '' });
const parsed = parseRowsToSlides(rows);
const slides = parsed.slides;
const slideKeys = sortSlideKeys(slides);
if (!slideKeys.length) {
showFeedback('No valid slide data found. Please check your Excel columns and values.', 'error');
return;
}
presentationState.slides = slides;
presentationState.slideKeys = slideKeys;
presentationState.currentSlideKey = slideKeys[0];
presentationState.currentPointerIndex = 0;
presentationState.visiblePointerCount = 0;
presentationState.activeVisiblePointerIndex = null;
presentationState.presentationMeta = parsed.meta || {
title: '',
presentedBy: '',
organizedBy: '',
date: ''
};
ensureSessionKey();
renderSlideNav();
renderPresentationMeta();
loadSlideByKey(slideKeys[0], true);
persistLocalState();
saveSessionToServer();
showStageControls();
showFeedback('Presentation loaded successfully with ' + slideKeys.length + ' slide(s).', 'success');
}
function readFileAndLoad(file) {
if (!file) {
showFeedback('Please choose an Excel or CSV file first.', 'error');
return;
}
const fileName = file.name.toLowerCase();
const reader = new FileReader();
reader.onload = function (event) {
try {
let workbook;
if (fileName.endsWith('.csv')) {
workbook = XLSX.read(event.target.result, { type: 'string' });
} else {
const data = new Uint8Array(event.target.result);
workbook = XLSX.read(data, { type: 'array' });
}
handleWorkbook(workbook);
} catch (error) {
console.error(error);
showFeedback('Could not read the file. Please use a proper Excel or CSV template.', 'error');
}
};
if (fileName.endsWith('.csv')) {
reader.readAsText(file);
} else {
reader.readAsArrayBuffer(file);
}
}
function restoreStateFromStorage() {
try {
const raw = localStorage.getItem(LOCAL_STORAGE_KEY);
if (!raw) {
saveIndicator.textContent = 'Auto save off';
setLayout(presentationState.currentLayout);
renderPresentationMeta();
return;
}
const saved = JSON.parse(raw);
presentationState.renderHtml = !!saved.renderHtml;
if (!saved || !saved.slides || !saved.slideKeys || !saved.slideKeys.length) {
setLayout(presentationState.currentLayout);
renderPresentationMeta();
return;
}
presentationState.slides = saved.slides || {};
presentationState.slideKeys = saved.slideKeys || [];
presentationState.currentSlideKey = saved.currentSlideKey || saved.slideKeys[0];
presentationState.currentPointerIndex = saved.currentPointerIndex || 0;
presentationState.visiblePointerCount = saved.visiblePointerCount || 0;
presentationState.activeVisiblePointerIndex = saved.activeVisiblePointerIndex;
presentationState.sessionKey = saved.sessionKey || localStorage.getItem(SESSION_KEY_STORAGE) || ensureSessionKey();
presentationState.currentLayout = saved.currentLayout || localStorage.getItem(LAYOUT_STORAGE_KEY) || 'top';
presentationState.presentationMeta = saved.presentationMeta || {
title: '',
presentedBy: '',
organizedBy: '',
date: ''
};
setLayout(presentationState.currentLayout);
renderPresentationMeta();
renderSlideNav();
loadSlideByKey(presentationState.currentSlideKey, false);
showStageControls();
saveIndicator.textContent = 'Auto saved';
showFeedback('Saved presentation restored from this browser.', 'success');
} catch (error) {
console.error(error);
saveIndicator.textContent = 'Restore failed';
setLayout(presentationState.currentLayout);
renderPresentationMeta();
}
}
function resetPresentation() {
presentationState.slides = {};
presentationState.slideKeys = [];
presentationState.currentSlideKey = null;
presentationState.currentPointerIndex = 0;
presentationState.visiblePointerCount = 0;
presentationState.activeVisiblePointerIndex = null;
presentationState.renderHtml = false;
presentationState.presentationMeta = {
title: '',
presentedBy: '',
organizedBy: '',
date: ''
};
slideNav.innerHTML = '';
stageTitle.textContent = 'Slide title will appear here';
stageSubtitle.textContent = 'Slide subtitle will appear here';
stageMeta.textContent = '0 pointer(s) in this slide';
pointerList.innerHTML = '';
activeMediaBox.className = 'presentation-active-media-box';
activeMediaBox.innerHTML = '';
explanationEditor.value = '';
progressBar.style.width = '0%';
progressText.textContent = '0 / 0 points visible';
fileInput.value = '';
clearPersistedState();
resetSessionKey();
clearFeedback();
hideStageControls();
saveIndicator.textContent = 'Auto save off';
layoutMiniGroup.classList.remove('show');
renderPresentationMeta();
syncDetachedWindow();
if (detachedWindow && !detachedWindow.closed) {
detachedWindow.close();
detachedWindow = null;
}
explanationPanel.style.display = 'flex';
detachBtn.textContent = '⧉';
detachBtn.title = 'Detach Explanation';
setLayout(localStorage.getItem(LAYOUT_STORAGE_KEY) || 'top');
}
function toggleFullscreen() {
if (!document.fullscreenElement) {
if (pageShell.requestFullscreen) {
pageShell.requestFullscreen();
} else if (pageShell.webkitRequestFullscreen) {
pageShell.webkitRequestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
}
function syncFullscreenUi() {
const isFs = !!document.fullscreenElement;
pageShell.classList.toggle('fullscreen-mode', isFs);
fullscreenBtn.textContent = isFs ? '⤢' : '⛶';
fullscreenBtn.title = isFs ? 'Exit Fullscreen' : 'Fullscreen';
}
function toggleDetachExplanation() {
if (detachedWindow && !detachedWindow.closed) {
detachedWindow.close();
detachedWindow = null;
explanationPanel.style.display = 'flex';
detachBtn.textContent = '⧉';
detachBtn.title = 'Detach Explanation';
return;
}
detachedWindow = window.open('', 'presentationExplanationWindow', 'width=700,height=900,left=100,top=60,resizable=yes,scrollbars=yes');
if (!detachedWindow) {
showFeedback('Popup window blocked by browser. Please allow popups for this page.', 'error');
return;
}
detachedWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>Presentation Explanation</title>
<meta charset="utf-8">
<style>
body{
margin:0;
padding:18px;
font-family: Arial, sans-serif;
background:#f8fafc;
color:#111827;
}
.wrap{
max-width:100%;
}
.title{
font-size:24px;
font-weight:700;
margin-bottom:8px;
line-height:1.4;
}
.subtitle{
color:#6b7280;
margin-bottom:10px;
line-height:1.7;
}
.meta{
font-size:13px;
color:#64748b;
margin-bottom:14px;
}
textarea{
width:100%;
min-height:360px;
border:1px solid #e5e7eb;
border-radius:14px;
padding:16px;
line-height:1.85;
font-size:15px;
resize:vertical;
box-sizing:border-box;
}
</style>
</head>
<body>
<div class="wrap">
<div class="title" id="detachedTitle"></div>
<div class="subtitle" id="detachedSubtitle"></div>
<div class="meta" id="detachedMeta"></div>
<textarea id="detachedExplanationEditor"></textarea>
</div>
</body>
</html>
`);
detachedWindow.document.close();
explanationPanel.style.display = 'none';
detachBtn.textContent = '⇲';
detachBtn.title = 'Attach Explanation Back';
syncDetachedWindow();
// const extEditor = detachedWindow.document.getElementById('detachedExplanationEditor');
// if (extEditor) {
// extEditor.addEventListener('input', function () {
// explanationEditor.value = extEditor.value;
// const item = getCurrentItemByVisibleIndex(presentationState.activeVisiblePointerIndex);
// if (item) {
// item.explanation = explanationEditor.value;
// persistLocalState();
// syncServerState();
// }
// });
// }
const extEditor = detachedWindow.document.getElementById('detachedExplanationEditor');
if (extEditor) {
extEditor.addEventListener('input', function () {
if (presentationState.renderHtml) {
explanationEditor.innerHTML = extEditor.value;
} else {
explanationEditor.textContent = extEditor.value;
}
const item = getCurrentItemByVisibleIndex(presentationState.activeVisiblePointerIndex);
if (item) {
item.explanation = extEditor.value;
persistLocalState();
syncServerState();
syncDetachedWindow();
}
});
}
detachedWindow.addEventListener('beforeunload', function () {
detachedWindow = null;
explanationPanel.style.display = 'flex';
detachBtn.textContent = '⧉';
detachBtn.title = 'Detach Explanation';
});
}
function syncDetachedWindow() {
if (!detachedWindow || detachedWindow.closed) {
return;
}
const slide = getSlideObject(presentationState.currentSlideKey);
const activeItem = getCurrentItemByVisibleIndex(presentationState.activeVisiblePointerIndex);
const titleEl = detachedWindow.document.getElementById('detachedTitle');
const subtitleEl = detachedWindow.document.getElementById('detachedSubtitle');
const metaEl = detachedWindow.document.getElementById('detachedMeta');
const editorEl = detachedWindow.document.getElementById('detachedExplanationEditor');
if (!titleEl || !subtitleEl || !metaEl || !editorEl) {
return;
}
titleEl.textContent = slide?.title || 'Presentation Explanation';
subtitleEl.textContent = slide?.subtitle || '';
metaEl.textContent = activeItem?.pointer ? ('Current Pointer: ' + activeItem.pointer) : 'No pointer selected';
// if (detachedWindow.document.activeElement !== editorEl) {
// editorEl.value = explanationEditor.value || '';
// }
if (detachedWindow.document.activeElement !== editorEl) {
editorEl.value = presentationState.renderHtml
? (explanationEditor.innerHTML || '')
: (explanationEditor.textContent || '');
}
}
async function saveSessionToServer() {
try {
await fetch('/presentation/save', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
session_key: ensureSessionKey(),
slides: presentationState.slides
})
});
} catch (error) {
console.error(error);
}
}
async function syncServerState() {
if (!presentationState.currentSlideKey || !presentationState.slideKeys.length) {
return;
}
try {
await fetch('/presentation/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
session_key: ensureSessionKey(),
slide: presentationState.currentSlideKey,
pointer_index: presentationState.currentPointerIndex,
visible_count: presentationState.visiblePointerCount
})
});
} catch (error) {
console.error(error);
}
}
async function fetchLiveState() {
if (!presentationState.sessionKey) {
return;
}
try {
const response = await fetch('/presentation/state?session_key=' + encodeURIComponent(presentationState.sessionKey));
const result = await response.json();
if (!result || result.status !== 'success' || !result.data) {
return;
}
const serverState = result.data;
if (!serverState.current_slide) {
return;
}
if (!presentationState.slideKeys.length) {
return;
}
const targetSlideKey = serverState.current_slide;
const targetVisibleCount = parseInt(serverState.visible_pointer_count || 0, 10);
if (!presentationState.slides[targetSlideKey]) {
return;
}
if (presentationState.currentSlideKey !== targetSlideKey) {
loadSlideByKey(targetSlideKey, true);
}
if (presentationState.visiblePointerCount !== targetVisibleCount) {
presentationState.currentPointerIndex = targetVisibleCount;
presentationState.visiblePointerCount = targetVisibleCount;
presentationState.activeVisiblePointerIndex = targetVisibleCount > 0 ? targetVisibleCount - 1 : null;
renderVisiblePointers();
}
} catch (error) {
console.error(error);
}
}
function startLivePolling() {
if (syncInterval) {
clearInterval(syncInterval);
}
syncInterval = setInterval(function () {
fetchLiveState();
}, 2000);
}
// explanationEditor.addEventListener('input', function () {
// const item = getCurrentItemByVisibleIndex(presentationState.activeVisiblePointerIndex);
// if (!item) return;
// item.explanation = explanationEditor.value;
// persistLocalState();
// syncServerState();
// syncDetachedWindow();
// });
explanationEditor.addEventListener('input', function () {
const item = getCurrentItemByVisibleIndex(presentationState.activeVisiblePointerIndex);
if (!item) return;
item.explanation = presentationState.renderHtml
? explanationEditor.innerHTML
: explanationEditor.textContent;
persistLocalState();
syncServerState();
syncDetachedWindow();
});
layoutMiniGroup.addEventListener('click', function (event) {
const btn = event.target.closest('.presentation-layout-mini-btn[data-layout]');
if (!btn) return;
spotlightModeEnabled = false;
applySpotlightMode();
setLayout(btn.getAttribute('data-layout'));
persistLocalState();
});
loadBtn.addEventListener('click', function () {
clearFeedback();
readFileAndLoad(fileInput.files && fileInput.files[0] ? fileInput.files[0] : null);
});
resetBtn.addEventListener('click', resetPresentation);
prevBtn.addEventListener('click', function () {
if (!presentationState.currentSlideKey) {
showFeedback('Please load a presentation first.', 'error');
return;
}
hideLastPointer();
});
nextBtn.addEventListener('click', function () {
if (!presentationState.currentSlideKey) {
showFeedback('Please load a presentation first.', 'error');
return;
}
nextAction();
});
document.addEventListener('keydown', function (event) {
// Ignore typing inside editable/input fields
const activeTag = document.activeElement.tagName.toLowerCase();
const isTyping =
activeTag === 'input' ||
activeTag === 'textarea' ||
document.activeElement.isContentEditable;
if (isTyping) {
return;
}
// RIGHT ARROW = Next Point
if (event.key === 'ArrowRight') {
event.preventDefault();
if (!presentationState.currentSlideKey) {
return;
}
nextAction();
}
// LEFT ARROW = Previous Point
if (event.key === 'ArrowLeft') {
event.preventDefault();
if (!presentationState.currentSlideKey) {
return;
}
hideLastPointer();
}
});
replayBtn.addEventListener('click', replayCurrentSlide);
detachBtn.addEventListener('click', toggleDetachExplanation);
fullscreenBtn.addEventListener('click', toggleFullscreen);
mobileCloseBtn.addEventListener('click', closeMobileExplanation);
mobileModal.addEventListener('click', function (event) {
if (event.target === mobileModal) {
closeMobileExplanation();
}
});
document.addEventListener('fullscreenchange', syncFullscreenUi);
document.addEventListener('webkitfullscreenchange', syncFullscreenUi);
window.addEventListener('resize', function () {
if (!isMobileView()) {
closeMobileExplanation();
}
});
document.getElementById('presentationSaveToServerBtn').addEventListener('click', function () {
const file = fileInput.files && fileInput.files[0] ? fileInput.files[0] : null;
if (!file) {
showFeedback('Please choose an Excel file first before saving to server.', 'error');
return;
}
const formData = new FormData();
formData.append('excel_file', file);
const btn = document.getElementById('presentationSaveToServerBtn');
btn.textContent = 'Saving...';
btn.disabled = true;
fetch('/presentation/upload-excel', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
})
.then(function (response) { return response.json(); })
.then(function (result) {
if (result.status === 'success') {
showFeedback('File saved to server: ' + result.filename, 'success');
} else {
showFeedback('Server error: ' + result.message, 'error');
}
})
.catch(function () {
showFeedback('Network error. Could not reach server.', 'error');
})
.finally(function () {
btn.textContent = 'Save to Server';
btn.disabled = false;
});
});
/* ── helpers ── */
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / 1048576).toFixed(2) + ' MB';
}
function formatModifiedDate(ts) {
const d = new Date(ts * 1000);
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
/* ── load a file from a URL into the viewer ── */
function loadExcelFromUrl(url, filename) {
showFeedback('Fetching ' + filename + ' from server...', 'success');
fetch(url)
.then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.arrayBuffer();
})
.then(function (buffer) {
const data = new Uint8Array(buffer);
let workbook;
if (filename.toLowerCase().endsWith('.csv')) {
const text = new TextDecoder().decode(data);
workbook = XLSX.read(text, { type: 'string' });
} else {
workbook = XLSX.read(data, { type: 'array' });
}
handleWorkbook(workbook);
})
.catch(function (err) {
showFeedback('Could not load file from server: ' + err.message, 'error');
});
}
/* ── Save to Server ── */
document.getElementById('presentationSaveToServerBtn').addEventListener('click', function () {
const file = fileInput.files && fileInput.files[0] ? fileInput.files[0] : null;
if (!file) {
showFeedback('Please choose an Excel file first before saving to server.', 'error');
return;
}
const formData = new FormData();
formData.append('excel_file', file);
const btn = document.getElementById('presentationSaveToServerBtn');
btn.textContent = 'Saving...';
btn.disabled = true;
fetch('/presentation/upload-excel', {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
body: formData
})
.then(function (r) { return r.json(); })
.then(function (result) {
if (result.status === 'success') {
showFeedback('Saved to server: ' + result.filename, 'success');
} else {
showFeedback('Server error: ' + result.message, 'error');
}
})
.catch(function () {
showFeedback('Network error. Could not reach server.', 'error');
})
.finally(function () {
btn.textContent = 'Save to Server';
btn.disabled = false;
});
});
/* ── Load Latest ── */
document.getElementById('presentationLoadLatestBtn').addEventListener('click', function () {
const btn = document.getElementById('presentationLoadLatestBtn');
btn.textContent = 'Loading...';
btn.disabled = true;
fetch('/presentation/latest-excel', {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function (r) { return r.json(); })
.then(function (result) {
if (result.status !== 'success') {
showFeedback(result.message || 'No files found on server.', 'error');
return;
}
loadExcelFromUrl(result.url, result.filename);
})
.catch(function () {
showFeedback('Could not reach server.', 'error');
})
.finally(function () {
btn.textContent = 'Load Latest';
btn.disabled = false;
});
});
/* ── All Excels modal ── */
const excelModal = document.getElementById('presentationExcelModal');
const excelModalBody = document.getElementById('presentationExcelModalBody');
document.getElementById('presentationAllExcelsBtn').addEventListener('click', function () {
excelModal.style.display = 'flex';
excelModalBody.innerHTML = '<div style="color:var(--presentation-text-soft,#6b7280);font-size:0.9rem;">Loading...</div>';
fetch('/presentation/list-excels', {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function (r) { return r.json(); })
.then(function (result) {
if (!result.files || !result.files.length) {
excelModalBody.innerHTML =
'<div style="color:var(--presentation-text-soft,#6b7280);font-size:0.9rem;padding:20px 0;">No Excel files found on server.</div>';
return;
}
let html = '<div style="display:flex;flex-direction:column;gap:10px;">';
result.files.forEach(function (file, index) {
const isLatest = index === 0;
html += '<div style="' +
'display:flex;align-items:center;gap:12px;' +
'padding:12px 14px;border-radius:14px;' +
'border:1px solid var(--presentation-border,rgba(0,0,0,0.08));' +
'background:var(--presentation-panel-bg,#fff);' +
(isLatest ? 'border-color:rgba(13,110,253,0.35);' : '') +
'">';
/* icon */
html += '<div style="' +
'width:42px;height:42px;min-width:42px;border-radius:12px;' +
'background:rgba(13,110,253,0.10);' +
'display:flex;align-items:center;justify-content:center;font-size:1.3rem;">📊</div>';
/* meta */
html += '<div style="flex:1;min-width:0;">' +
'<div style="font-weight:700;font-size:0.9rem;color:var(--presentation-text,#1f2937);' +
'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" title="' + file.name + '">' +
file.name +
(isLatest
? ' <span style="font-size:0.72rem;background:rgba(13,110,253,0.12);' +
'color:#0d6efd;padding:2px 8px;border-radius:999px;font-weight:700;">Latest</span>'
: '') +
'</div>' +
'<div style="font-size:0.78rem;color:var(--presentation-text-soft,#6b7280);margin-top:2px;">' +
formatFileSize(file.size) + ' · ' + formatModifiedDate(file.modified) +
'</div>' +
'</div>';
/* Load + Download buttons */
html += '<div style="display:flex;gap:6px;align-items:center;">';
html += '<button type="button" ' +
'data-url="' + file.url + '" ' +
'data-name="' + file.name + '" ' +
'class="presentation-excel-load-btn" ' +
'style="' +
'border:1px solid var(--presentation-border,rgba(0,0,0,0.08));' +
'background:var(--presentation-primary,#0d6efd);color:#fff;' +
'border-radius:10px;padding:7px 14px;font-weight:700;' +
'font-size:0.82rem;cursor:pointer;white-space:nowrap;' +
'transition:opacity 0.18s;">Load</button>';
html += '<a href="' + file.url + '" download="' + file.name + '" ' +
'style="' +
'display:inline-flex;align-items:center;gap:5px;' +
'border:1px solid var(--presentation-border,rgba(0,0,0,0.08));' +
'background:var(--presentation-panel-bg,#fff);' +
'color:var(--presentation-text,#1f2937);' +
'border-radius:10px;padding:7px 14px;font-weight:700;' +
'font-size:0.82rem;cursor:pointer;white-space:nowrap;' +
'text-decoration:none;transition:opacity 0.18s;">⬇ Download</a>';
html += '</div>';
html += '</div>';
});
html += '</div>';
excelModalBody.innerHTML = html;
/* wire up load buttons */
excelModalBody.querySelectorAll('.presentation-excel-load-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
const url = btn.getAttribute('data-url');
const name = btn.getAttribute('data-name');
excelModal.style.display = 'none';
loadExcelFromUrl(url, name);
});
});
})
.catch(function () {
excelModalBody.innerHTML =
'<div style="color:#dc3545;font-size:0.9rem;">Could not load file list from server.</div>';
});
});
document.getElementById('presentationExcelModalClose').addEventListener('click', function () {
excelModal.style.display = 'none';
});
excelModal.addEventListener('click', function (e) {
if (e.target === excelModal) excelModal.style.display = 'none';
});
resetSessionKey();
setLayout(presentationState.currentLayout);
renderPresentationMeta();
restoreStateFromStorage();
updateHtmlToggleUi();
startLivePolling();
presentationState.fontScale = 1.76;
applyFontSize();
})();
</script>
<?php require_once __DIR__ . '/../partials/footer.php'; ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/prismjs/prism.min.js"></script>
<!-- Load languages you need -->
<script src="https://cdn.jsdelivr.net/npm/prismjs/components/prism-java.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs/components/prism-php.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs/components/prism-javascript.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs/components/prism-markup.min.js"></script>
</pre></xmp>