From 96a21f6c7bd66b98777b3858f39ec8615bc3a7b8 Mon Sep 17 00:00:00 2001 From: cub8 Date: Fri, 30 Oct 2026 19:05:05 +0100 Subject: [PATCH 1/2] Minor refactor --- app/views/story_group_invites/_code.html.haml | 6 +++--- app/views/story_group_invites/_quick.html.haml | 13 +++++++------ app/views/story_group_invites/quick.html.haml | 10 ++++++++-- app/views/story_group_invites/show.html.haml | 10 ++++++++-- app/views/students/index.html.haml | 5 +++-- 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/app/views/story_group_invites/_code.html.haml b/app/views/story_group_invites/_code.html.haml index 3a34da8..c0c2f61 100644 --- a/app/views/story_group_invites/_code.html.haml +++ b/app/views/story_group_invites/_code.html.haml @@ -1,10 +1,10 @@ -%h2#gh-code-title.gh-h2= t('.title', name: @story_group.name) +%h2.gh-h2#gh-code-title= t('.title', name: story_group.name) %p= t('.hint') = render 'qr_stat', invite: invite .gh-dialog-button-row - - if @in_modal + - if in_modal %button.gh-btn{ type: 'button', data: { action: 'dialog#close' }, autofocus: true }= t('shared.actions.close') - else - = link_to t('shared.actions.close'), story_group_invites_path(@story_group), class: 'gh-btn' + = link_to t('shared.actions.close'), story_group_invites_path(story_group), class: 'gh-btn' diff --git a/app/views/story_group_invites/_quick.html.haml b/app/views/story_group_invites/_quick.html.haml index 9db03f6..42d0776 100644 --- a/app/views/story_group_invites/_quick.html.haml +++ b/app/views/story_group_invites/_quick.html.haml @@ -1,4 +1,4 @@ -%h2.gh-h2#gh-quick-title= t('.title', name: @story_group.name) +%h2.gh-h2#gh-quick-title= t('.title', name: story_group.name) %p= t('.hint') - if active.any? @@ -21,15 +21,16 @@ - else %p.gh-explainer-note= t('.empty') .gh-button-row.gh-quick-invite-cta - = link_to new_story_group_invite_path(@story_group), class: 'gh-btn', - data: { turbo_frame: 'modal' } do + = link_to new_story_group_invite_path(story_group), + class: 'gh-btn', + data: { turbo_frame: 'modal' } do %i.fa-solid.fa-plus{ 'aria-hidden': 'true' } = t('.new') .gh-dialog-button-row - = link_to t('.manage'), story_group_invites_path(@story_group), + = link_to t('.manage'), story_group_invites_path(story_group), class: 'gh-btn gh-btn--secondary', data: { turbo_frame: '_top' } - - if @in_modal + - if in_modal %button.gh-btn{ type: 'button', data: { action: 'dialog#close' }, autofocus: true }= t('shared.actions.close') - else - = link_to t('shared.actions.close'), story_group_invites_path(@story_group), class: 'gh-btn' + = link_to t('shared.actions.close'), story_group_invites_path(story_group), class: 'gh-btn' diff --git a/app/views/story_group_invites/quick.html.haml b/app/views/story_group_invites/quick.html.haml index 6327f72..f080aaa 100644 --- a/app/views/story_group_invites/quick.html.haml +++ b/app/views/story_group_invites/quick.html.haml @@ -2,7 +2,13 @@ - if @in_modal = turbo_frame_tag 'modal' do - = render 'quick', active: @active + = render 'quick', + active: @active, + story_group: @story_group, + in_modal: @in_modal - else %section.gh-panel.gh-dialog-page-panel - = render 'quick', active: @active + = render 'quick', + active: @active, + story_group: @story_group, + in_modal: @in_modal diff --git a/app/views/story_group_invites/show.html.haml b/app/views/story_group_invites/show.html.haml index 5444c2c..6159c9d 100644 --- a/app/views/story_group_invites/show.html.haml +++ b/app/views/story_group_invites/show.html.haml @@ -2,7 +2,13 @@ - if @in_modal = turbo_frame_tag 'modal' do - = render 'code', invite: @invite + = render 'code', + invite: @invite, + story_group: @story_group, + in_modal: @in_modal - else %section.gh-panel.gh-dialog-page-panel - = render 'code', invite: @invite + = render 'code', + invite: @invite, + story_group: @story_group, + in_modal: @in_modal diff --git a/app/views/students/index.html.haml b/app/views/students/index.html.haml index 3740ee3..caf8abb 100644 --- a/app/views/students/index.html.haml +++ b/app/views/students/index.html.haml @@ -9,8 +9,9 @@ = link_to story_group_invites_path(@story_group), class: 'gh-btn gh-btn--secondary' do %i.fa-solid.fa-qrcode{ 'aria-hidden': 'true' } = t('.invites') - = link_to quick_story_group_invites_path(@story_group), class: 'gh-btn', - data: { turbo_frame: 'modal' } do + = link_to quick_story_group_invites_path(@story_group), + class: 'gh-btn', + data: { turbo_frame: 'modal' } do %i.fa-solid.fa-eye{ 'aria-hidden': 'true' } = t('.show_code') From 3fdc533c81abf6902d793c0958eb5538e5fa83c6 Mon Sep 17 00:00:00 2001 From: cub8 Date: Fri, 30 Oct 2026 20:36:12 +0100 Subject: [PATCH 2/2] Add printing QR codes --- app/assets/stylesheets/application.css | 1 + app/assets/stylesheets/components/print.css | 50 ++++++++++++++++++ app/helpers/story_group_invites_helper.rb | 9 ++++ app/javascript/controllers/index.ts | 1 + .../controllers/print_controller.ts | 51 +++++++++++++++++++ app/views/story_group_invites/_code.html.haml | 16 ++++-- .../_print_button.html.haml | 10 ++++ .../locales/views/story_group_invites.en.yml | 5 ++ .../locales/views/story_group_invites.pl.yml | 5 ++ .../story_group_invites_controller_test.rb | 10 ++++ 10 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 app/assets/stylesheets/components/print.css create mode 100644 app/javascript/controllers/print_controller.ts create mode 100644 app/views/story_group_invites/_print_button.html.haml diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 6ac5a92..8237aac 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -32,6 +32,7 @@ @import "./components/overview.css"; @import "./components/join.css"; @import "./components/invites.css"; +@import "./components/print.css"; @import "./components/ranks.css"; @import "./components/ranking.css"; @import "./components/teachers.css"; diff --git a/app/assets/stylesheets/components/print.css b/app/assets/stylesheets/components/print.css new file mode 100644 index 0000000..bb561ad --- /dev/null +++ b/app/assets/stylesheets/components/print.css @@ -0,0 +1,50 @@ +.gh-print-sheet, +.gh-print-root { + display: none; +} + +@media print { + @page { + size: A4; + margin: 0; + } + + html.gh-printing, + html.gh-printing body { + margin: 0; + padding: 0; + height: auto; + overflow: visible; + background: #fff; + } + + html.gh-printing body> :not(.gh-print-root), + html.gh-printing dialog { + display: none !important; + } + + html.gh-printing .gh-print-root { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10mm; + width: 210mm; + height: 296mm; + overflow: hidden; + background: #fff; + color: #000; + print-color-adjust: exact; + } + + .gh-print-root .gh-print-qr { + display: block; + width: 170mm; + height: 170mm; + } + + .gh-print-root .gh-print-code { + font: 700 24mm / 1 var(--gh-f-mono, monospace); + letter-spacing: 0.08em; + } +} diff --git a/app/helpers/story_group_invites_helper.rb b/app/helpers/story_group_invites_helper.rb index e10c129..836060b 100644 --- a/app/helpers/story_group_invites_helper.rb +++ b/app/helpers/story_group_invites_helper.rb @@ -20,6 +20,15 @@ def invite_qr_code(invite) ) end + def invite_qr_svg(invite) + RQRCode::QRCode.new(join_url(code: invite.code)).as_svg( + viewbox: true, + module_size: 1, + shape_rendering: 'crispEdges', + svg_attributes: { class: 'gh-print-qr', 'aria-hidden': 'true' }, + ).html_safe + end + def invite_uses_label(invite) return t('story_group_invites.helpers.uses_unlimited', uses: invite.uses) if invite.max_uses.nil? diff --git a/app/javascript/controllers/index.ts b/app/javascript/controllers/index.ts index f13257c..e2b620f 100644 --- a/app/javascript/controllers/index.ts +++ b/app/javascript/controllers/index.ts @@ -20,6 +20,7 @@ import "@controllers/list_search_controller" import "@controllers/lives_stepper_controller" import "@controllers/menu_controller" import "@controllers/preset_row_controller" +import "@controllers/print_controller" import "@controllers/rank_form_controller" import "@controllers/sheet_create_controller" import "@controllers/sheet_form_controller" diff --git a/app/javascript/controllers/print_controller.ts b/app/javascript/controllers/print_controller.ts new file mode 100644 index 0000000..1579b24 --- /dev/null +++ b/app/javascript/controllers/print_controller.ts @@ -0,0 +1,51 @@ +import { application } from "@controllers/application" +import { Controller } from "@hotwired/stimulus" + +const PRINTING_CLASS = "gh-printing" +const ROOT_CLASS = "gh-print-root" + +class PrintController extends Controller { + static targets = ["sheet"] + static values = { title: String } + + declare readonly sheetTarget: HTMLElement + declare readonly hasSheetTarget: boolean + declare readonly titleValue: string + declare readonly hasTitleValue: boolean + + private originalTitle: string | null = null + + disconnect() { + this.reset() + } + + print() { + if (!this.hasSheetTarget) return + + this.reset() + + const root = document.createElement("div") + root.className = ROOT_CLASS + root.innerHTML = this.sheetTarget.innerHTML + + this.originalTitle = document.title + if (this.hasTitleValue) document.title = this.titleValue + + window.addEventListener("afterprint", () => this.reset(), { once: true }) + document.body.append(root) + document.documentElement.classList.add(PRINTING_CLASS) + window.print() + } + + private reset() { + document.querySelectorAll(`.${ROOT_CLASS}`).forEach((root) => root.remove()) + document.documentElement.classList.remove(PRINTING_CLASS) + + if (this.originalTitle !== null) { + document.title = this.originalTitle + this.originalTitle = null + } + } +} + +application.register("print", PrintController) diff --git a/app/views/story_group_invites/_code.html.haml b/app/views/story_group_invites/_code.html.haml index c0c2f61..2aee76a 100644 --- a/app/views/story_group_invites/_code.html.haml +++ b/app/views/story_group_invites/_code.html.haml @@ -3,8 +3,14 @@ = render 'qr_stat', invite: invite -.gh-dialog-button-row - - if in_modal - %button.gh-btn{ type: 'button', data: { action: 'dialog#close' }, autofocus: true }= t('shared.actions.close') - - else - = link_to t('shared.actions.close'), story_group_invites_path(story_group), class: 'gh-btn' +.contents{ data: { controller: 'print', print_title_value: t('story_group_invites.print_button.document_title', name: story_group.name) } } + .gh-print-sheet{ 'aria-hidden': 'true', data: { print_target: 'sheet' } } + = invite_qr_svg(invite) + .gh-print-code= invite.code + + .gh-dialog-button-row + = render 'print_button', invite: invite, story_group: story_group + - if in_modal + %button.gh-btn{ type: 'button', data: { action: 'dialog#close' }, autofocus: true }= t('shared.actions.close') + - else + = link_to t('shared.actions.close'), story_group_invites_path(story_group), class: 'gh-btn' diff --git a/app/views/story_group_invites/_print_button.html.haml b/app/views/story_group_invites/_print_button.html.haml new file mode 100644 index 0000000..ea0f1de --- /dev/null +++ b/app/views/story_group_invites/_print_button.html.haml @@ -0,0 +1,10 @@ +- title = t('.title', code: invite.code) + +%button.gh-btn.gh-btn--secondary{ + type: 'button', + title: title, + 'aria-label': title, + data: { action: 'print#print' }, +} + %i.fa-solid.fa-print{ 'aria-hidden': 'true' } + %span= t('.label') diff --git a/config/locales/views/story_group_invites.en.yml b/config/locales/views/story_group_invites.en.yml index fab933b..e3ac29f 100644 --- a/config/locales/views/story_group_invites.en.yml +++ b/config/locales/views/story_group_invites.en.yml @@ -64,6 +64,11 @@ en: message: "Copied the code %{code}." label: "Copy code" + print_button: + title: "Print code %{code}" + label: "Print" + document_title: "%{name} qrcode" + code: title: "Join the group %{name}" diff --git a/config/locales/views/story_group_invites.pl.yml b/config/locales/views/story_group_invites.pl.yml index c1dc391..797fedd 100644 --- a/config/locales/views/story_group_invites.pl.yml +++ b/config/locales/views/story_group_invites.pl.yml @@ -68,6 +68,11 @@ pl: message: "Skopiowano kod %{code}." label: "Kopiuj kod" + print_button: + title: "Drukuj kod %{code}" + label: "Drukuj" + document_title: "%{name} qrcode" + code: title: "Dołącz do grupy %{name}" diff --git a/test/controllers/story_group_invites_controller_test.rb b/test/controllers/story_group_invites_controller_test.rb index cf160f3..fd29855 100644 --- a/test/controllers/story_group_invites_controller_test.rb +++ b/test/controllers/story_group_invites_controller_test.rb @@ -55,6 +55,10 @@ def form_params(limit: true, max_uses: 10, expiry: true, on: Time.zone.tomorrow, test 'should get show' do get story_group_invite_url(@story_group, @invite) assert_response :success + assert_select "[data-controller='print'] [data-print-target='sheet']" + assert_select '.gh-print-sheet svg.gh-print-qr' + assert_select '.gh-print-sheet .gh-print-code', text: @invite.code + assert_select "button[data-action='print#print']" end test 'should get quick' do @@ -62,6 +66,12 @@ def form_params(limit: true, max_uses: 10, expiry: true, on: Time.zone.tomorrow, assert_response :success end + test 'quick does not offer the PDF export' do + get quick_story_group_invites_url(@story_group) + assert_select '.gh-print-sheet', count: 0 + assert_select "button[data-action='print#print']", count: 0 + end + test 'should get the delete confirmation' do get confirm_destroy_story_group_invite_url(@story_group, @invite)