Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
31ed25f
Add presenter notes feature
aplicacionesitgpc Apr 6, 2026
1b15b28
Make notes/interactions panels vertically resizable
aplicacionesitgpc Apr 6, 2026
4396cab
Fix notes textarea not filling its grid pane
aplicacionesitgpc Apr 6, 2026
4f0bd8b
Add rich text editing to presenter notes with Quill.js
aplicacionesitgpc Apr 6, 2026
71c848d
Fix Quill toolbar disappearing and add keyboard shortcuts
aplicacionesitgpc Apr 6, 2026
1342fcd
Fix Quill toolbar, JS error, and preserve note formatting
aplicacionesitgpc Apr 7, 2026
71f289b
Fix notes disappearing after typing
aplicacionesitgpc Apr 7, 2026
07da7ce
Fix all compiler warnings
aplicacionesitgpc Apr 7, 2026
a95629f
Rewrite Quill integration: fix toolbar and note persistence
aplicacionesitgpc Apr 7, 2026
0b9a604
Change join screen from full-screen overlay to side-by-side panel
aplicacionesitgpc Apr 7, 2026
27f9b1b
Shrink join screen panel to 20% width, slides fill full height
aplicacionesitgpc Apr 7, 2026
1e320f4
Fix slide scaling: fill full viewport height with object-fit contain
aplicacionesitgpc Apr 7, 2026
83ca4c4
Respect aspect ratio for slide and join panel sizing
aplicacionesitgpc Apr 7, 2026
9203fdf
Fix black bars: size wrapper from full-width slide ratio, shrink QR
aplicacionesitgpc Apr 7, 2026
6e140ef
Fix wrapper height: calculate from actual slide width, not full width
aplicacionesitgpc Apr 7, 2026
c241a91
Measure actual slides div width instead of guessing percentages
aplicacionesitgpc Apr 7, 2026
490d28c
Fill full viewport: all panels use 100vh, remove JS height calc
aplicacionesitgpc Apr 7, 2026
19a3e46
Proportional scaling: calculate wrapper to exactly fit slide aspect r…
aplicacionesitgpc Apr 7, 2026
80c510d
Simplify to pure CSS 100vh layout, fix grid stretch
aplicacionesitgpc Apr 7, 2026
fcb43e7
Change join screen to overlay on top of slide instead of side panel
aplicacionesitgpc Apr 7, 2026
ba98015
Join screen as top banner, slide fills remaining viewport height
aplicacionesitgpc Apr 7, 2026
28b85e3
Fix join banner: cap height at 120px, fixed 80px QR code
aplicacionesitgpc Apr 7, 2026
b86deaa
Restore original slide layout, join banner as absolute overlay at top
aplicacionesitgpc Apr 7, 2026
b329e99
Join banner in flow above slide, chat matches slide height
aplicacionesitgpc Apr 7, 2026
d4db3d9
Change presenter background from black to light gray
aplicacionesitgpc Apr 7, 2026
8646db0
Fix presenter layout: center slide, match chat bg, hide URL bar
aplicacionesitgpc Apr 7, 2026
3984dac
Fix fullscreen to use document.documentElement
aplicacionesitgpc Apr 7, 2026
6b4042e
Fix presenter: center slide, add join URL, auto-fullscreen
aplicacionesitgpc Apr 7, 2026
11a1959
Remove auto-fullscreen from presenter
aplicacionesitgpc Apr 7, 2026
31410a0
Center presenter content vertically, match chat to slide height
aplicacionesitgpc Apr 7, 2026
bd75afe
Wrap grid in flex centering container for vertical alignment
aplicacionesitgpc Apr 7, 2026
bf5dec7
Add individual slide image insertion to presentations
aplicacionesitgpc Apr 7, 2026
f18b5cc
Add drag-and-drop slide reordering and '+' add slide button
aplicacionesitgpc Apr 7, 2026
2dfcb8c
Fix: replace SortableJS with native HTML5 drag-and-drop
aplicacionesitgpc Apr 7, 2026
7dec745
Fix: move add-slide button outside phx-update=ignore container
aplicacionesitgpc Apr 7, 2026
96b704d
Fix PresentationState module reference and add-slide button layout
aplicacionesitgpc Apr 7, 2026
cb1a3bf
Fix slide thumbnails: restore contents class and grid layout
aplicacionesitgpc Apr 7, 2026
63b5a70
Fix slide thumbnail sizing in manage page grid layout
aplicacionesitgpc Apr 7, 2026
bbab131
Fix add-slide button: use .link component for live_patch navigation
aplicacionesitgpc Apr 7, 2026
8487f87
Fix add-slide button: use phx-click event instead of link navigation
aplicacionesitgpc Apr 7, 2026
0f4739d
Rework add-slide and drag-and-drop slide reordering
aplicacionesitgpc Apr 7, 2026
c8c252f
Add visible + button at end of slide thumbnail strip
aplicacionesitgpc Apr 7, 2026
d9b6148
Fix slide upload: copy to stable temp file before consume cleans up
aplicacionesitgpc Apr 7, 2026
4cda992
Fix insert_slide and reorder_slides to support S3 storage
aplicacionesitgpc Apr 7, 2026
83ae596
Add debug logging to insert_slide to diagnose file path issue
aplicacionesitgpc Apr 7, 2026
c0338bc
Fix drag-and-drop slide reorder: don't manipulate DOM, let LiveView r…
aplicacionesitgpc Apr 7, 2026
cba5965
Add delete slide functionality from manage page
aplicacionesitgpc Apr 7, 2026
9d2bab7
Fix delete_slide guard clause and add error logging
aplicacionesitgpc Apr 7, 2026
ee924f2
Fix slide thumbnail scroll position reset on click
aplicacionesitgpc Apr 7, 2026
f0d96f4
Optimize reorder/delete: rename files in-place, no hash change
aplicacionesitgpc Apr 7, 2026
21c5343
Fix delete slide (File.rename! doesn't exist) and scroll position
aplicacionesitgpc Apr 7, 2026
1fd9c9b
Add error logging to reorder-slides handler for debugging
aplicacionesitgpc Apr 7, 2026
d55c5a0
Revert to safe copy-to-new-hash for reorder and delete
aplicacionesitgpc Apr 7, 2026
5c0f7ef
Move join info to floating top-left panel in presenter view
aplicacionesitgpc Apr 7, 2026
be1e6df
Make embed interactions use full screen width and height
aplicacionesitgpc Apr 7, 2026
4742c13
Fix embed iframe height in presenter view
aplicacionesitgpc Apr 7, 2026
ee49b36
Fix embed component height chain for iframes
aplicacionesitgpc Apr 7, 2026
b6e3621
Fix embed iframe not filling full screen in presenter and attendee views
aplicacionesitgpc Apr 7, 2026
ec10704
Fix presenter slider jumping multiple slides on arrow key press
aplicacionesitgpc Apr 7, 2026
478713c
Stop arrow keys from navigating slides when typing in presenter notes
aplicacionesitgpc Apr 7, 2026
4c6b658
Fix presenter arrow keys broken by null check on synthetic events
aplicacionesitgpc Apr 7, 2026
36eb119
Fix presenter slides not updating on thumbnail click
aplicacionesitgpc Apr 7, 2026
45e3e90
Remove slider destroy() call that broke reinitialization
aplicacionesitgpc Apr 7, 2026
f478d63
Only scroll thumbnails when the current slide actually changes
aplicacionesitgpc Apr 7, 2026
2f562b7
Duplicate presenter notes when duplicating an event
aplicacionesitgpc Apr 8, 2026
7b5e62f
Remove redundant import Ecto.Query in duplicate_presenter_notes
aplicacionesitgpc Apr 12, 2026
200e1b5
Apply mix format to fix CI formatting check
aplicacionesitgpc Apr 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions assets/css/app.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
@import 'air-datepicker/air-datepicker.css';
@import 'animate.css/animate.min.css';
@import 'quill/dist/quill.snow.css';

/* Presenter notes – make the Quill container fill its flex/grid pane */
#presenter-notes-editor .ql-toolbar {
flex-shrink: 0;
border-left: none;
border-right: none;
}
#presenter-notes-editor .ql-container {
flex: 1;
min-height: 0;
overflow-y: auto;
font-size: 0.875rem;
}

