Open
Conversation
Adds per-slide presenter notes to the event manager page. Notes are automatically extracted from PPTX speaker notes on upload (using SweetXml to parse notesSlide XML), stored in a new presenter_notes table, and displayed in an editable textarea in the manage view that auto-saves on blur. PDF and PPT uploads are unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the fixed-height notes textarea with a draggable Split layout inside the interactions column. Notes start at 2/3 height and interactions at 1/3, with a ••• gutter the presenter can drag to resize freely. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switch the notes pane to a flex column layout so the textarea stretches to fill all available height in the Split row pane, rather than staying at a fixed intrinsic height. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the plain textarea with a Quill editor (Snow theme) supporting bold, italic, underline, strikethrough, headings, font color/highlight, ordered/unordered lists, and a clean-format button. Content is stored as HTML. The editor auto-saves 800 ms after the user stops typing and updates instantly when navigating slides via the PresenterNotes LiveView hook. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move all Quill-managed DOM (toolbar + editor) inside a single phx-update="ignore" wrapper so LiveView never patches it away. Use Quill's modules.toolbar to point at the pre-rendered container. Add explicit CMD/Ctrl keyboard bindings for bold, italic, underline, and strikethrough. Add CSS overrides so the editor fills its flex pane. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace data-content attribute with a <script type="application/json"> carrier element to avoid HTML-in-attribute encoding issues that caused the Unexpected token '&' JS syntax error - Remove broken custom keyboard bindings (Quill 2.x has CMD+B/I/U built-in) - Rewrite PPTX notes extractor to produce <p>-tagged HTML per paragraph instead of a flat space-joined string, preserving line breaks and paragraph structure when notes are imported from PPTX files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: save-note updated @current_note, re-rendering the <script> carrier and triggering updated() while the user was still in the editor, which reset Quill to the old (or empty) content. Fix: - Track slide position (data-position) in the hook; updated() only reloads the editor when the slide actually changes, never on a save-triggered re-render - Remove @current_note update from save-note handler — content is already in the editor, pushing it back served no purpose Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move save-note handle_event next to other handle_event clauses - Replace deprecated <%# with <%!-- comment syntax in HEEx template - Remove unused default argument values from jpg_upload/7 and success/6 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root causes:
- Toolbar never showed because { container: emptyDiv } tells Quill to
USE an existing toolbar, not CREATE one. Pass the options array instead
so Quill builds the toolbar itself.
- Notes disappeared on slide change because the complex phx-update/script/
updated() approach had race conditions with LiveView DOM patching.
New approach (canonical LiveView + third-party editor pattern):
- phx-update="ignore" on the entire hook element so LiveView never
touches the Quill DOM (toolbar, editor, formatting) after mount
- data-initial-content for the first render (read once in mounted())
- push_event("load-note") from the server for slide navigation
- pushEvent("save-note") from the client for auto-save
- No updated() callback, no <script> carrier, no data-position tracking
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The "instructions to join" screen now appears as a left panel alongside the presentation slides instead of covering the entire screen. QR code sizing adapts to the panel width. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reduced panel from w-2/5 to w-1/5, scaled down text sizes, and made slides fill the full viewport height with object-contain scaling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add CSS rules targeting tiny-slider wrapper elements to ensure they all fill 100vh. Slide images now use width/height 100% with object-fit: contain so they scale proportionally with viewport size. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use JS to calculate the correct wrapper height from the slide image's natural aspect ratio and available width, capped at viewport height. Both the join panel and slide inherit this height so they stay proportional. Recalculates on resize, join-screen toggle, and chat-visible toggle. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Calculate wrapper height from the slide's aspect ratio at full available width, so the join panel doesn't reduce slide height. QR code now caps at 40% of panel height to avoid overflow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The wrapper height now accounts for the join panel's 20% width so the slide and panel heights match exactly. Chat toggle recalculates with a delay to let the grid animation settle. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fitSlideArea() now reads slidesDiv.clientWidth directly, which is correct regardless of chat/join panel combination. All callers use requestAnimationFrame or setTimeout to ensure measurements happen after layout settles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All panels (join, slides, chat) now fill the full viewport height. Slide images use object-fit:contain within 100vh to preserve aspect ratio. Removed fitSlideArea() JS — pure CSS handles everything. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…atio JS now computes both width and height of the wrapper so the slide fills it with zero wasted space. The join panel width is set proportionally (20% of wrapper). The wrapper is centered with margin:auto. Everything scales proportionally when the window resizes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove all JS fitSlideArea() sizing. Use CSS-only approach: grid uses items-stretch + h-screen so all children fill viewport height. Slide image uses max-width/max-height 100% with object-fit:contain. Chat, join panel, and slides all fill the full viewport height naturally. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Restore original slide layout (grid with chat + slides). Join screen is now an absolute-positioned overlay on the left 25% of the slide area with semi-transparent white background and shadow. This avoids all the proportional sizing issues of the side-by-side approach. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Join info displayed as a horizontal bar at the top with QR code on the left and text on the right. Slides area uses flex-1 to fill the remaining viewport height below the banner. Grid uses items-stretch + h-screen so all columns fill the full viewport. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The join banner no longer stretches with the viewport. QR code is a fixed 80px, banner max-height is 120px with flex-grow-0. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reverted grid and slide structure to match the original codebase exactly. Join screen is now an absolute-positioned banner at the top of the slide area that floats over the slide without affecting layout. Removed URL text from the banner — only shows QR code and event code. Removed all custom tns CSS overrides. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Join screen is a flex-shrink-0 banner in normal document flow above the slide — no overlay. Slide area uses flex-1 to fill remaining height. Grid uses items-stretch so chat column matches the slides column height. CSS propagates height through all tns wrappers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces the high-contrast black background with light gray (#f3f4f6) so empty space around slides blends with typical white/light slide backgrounds, matching Google Slides' presenter mode approach. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove #slide-content from CSS height chain so flex-1 works correctly and the slide centers vertically (not stuck at top) - Change chat panel background from black to gray-100 to match the slide area background - Add location=no,menubar=no,toolbar=no to presenter popup window Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous fullscreen code targeted the #presenter div and had broken try/catch logic that silently swallowed errors. Now uses document.documentElement for true browser fullscreen which hides the address bar. Also reverts ineffective window.open params. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add justify-center to #slides container to vertically center the slide content instead of pinning it to the top - Restore #slide-content in the CSS height chain so tns wrappers get proper height context for vertical centering - Add the full join URL to the join info banner (QR + URL + code) - Auto-enter fullscreen on first click to hide browser URL bar Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Safari shows the toolbar in fullscreen regardless — this is a browser setting (View > Always Show Toolbar in Full Screen), not controllable from JavaScript. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use JS to calculate slide container height from the actual image aspect ratio. Use CSS grid content-center to vertically center the entire layout (join banner + slide + chat) in the viewport. Chat column stretches to match via items-stretch. Recalculates on window resize and join banner toggle. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use fully qualified Claper.Presentations.PresentationState in reorder_slides/2 (alias is defined after the function) - Fix '+' button: wrap slides strip and button in a flex container so the button appears at the end of the strip, not overlapping slides. Slides scroll in flex-1, button is flex-shrink-0. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous change broke the grid-rows-[0.3fr_10px_1fr] layout by removing the 'contents' class from slide buttons. Restored 'contents' so images are direct flex children of #slides-layout, preserving original thumbnail sizing. The '+' button sits in a flex wrapper alongside the scroll container, occupying the first grid row correctly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add overflow-hidden and min-h-0 to wrapper div so it respects the 0.3fr grid row height, and max-h-full to images to constrain them within bounds. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The raw <a data-phx-link="patch"> was not triggering LiveView navigation.
Replaced with the proper <.link patch={...}> component.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The live_patch link wasn't triggering. Replaced with a phx-click event handler that directly assigns the slide creation state. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move add-slide button out of thumbnail area into interaction modal (option-5 "Slide image") using phx-click instead of link navigation - Always insert new slides at the end (remove position selector) - Remove phx-update="ignore" from slides container so LiveView can update thumbnails after reorder/insert - Fix drag-and-drop: use div wrappers instead of button+contents, add updated() callback to re-bind events after DOM patches - Remove push_navigate from save-slide and reorder-slides handlers to avoid full page reloads that reset the UI - Replace × with × to avoid HTML entity parse issues Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The add-slide button is now a visible dashed-border + icon at the end of the slide thumbnails, inside the same container (no phx-update=ignore so phx-click works). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phoenix removes uploaded temp files after consume_uploaded_entries returns. Copy to a stable temp path first so insert_slide can read it. Also close modal on all error paths and add error logging. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous implementation only worked with local filesystem storage. When running with S3, local files don't exist (deleted after upload), causing File.cp! to fail with :enoent. Refactored to use storage-agnostic helpers: - copy_slide_to_new_hash: uploads a local file to the correct storage - copy_slide_between_hashes: copies between storage locations (S3 copy or local File.cp) - clear_slide_hash: removes old hash directory/prefix from storage Also fixed Range warning when appending at end (file_insert_index > length). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Logs storage mode, paths, directory existence, and file listing to help identify why File.cp fails with :enoent. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e-render The JS hook was manually reordering DOM elements and then LiveView also re-rendered, causing duplicate/misplaced thumbnails. Now the hook only calculates the new order array (splice-based) and pushes it to the server. LiveView handles the DOM update. Added visual feedback: purple border indicator shows where the slide will be inserted during drag. Cleaned up all visual states on dragend. Also removed debug logging from insert_slide. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New delete_slide/2 in Presentations context: copies files to new hash skipping the deleted slide, removes interactions at that position, shifts remaining positions down, fixes presentation state - Event handler in manage.ex with confirmation dialog - Red X button appears on thumbnail hover (hidden for last slide) - Supports both local and S3 storage via existing helpers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The guard `when pf.length > 1` doesn't work reliably with struct field
access. Changed to pattern match `%PresentationFile{length: length}`
and guard on the bound variable instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
LiveView DOM patches reset the scroll position of the slides container. Added beforeUpdate/updated hooks on SortableSlides to save and restore scrollLeft across patches. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reorder and delete now rename files within the same directory instead
of copying all files to a new hash. This is instant on local filesystem
and avoids forcing the browser to re-download all slide images.
- reorder_slides: two-pass rename (to temp names, then final positions)
- delete_slide: remove file, shift subsequent files down by 1
- No hash change means same URLs, so added ?v= cache buster using
updated_at timestamp on thumbnail images
- Removed push_event("page-manage") from reorder handler to prevent
unwanted scroll reset
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace File.rename! with File.rename + :ok pattern match (Elixir has no bang version of File.rename) - Fix scroll calculation: use el.offsetLeft/offsetWidth instead of el.children[0] which returns 0 due to position:relative on parent Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The in-place rename approach corrupted files when it failed mid-way (File.rename! doesn't exist in Elixir, causing partial renames to tmp_ prefixes). Reverted to the same copy-to-new-hash pattern used by insert_slide, which is safe: old files are only deleted on success, new hash is cleaned up on failure. Removed broken delete_slide_file and reorder_slide_files helpers. Removed ?v= cache buster (new hash already busts browser cache). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The join info panel was inside the slide area as a flex child, causing it to overlap with poll/quiz interaction overlays. Moved it to an absolute-positioned floating panel in the top-left corner of the presenter with z-50 (above z-30 interaction overlays). Compact design with semi-transparent background and blur. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The embed wrapper was constrained to lg:w-1/3 (1/3 of screen width), making custom iframes like LLM UIs unusable. Changed to full-width with top-16/bottom-0 to fill the available viewport below the header. Propagated h-full and flex-col through the component chain so the iframe gets maximum space. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The #slides and #slide-content divs had no explicit height, so the iframe (which uses height:100%) collapsed. Added h-full to #slides and flex-1 min-h-0 to #slide-content so the embed fills the grid cell. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add scoped iframe CSS (width/height 100%) to EmbedIframeComponent so it applies in both views, add h-full to extended-embed and slider-wrapper to complete the height propagation chain. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three issues caused the "crazy" slide movement: 1. update() recreated tns slider without destroying the old instance, leaving ghost DOM elements and event listeners 2. update() never read the new currentPage from DOM data attributes, so the new slider was created at the stale position then goTo'd 3. Both storage and page events fired goTo() for the same navigation, causing redundant transitions Now update() checks if only the page changed (same hash/maxPage) and uses goTo() directly instead of rebuilding. When structural changes occur (slides added/removed), old slider is destroyed before rebuild. Storage event is suppressed when page event already handled it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The keydown handler only checked for <input> tags, but the Quill rich text editor uses a contenteditable <div>. Now also skips textarea, select, and any contenteditable element so arrow keys work normally inside the notes editor. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The presenter window dispatches synthetic KeyboardEvent to window.opener, where e.target is the window object (not a DOM element). Calling e.target.closest() on window throws because closest is an Element method. Add a guard check before calling closest(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
LiveView DOM patching corrupts tiny-slider's internal wrapper elements, making slider.goTo() fail silently. Restore always-rebuild behavior on update() but now properly destroying the old slider first and reading updated data attributes from the DOM before reinitializing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tns.destroy() strips internal DOM wrappers, and reinitializing on LiveView-patched DOM fails. Revert to original behavior of simply overwriting the slider reference while keeping the fix that reads updated data attributes before rebuilding. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Manager update() runs on every LiveView re-render, including when opening modals. It was unconditionally scrolling the thumbnail strip, causing a visible jump-and-scroll-back. Now only scrolls when the page position actually changed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The duplicate_event function copied polls, forms, embeds, and quizzes but skipped presenter notes. Add duplicate_presenter_notes step to the Ecto.Multi chain that copies all notes from the original presentation file to the new one. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds per-slide presenter notes to the event manager page. Notes are automatically extracted from PPTX speaker notes on upload (using SweetXml to parse notesSlide XML), stored in a new presenter_notes table, and displayed in an editable textarea in the manage view that auto-saves on blur. PDF and PPT uploads are unaffected.