Connect Card Upload Viewer

Add a pop-up next to the Form view modals for Connect Cards, with zoom + print support.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Connect Card Upload Viewer
// @namespace    https://github.com/nate-kean/
// @version      2026.04.05
// @description  Add a pop-up next to the Form view modals for Connect Cards, with zoom + print support.
// @author       Nate Kean
// @match        https://jamesriver.fellowshiponego.com/forms
// @grant        none
// @license      MIT
// @require      https://cdnjs.cloudflare.com/ajax/libs/viewerjs/1.11.7/viewer.min.js
// ==/UserScript==

(async function() {
    function delay(ms) {
        return new Promise((resolve) => setTimeout(resolve, ms));
    }

    async function waitForElement(selector, pollingRateMs=100, parent=document) {
        let el;
        while (true) {
            el = parent.querySelector(selector);
            if (el) return el;
            await delay(pollingRateMs);
        }
    }

    async function elementGone(selector, pollingRateMs=100, parent=document) {
        let el;
        while (true) {
            el = parent.querySelector(selector);
            if (!el) return;
            await delay(pollingRateMs);
        }
    }

    document.head.insertAdjacentHTML("beforeend", `
        <style id="nates-connect-card-viewer-css">
            #nates-connect-card-viewer {
                position: absolute;
                top: 6rem;
                right: 1rem;
                width: 23vw;
                height: 80vh;
                background-color: #fff;
                padding: 1.5rem;
                box-shadow: 0 20px 50px 0 rgba(52,52,52,.13) !important;
                overflow-y: auto;
            }

            #nates-connect-card-viewer img {
                max-width: 100%;
                cursor: zoom-in;
            }

            #nates-connect-card-viewer button {
                float: right;
                margin-left: 0.5rem;
            }
        </style>

        <link
            rel="stylesheet"
            href="https://cdnjs.cloudflare.com/ajax/libs/viewerjs/1.11.7/viewer.min.css"
        />
    `);

    while (true) {
        const modal = await waitForElement(".modal-dialog");
        const detail = modal.querySelector("h3.modal-title > span > span.info");

        while (detail.textContent === "") {
            await delay(10);
        }

        let popup = null;

        if (detail.textContent === "Connect Card Upload" || detail.textContent === "Baptism Card Upload") {

            const firstName = modal.querySelector("input[data-qa='field-person-name-input-first-name']").value;
            const lastName = modal.querySelector("input[data-qa='field-person-name-input-last-name']").value;
            const fileURLs = modal.querySelectorAll("a.file-upload-link");
            const memos = modal.querySelectorAll("textarea[data-qa='field-memo-textarea']");
            const campus = modal.querySelector("input[data-qa='field-radio-radio-value']:checked")?.value;

            popup = document.createElement("div");
            popup.id = "nates-connect-card-viewer";

            // Close button
            const closeBtn = document.createElement("button");
            closeBtn.textContent = "X";
            closeBtn.addEventListener("click", () => popup.remove());
            popup.appendChild(closeBtn);

            // Print button
            const printBtn = document.createElement("button");
            printBtn.textContent = "Print";

            printBtn.addEventListener("click", () => {
                const printWindow = window.open("", "_blank");

                const blocksHTML = [];

                for (let i = 0; i < fileURLs.length; i++) {
                    const text = `${firstName} ${lastName} - ${campus} - ${memos[i].textContent || "(no desc)"}`;

                    // Adjust image size dynamically based on text length
                    const imgMaxHeight = text.length > 120 ? "65vh" : "75vh";

                    blocksHTML.push(`
                        <div class="print-block">
                            <img src="${fileURLs[i].href}" style="max-height:${imgMaxHeight}" />
                            <p>${text}</p>
                        </div>
                    `);
                }

                printWindow.document.write(`
                    <html>
                        <head>
                            <title>Print</title>
                            <style>
                                @media print {
                                    body {
                                        margin: 0;
                                        padding: 0;
                                        font-family: Arial, sans-serif;
                                    }

                                    .print-block {
                                        height: 100vh;
                                        display: flex;
                                        flex-direction: column;
                                        justify-content: center;
                                        align-items: center;

                                        page-break-after: always;
                                        break-after: page;
                                        break-inside: avoid;
                                        page-break-inside: avoid;

                                        padding: 20px;
                                        box-sizing: border-box;
                                    }

                                    img {
                                        max-width: 100%;
                                        object-fit: contain;
                                    }

                                    p {
                                        margin-top: 10px;
                                        font-size: 16px;
                                        text-align: center;
                                    }
                                }

                                button {
                                    display: none;
                                }
                            </style>
                        </head>
                        <body>
                            ${blocksHTML.join("")}
                        </body>
                    </html>
                `);

                printWindow.document.close();
                printWindow.focus();
                printWindow.print();
                // intentionally NOT closing window
            });

            popup.appendChild(printBtn);

            // Viewer content
            for (let i = 0; i < fileURLs.length; i++) {
                const wrapper = document.createElement("div");

                const img = document.createElement("img");
                img.src = fileURLs[i].href;
                new Viewer(img);

                const desc = document.createElement("p");
                desc.textContent = `${firstName} ${lastName} - ${campus} - ${memos[i].textContent || "(no desc)"}`;

                wrapper.appendChild(img);
                wrapper.appendChild(desc);
                popup.appendChild(wrapper);
            }

            document.body.appendChild(popup);
        }

        await elementGone(".modal-dialog");
        popup?.remove();
    }
})();