@import 'tailwindcss';
@import './theme.css' layer(theme);
Expand Down
129 changes: 115 additions & 14 deletions assets/js/app.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html";
import PresenterNotes from "./presenter_notes";
// Establish Phoenix Socket and LiveView configuration.
import { Socket, Presence } from "phoenix";
import { LiveSocket } from "phoenix_live_view";
Expand Down Expand Up @@ -568,17 +569,24 @@ Hooks.QRCode = {
"/e/" +
this.el.dataset.code
: window.location.href;
this.el.style.width = document.documentElement.clientWidth * 0.27 + "px";
this.el.style.height = document.documentElement.clientWidth * 0.27 + "px";

var qrSize;
if (this.el.dataset.panel) {
// Top banner mode: fixed small QR code
qrSize = 80;
} else if (this.el.dataset.dynamic) {
qrSize = document.documentElement.clientWidth * 0.25;
} else {
qrSize = 240;
}

this.el.style.width = (qrSize * 1.08) + "px";
this.el.style.height = (qrSize * 1.08) + "px";

if (this.qrCode == null) {
this.qrCode = new QRCodeStyling({
width: this.el.dataset.dynamic
? document.documentElement.clientWidth * 0.25
: 240,
height: this.el.dataset.dynamic
? document.documentElement.clientWidth * 0.25
: 240,
width: qrSize,
height: qrSize,
margin: 0,
data: url,
cornersSquareOptions: {
Expand All @@ -600,12 +608,8 @@ Hooks.QRCode = {
this.qrCode.append(this.el);
} else {
this.qrCode.update({
width: this.el.dataset.dynamic
? document.documentElement.clientWidth * 0.25
: 240,
height: this.el.dataset.dynamic
? document.documentElement.clientWidth * 0.25
: 240,
width: qrSize,
height: qrSize,
});
}
},
Expand Down Expand Up @@ -682,6 +686,103 @@ Hooks.CSVDownloader = {
}
};

Hooks.PresenterNotes = PresenterNotes;

Hooks.SortableSlides = {
beforeUpdate() {
// Preserve scroll position before LiveView patches the DOM
this._scrollLeft = this.el.scrollLeft;
},
updated() {
// Restore scroll position after LiveView patches the DOM
if (this._scrollLeft !== undefined) {
this.el.scrollLeft = this._scrollLeft;
}
// Re-bind drag events
this.mounted();
},
mounted() {
this.dragSrcIdx = null;
const container = this.el;

container.ondragstart = (e) => {
const item = e.target.closest("[data-slide-index]");
if (!item) return;
this.dragSrcIdx = parseInt(item.dataset.slideIndex);
item.style.opacity = "0.3";
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", item.dataset.slideIndex);
};

container.ondragover = (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
// Highlight drop target
const target = e.target.closest("[data-slide-index]");
container.querySelectorAll("[data-slide-index]").forEach(el => {
el.style.borderLeft = "";
el.style.borderRight = "";
});
if (target && parseInt(target.dataset.slideIndex) !== this.dragSrcIdx) {
const items = Array.from(container.querySelectorAll("[data-slide-index]"));
const srcPos = items.findIndex(el => parseInt(el.dataset.slideIndex) === this.dragSrcIdx);
const tgtPos = items.indexOf(target);
if (tgtPos > srcPos) {
target.style.borderRight = "3px solid #7c3aed";
} else {
target.style.borderLeft = "3px solid #7c3aed";
}
}
};

container.ondragleave = (e) => {
const target = e.target.closest("[data-slide-index]");
if (target) {
target.style.borderLeft = "";
target.style.borderRight = "";
}
};

container.ondragend = (e) => {
// Clear all visual indicators
container.querySelectorAll("[data-slide-index]").forEach(el => {
el.style.opacity = "";
el.style.borderLeft = "";
el.style.borderRight = "";
});
this.dragSrcIdx = null;
};

container.ondrop = (e) => {
e.preventDefault();
const target = e.target.closest("[data-slide-index]");
if (!target || this.dragSrcIdx === null) return;
const targetIdx = parseInt(target.dataset.slideIndex);
if (targetIdx === this.dragSrcIdx) return;

// Build new order: take current order, move dragSrc to target position
const items = Array.from(container.querySelectorAll("[data-slide-index]"));
const currentOrder = items.map(el => parseInt(el.dataset.slideIndex));
const fromPos = currentOrder.indexOf(this.dragSrcIdx);
const toPos = currentOrder.indexOf(targetIdx);

// Remove from old position, insert at new position
currentOrder.splice(fromPos, 1);
currentOrder.splice(toPos, 0, this.dragSrcIdx);

// Clear visuals
container.querySelectorAll("[data-slide-index]").forEach(el => {
el.style.opacity = "";
el.style.borderLeft = "";
el.style.borderRight = "";
});

this.dragSrcIdx = null;
this.pushEvent("reorder-slides", { order: currentOrder });
};
}
};

// Merge our custom hooks with the existing hooks
Object.assign(Hooks, CustomHooks);

Expand Down
71 changes: 42 additions & 29 deletions assets/js/manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ export class Manager {
setTimeout(
() => {
const slidesLayout = document.getElementById("slides-layout");
if (!slidesLayout) return;
const layoutWidth = slidesLayout.clientWidth;
const elementWidth = el.children[0].scrollWidth;
const elementWidth = el.offsetWidth;
const scrollPosition =
el.children[0].offsetLeft - layoutWidth / 2 + elementWidth / 2;
el.offsetLeft - layoutWidth / 2 + elementWidth / 2;

slidesLayout.scrollTo({
left: scrollPosition,
Expand All @@ -32,18 +33,23 @@ export class Manager {
});

window.addEventListener("keydown", (e) => {
if ((e.target.tagName || "").toLowerCase() != "input") {

switch (e.key) {
case "ArrowLeft":
e.preventDefault();
this.prevPage();
break;
case "ArrowRight":
e.preventDefault();
this.nextPage();
break;
}
const tag = (e.target.tagName || "").toLowerCase();
// Don't navigate slides when focus is in an input, textarea,
// contenteditable element (e.g. Quill presenter notes), or select.
if (tag === "input" || tag === "textarea" || tag === "select" ||
e.target.isContentEditable || (e.target.closest && e.target.closest(".ql-editor"))) {
return;
}

switch (e.key) {
case "ArrowLeft":
e.preventDefault();
this.prevPage();
break;
case "ArrowRight":
e.preventDefault();
this.nextPage();
break;
}
});

Expand Down Expand Up @@ -140,22 +146,29 @@ export class Manager {
}

update() {
const prevPage = this.currentPage;
this.currentPage = parseInt(this.context.el.dataset.currentPage);
var el = document.getElementById("slide-preview-" + this.currentPage);

if (el) {
setTimeout(() => {
const slidesLayout = document.getElementById("slides-layout");
const layoutWidth = slidesLayout.clientWidth;
const elementWidth = el.children[0].scrollWidth;
const scrollPosition =
el.children[0].offsetLeft - layoutWidth / 2 + elementWidth / 2;

slidesLayout.scrollTo({
left: scrollPosition,
behavior: "smooth",
});
}, 50);

// Only scroll thumbnails when the slide actually changed,
// not on every LiveView re-render (e.g. opening a modal).
if (this.currentPage !== prevPage) {
var el = document.getElementById("slide-preview-" + this.currentPage);

if (el) {
setTimeout(() => {
const slidesLayout = document.getElementById("slides-layout");
if (!slidesLayout) return;
const layoutWidth = slidesLayout.clientWidth;
const elementWidth = el.offsetWidth;
const scrollPosition =
el.offsetLeft - layoutWidth / 2 + elementWidth / 2;

slidesLayout.scrollTo({
left: scrollPosition,
behavior: "smooth",
});
}, 50);
}
}

this.initPreview();
Expand Down
Loading