{"id":21741,"date":"2026-09-18T23:51:57","date_gmt":"2026-09-18T13:51:57","guid":{"rendered":"https:\/\/unswoc.org\/wordpress\/?page_id=21741"},"modified":"2026-11-11T02:06:12","modified_gmt":"2026-11-10T15:06:12","slug":"2026-unswoc-photo-competition","status":"publish","type":"page","link":"https:\/\/unswoc.org\/wordpress\/2026-unswoc-photo-competition\/","title":{"rendered":"2026 UNSWOC Photo Competition"},"content":{"rendered":"\n<p class=\"has-large-font-size wp-block-paragraph\">Choose your favourite three images from each category. The button to start voting is at the bottom of page.<\/p>\n\n\n\n<p class=\"has-large-font-size wp-block-paragraph\">Click the image to view the larger version.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you have any inquiries about the photos, or if your submission has failed to be uploaded, please email webadmin@unswoc.org.<\/p>\n\n\n\n<div class=\"wp-block-group photo-vote-category nature\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained\">\n<h2 class=\"wp-block-heading has-text-align-center\">Nature<\/h2>\n\n\nngg_shortcode_0_placeholder\n<\/div><\/div>\n\n\n\n<div class=\"wp-block-group photo-vote-category action\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained\">\n<h2 class=\"wp-block-heading has-text-align-center\">Action<\/h2>\n\n\nngg_shortcode_1_placeholder\n<\/div><\/div>\n\n\n\n<div class=\"wp-block-group photo-vote-category people\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained\">\n<div class=\"wp-block-group\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained\">\n<h2 class=\"wp-block-heading has-text-align-center\">People<\/h2>\n\n\nngg_shortcode_2_placeholder\n<\/div><\/div>\n<\/div><\/div>\n\n\n\n<div class=\"wp-block-group photo-vote-category funny\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained\">\n<h2 class=\"wp-block-heading has-text-align-center\">Funny<\/h2>\n\n\nngg_shortcode_3_placeholder\n<\/div><\/div>\n\n\n\n<div class=\"wp-block-group photo-vote-category club_card\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained\">\n<h2 class=\"wp-block-heading has-text-align-center\">Club Card<\/h2>\n\n\nngg_shortcode_4_placeholder\n<\/div><\/div>\n\n\n\n<div class=\"photo-vote-controls\">\n    <button type=\"button\" id=\"photo-vote-button\" disabled=\"\">\n        Checking voting status...\n    <\/button>\n\n    <p id=\"photo-vote-status\" role=\"status\" aria-live=\"polite\">\n        Please wait...\n    <\/p>\n<\/div>\n\n<style>\n.photo-vote-controls {\n    text-align: center;\n    margin: 30px 0;\n}\n\n#photo-vote-button {\n    padding: 14px 28px;\n    font-size: 18px;\n    cursor: pointer;\n    border-radius: 8px;\n}\n\n#photo-vote-button:disabled {\n    cursor: not-allowed;\n    opacity: 0.6;\n}\n\n.photo-vote-category.voting-mode .ngg-gallery-thumbnail-box,\n.photo-vote-category.voting-mode .ngg-gallery-thumbnail {\n    cursor: pointer;\n}\n\n.photo-vote-category.voting-mode .ngg-gallery-thumbnail a {\n    pointer-events: none !important;\n}\n\n.photo-vote-category.voting-mode .ngg-gallery-thumbnail-box:not(.vote-selected),\n.photo-vote-category.voting-mode .ngg-gallery-thumbnail:not(.vote-selected) {\n    opacity: 0.65;\n}\n\n.photo-vote-category .vote-selected {\n    opacity: 1 !important;\n    outline: 5px solid #35a853;\n    outline-offset: -5px;\n    border-radius: 4px;\n}\n\n.photo-vote-category .ngg-gallery-thumbnail-box,\n.photo-vote-category .ngg-gallery-thumbnail {\n    transition: opacity 0.2s ease, outline 0.2s ease;\n}\n\n#photo-vote-status {\n    margin-top: 12px;\n    font-weight: 600;\n}\n<\/style>\n\n<script>\ndocument.addEventListener('DOMContentLoaded', async function () {\n\n    const MAX_VOTES = 3;\n\n    const categoryNames = [\n        'nature',\n        'action',\n        'people',\n        'funny',\n        'club_card'\n    ];\n\n    const button = document.getElementById('photo-vote-button');\n    const status = document.getElementById('photo-vote-status');\n    const controls = document.querySelector('.photo-vote-controls');\n\n    if (!button || !status || !controls) {\n        return;\n    }\n\n    const config = window.photoVoting;\n\n    if (!config || !config.restUrl || !config.nonce) {\n        status.textContent = 'Voting is unavailable. Please refresh the page.';\n        button.style.display = 'none';\n        return;\n    }\n\n    const apiBase = config.restUrl.replace(\/\\\/$\/, '');\n    const categories = {};\n\n    categoryNames.forEach(function (name) {\n        categories[name] = document.querySelector(\n            '.photo-vote-category.' + name\n        );\n    });\n\n    let votingMode = false;\n    let submitting = false;\n    let hasVoted = false;\n\n    async function apiRequest(path, options = {}) {\n        const response = await fetch(apiBase + path, {\n            ...options,\n            credentials: 'same-origin',\n            cache: 'no-store',\n            headers: {\n                'X-WP-Nonce': config.nonce,\n                ...(options.body ? {\n                    'Content-Type': 'application\/json'\n                } : {}),\n                ...(options.headers || {})\n            }\n        });\n\n        const result = await response.json();\n\n        if (!response.ok) {\n            const error = new Error(\n                result.message || 'Request failed.'\n            );\n            error.status = response.status;\n            error.code = result.code;\n            throw error;\n        }\n\n        return result;\n    }\n\n    function getTile(target) {\n        if (!(target instanceof Element)) {\n            return null;\n        }\n\n        return (\n            target.closest('.ngg-gallery-thumbnail-box') ||\n            target.closest('.ngg-gallery-thumbnail')\n        );\n    }\n\n    function getSelected(categoryElement) {\n        return categoryElement.querySelectorAll(\n            '.ngg-gallery-thumbnail-box.vote-selected, ' +\n            '.ngg-gallery-thumbnail.vote-selected'\n        );\n    }\n\n    function restoreVotes(votes) {\n        categoryNames.forEach(function (name) {\n            const categoryElement = categories[name];\n\n            if (!categoryElement || !Array.isArray(votes[name])) {\n                return;\n            }\n\n            const selectedIds = new Set(votes[name].map(Number));\n\n            categoryElement.querySelectorAll(\n                'a[data-image-id]'\n            ).forEach(function (link) {\n\n                const tile = getTile(link);\n\n                if (tile) {\n                    tile.classList.toggle(\n                        'vote-selected',\n                        selectedIds.has(Number(link.dataset.imageId))\n                    );\n                }\n            });\n        });\n    }\n\n    function showAlreadyVoted() {\n        hasVoted = true;\n        votingMode = false;\n\n        button.style.display = 'none';\n        status.style.color = '#35a853';\n        status.textContent =\n            'Thank you! Your votes have been submitted. ' +\n            'Your selected photos are highlighted in green.';\n\n        categoryNames.forEach(function (name) {\n            if (categories[name]) {\n                categories[name].classList.add('voting-mode');\n            }\n        });\n    }\n\n    function showError(message) {\n        status.style.color = '#d32f2f';\n        status.textContent = message;\n    }\n\n    async function checkVotingStatus() {\n        button.disabled = true;\n        status.textContent = 'Checking voting eligibility...';\n\n        try {\n            const result = await apiRequest('\/status');\n\n            if (result.has_voted) {\n                if (result.votes) {\n                    restoreVotes(result.votes);\n                }\n\n                showAlreadyVoted();\n                return;\n            }\n\n            hasVoted = false;\n            button.disabled = false;\n            button.textContent = 'Start Voting';\n            status.style.color = '';\n            status.textContent =\n                'Click \"Start Voting\" when you are ready.';\n\n        } catch (error) {\n            button.disabled = true;\n\n            if (error.status === 401 || error.status === 403) {\n                showError(\n                    'Please log in to your WordPress account to vote.'\n                );\n            } else {\n                showError(\n                    'Could not check voting status. Please refresh the page.'\n                );\n            }\n        }\n    }\n\n    function updateStatus() {\n        let complete = 0;\n\n        categoryNames.forEach(function (name) {\n            const categoryElement = categories[name];\n\n            if (\n                categoryElement &&\n                getSelected(categoryElement).length === MAX_VOTES\n            ) {\n                complete++;\n            }\n        });\n\n        status.style.color = '';\n        status.textContent =\n            complete + ' of ' + categoryNames.length +\n            ' categories complete.';\n    }\n\n    categoryNames.forEach(function (name) {\n        const categoryElement = categories[name];\n\n        if (!categoryElement) {\n            return;\n        }\n\n        categoryElement.addEventListener(\n            'click',\n            function (event) {\n\n                if (!votingMode || submitting || hasVoted) {\n                    return;\n                }\n\n                const tile = getTile(event.target);\n\n                if (!tile || !categoryElement.contains(tile)) {\n                    return;\n                }\n\n                event.preventDefault();\n                event.stopPropagation();\n                event.stopImmediatePropagation();\n\n                if (tile.classList.contains('vote-selected')) {\n                    tile.classList.remove('vote-selected');\n                    updateStatus();\n                    return;\n                }\n\n                if (getSelected(categoryElement).length >= MAX_VOTES) {\n                    alert(\n                        'You can select a maximum of 3 photos in this category.'\n                    );\n                    return;\n                }\n\n                tile.classList.add('vote-selected');\n                updateStatus();\n            },\n            true\n        );\n    });\n\n    button.addEventListener('click', async function () {\n\n        if (submitting || hasVoted || button.disabled) {\n            return;\n        }\n\n        if (!votingMode) {\n            votingMode = true;\n\n            categoryNames.forEach(function (name) {\n                if (categories[name]) {\n                    categories[name].classList.add('voting-mode');\n                }\n            });\n\n            button.textContent = 'Submit Votes';\n            updateStatus();\n            return;\n        }\n\n        const votes = {};\n\n        for (const name of categoryNames) {\n            const categoryElement = categories[name];\n\n            if (!categoryElement) {\n                alert('Could not find category: ' + name);\n                return;\n            }\n\n            const selected = getSelected(categoryElement);\n\n            if (selected.length !== MAX_VOTES) {\n                alert(\n                    'Please select exactly 3 photos for ' +\n                    name.replace('_', ' ') + '.'\n                );\n                return;\n            }\n\n            const imageIds = [];\n\n            selected.forEach(function (tile) {\n                const link = tile.querySelector('a[data-image-id]');\n\n                if (!link) {\n                    return;\n                }\n\n                const rawId = link.dataset.imageId;\n\n                if (!\/^[1-9][0-9]*$\/.test(rawId)) {\n                    return;\n                }\n\n                const imageId = Number(rawId);\n\n                if (Number.isSafeInteger(imageId)) {\n                    imageIds.push(imageId);\n                }\n            });\n\n            if (\n                imageIds.length !== MAX_VOTES ||\n                new Set(imageIds).size !== MAX_VOTES\n            ) {\n                alert(\n                    'Could not read three unique image IDs for ' + name + '.'\n                );\n                return;\n            }\n\n            votes[name] = imageIds;\n        }\n\n        submitting = true;\n        button.disabled = true;\n        status.style.color = '';\n        status.textContent = 'Saving votes...';\n\n        try {\n            const result = await apiRequest('\/submit', {\n                method: 'POST',\n                body: JSON.stringify({ votes: votes })\n            });\n\n            if (result.success) {\n                showAlreadyVoted();\n            } else {\n                throw new Error('Could not save votes.');\n            }\n\n        } catch (error) {\n\n            if (\n                error.status === 409 &&\n                error.code === 'already_voted'\n            ) {\n                try {\n                    const result = await apiRequest('\/status');\n\n                    if (result.has_voted) {\n                        if (result.votes) {\n                            restoreVotes(result.votes);\n                        }\n                        showAlreadyVoted();\n                    } else {\n                        showError('Could not confirm your saved votes.');\n                    }\n                } catch (statusError) {\n                    showError(\n                        'Could not load your saved votes. Please refresh the page.'\n                    );\n                }\n            } else if (\n                error.status === 401 ||\n                error.status === 403\n            ) {\n                showError(\n                    'Your login session may have expired. Please refresh and log in again.'\n                );\n                button.disabled = true;\n                return;\n            } else {\n                showError(error.message);\n            }\n\n        } finally {\n            submitting = false;\n\n            if (!hasVoted && status.style.color !== 'rgb(211, 47, 47)') {\n                button.disabled = false;\n            }\n        }\n    });\n\n    await checkVotingStatus();\n});\n<\/script>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Choose your favourite three images from each category. The button to start voting is at the bottom of page. Click &hellip; <a class=\"kt-excerpt-readmore more-link\" href=\"https:\/\/unswoc.org\/wordpress\/2026-unswoc-photo-competition\/\" aria-label=\"2026 UNSWOC Photo Competition\">Read More<\/a><\/p>\n","protected":false},"author":5258,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"kt_blocks_editor_width":"","ngg_post_thumbnail":0,"footnotes":""},"class_list":["post-21741","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/unswoc.org\/wordpress\/wp-json\/wp\/v2\/pages\/21741","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/unswoc.org\/wordpress\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/unswoc.org\/wordpress\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/unswoc.org\/wordpress\/wp-json\/wp\/v2\/users\/5258"}],"replies":[{"embeddable":true,"href":"https:\/\/unswoc.org\/wordpress\/wp-json\/wp\/v2\/comments?post=21741"}],"version-history":[{"count":5,"href":"https:\/\/unswoc.org\/wordpress\/wp-json\/wp\/v2\/pages\/21741\/revisions"}],"predecessor-version":[{"id":22258,"href":"https:\/\/unswoc.org\/wordpress\/wp-json\/wp\/v2\/pages\/21741\/revisions\/22258"}],"wp:attachment":[{"href":"https:\/\/unswoc.org\/wordpress\/wp-json\/wp\/v2\/media?parent=21741"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}