{"id":44596,"date":"2024-08-13T06:42:05","date_gmt":"2024-08-13T06:42:05","guid":{"rendered":"https:\/\/www.clevry.com\/?page_id=44596"},"modified":"2024-08-13T09:50:25","modified_gmt":"2024-08-13T09:50:25","slug":"avoimet-tyopaikat","status":"publish","type":"page","link":"https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/","title":{"rendered":"Avoimet Ty\u00f6paikat"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"44596\" class=\"elementor elementor-44596\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-ea5a0f4 e-flex e-con-boxed e-con e-parent\" data-id=\"ea5a0f4\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-97f8951 elementor-widget elementor-widget-shortcode\" data-id=\"97f8951\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"shortcode.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"elementor-shortcode\">\t\t<div data-elementor-type=\"section\" data-elementor-id=\"44598\" class=\"elementor elementor-44598\" data-elementor-post-type=\"elementor_library\">\n\t\t\t<div class=\"elementor-element elementor-element-ad43179 e-flex e-con-boxed e-con e-parent\" data-id=\"ad43179\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-3a026d8 elementor-widget elementor-widget-html\" data-id=\"3a026d8\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n    <meta charset=\"UTF-8\">\r\n    <title>Avoimet Ty\u00f6paikat - Optimized<\/title>\r\n    <style>\r\n        \/* Base Styles *\/\r\n        * {\r\n            box-sizing: border-box;\r\n        }\r\n        \r\n        body {\r\n            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\r\n            margin: 0;\r\n            padding: 20px;\r\n            background-color: #f0f0f0;\r\n        }\r\n        \r\n        .container {\r\n            max-width: 1200px;\r\n            margin: 0 auto;\r\n        }\r\n        \r\n        h1 {\r\n            font-size: 2.5em;\r\n            color: #36705b;\r\n            margin-bottom: 1.5rem;\r\n        }\r\n        \r\n        \/* Search Bar *\/\r\n        .search-bar {\r\n            display: flex;\r\n            gap: 10px;\r\n            margin-bottom: 20px;\r\n            flex-wrap: wrap;\r\n        }\r\n        \r\n        .search-bar label {\r\n            display: flex;\r\n            flex-direction: column;\r\n            flex: 1;\r\n            min-width: 200px;\r\n        }\r\n        \r\n        .search-bar label span {\r\n            margin-bottom: 5px;\r\n            font-size: 0.9em;\r\n            color: #555;\r\n        }\r\n        \r\n        .search-bar input {\r\n            padding: 10px;\r\n            border: 1px solid #ccc;\r\n            border-radius: 4px;\r\n            font-size: 14px;\r\n        }\r\n        \r\n        .search-bar input:focus {\r\n            outline: none;\r\n            border-color: #36705b;\r\n            box-shadow: 0 0 0 2px rgba(54, 112, 91, 0.1);\r\n        }\r\n        \r\n        .button-group {\r\n            display: flex;\r\n            gap: 10px;\r\n            align-items: flex-end;\r\n        }\r\n        \r\n        .search-bar button {\r\n            padding: 10px 20px;\r\n            background-color: #36705b;\r\n            color: white;\r\n            border: none;\r\n            border-radius: 4px;\r\n            cursor: pointer;\r\n            font-size: 14px;\r\n            transition: background-color 0.2s;\r\n        }\r\n        \r\n        .search-bar button:hover:not(:disabled) {\r\n            background-color: #2a5948;\r\n        }\r\n        \r\n        .search-bar button:disabled {\r\n            opacity: 0.6;\r\n            cursor: not-allowed;\r\n        }\r\n        \r\n        .search-bar button#clearButton {\r\n            background-color: #f0f0f0;\r\n            color: #36705b;\r\n            border: 1px solid #36705b;\r\n        }\r\n        \r\n        .search-bar button#clearButton:hover:not(:disabled) {\r\n            background-color: #e0e0e0;\r\n        }\r\n        \r\n        \/* View Toggle *\/\r\n        .controls-row {\r\n            display: flex;\r\n            justify-content: space-between;\r\n            align-items: center;\r\n            margin-bottom: 20px;\r\n            flex-wrap: wrap;\r\n            gap: 15px;\r\n        }\r\n        \r\n        .view-toggle {\r\n            display: flex;\r\n            align-items: center;\r\n            gap: 10px;\r\n        }\r\n        \r\n        .switch {\r\n            position: relative;\r\n            display: inline-block;\r\n            width: 50px;\r\n            height: 26px;\r\n        }\r\n        \r\n        .switch input {\r\n            opacity: 0;\r\n            width: 0;\r\n            height: 0;\r\n        }\r\n        \r\n        .slider {\r\n            position: absolute;\r\n            cursor: pointer;\r\n            inset: 0;\r\n            background-color: #ccc;\r\n            transition: background-color 0.3s;\r\n            border-radius: 26px;\r\n        }\r\n        \r\n        .slider:before {\r\n            position: absolute;\r\n            content: \"\";\r\n            height: 20px;\r\n            width: 20px;\r\n            left: 3px;\r\n            bottom: 3px;\r\n            background-color: white;\r\n            transition: transform 0.3s;\r\n            border-radius: 50%;\r\n        }\r\n        \r\n        input:checked + .slider {\r\n            background-color: #36705b;\r\n        }\r\n        \r\n        input:checked + .slider:before {\r\n            transform: translateX(24px);\r\n        }\r\n        \r\n        .view-toggle svg {\r\n            width: 20px;\r\n            height: 20px;\r\n            fill: #666;\r\n        }\r\n        \r\n        \/* Status Bar *\/\r\n        .status-bar {\r\n            display: flex;\r\n            align-items: center;\r\n            gap: 15px;\r\n            font-size: 0.9em;\r\n            color: #666;\r\n        }\r\n        \r\n        .cache-status {\r\n            display: flex;\r\n            align-items: center;\r\n            gap: 5px;\r\n        }\r\n        \r\n        .cache-indicator {\r\n            width: 8px;\r\n            height: 8px;\r\n            border-radius: 50%;\r\n            background-color: #4caf50;\r\n        }\r\n        \r\n        .cache-indicator.stale {\r\n            background-color: #ff9800;\r\n        }\r\n        \r\n        .cache-indicator.expired {\r\n            background-color: #f44336;\r\n        }\r\n        \r\n        \/* Loading Indicator - Pure CSS Animation *\/\r\n        #loading-indicator {\r\n            text-align: center;\r\n            padding: 40px;\r\n        }\r\n        \r\n        .loading-dots {\r\n            display: inline-flex;\r\n            gap: 8px;\r\n        }\r\n        \r\n        .dot {\r\n            width: 12px;\r\n            height: 12px;\r\n            background-color: #36705b;\r\n            border-radius: 50%;\r\n            animation: pulse 1.4s ease-in-out infinite both;\r\n        }\r\n        \r\n        .dot:nth-child(1) { animation-delay: 0s; }\r\n        .dot:nth-child(2) { animation-delay: 0.2s; }\r\n        .dot:nth-child(3) { animation-delay: 0.4s; }\r\n        \r\n        @keyframes pulse {\r\n            0%, 80%, 100% {\r\n                transform: scale(1);\r\n                opacity: 1;\r\n            }\r\n            40% {\r\n                transform: scale(1.3);\r\n                opacity: 0.5;\r\n            }\r\n        }\r\n        \r\n        \/* Job Listings *\/\r\n        .job-listings {\r\n            min-height: 200px;\r\n        }\r\n        \r\n        .job-listings.grid-view {\r\n            display: grid;\r\n            grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));\r\n            gap: 20px;\r\n        }\r\n        \r\n        .job-listings.list-view {\r\n            display: flex;\r\n            flex-direction: column;\r\n            gap: 15px;\r\n        }\r\n        \r\n        \/* Job Cards *\/\r\n        .job-card {\r\n            background-color: white;\r\n            border: 1px solid #ddd;\r\n            border-radius: 8px;\r\n            padding: 20px;\r\n            display: flex;\r\n            flex-direction: column;\r\n            transition: transform 0.2s, box-shadow 0.2s;\r\n            animation: fadeIn 0.3s ease-out;\r\n        }\r\n        \r\n        .job-logo {\r\n            width: 60px;\r\n            height: 60px;\r\n            object-fit: contain;\r\n            margin-bottom: 15px;\r\n            border-radius: 4px;\r\n            background-color: #f5f5f5;\r\n            padding: 5px;\r\n        }\r\n        \r\n        .list-view .job-logo {\r\n            margin-right: 15px;\r\n            margin-bottom: 0;\r\n        }\r\n        \r\n        @keyframes fadeIn {\r\n            from {\r\n                opacity: 0;\r\n                transform: translateY(10px);\r\n            }\r\n            to {\r\n                opacity: 1;\r\n                transform: translateY(0);\r\n            }\r\n        }\r\n        \r\n        .job-card:hover {\r\n            transform: translateY(-2px);\r\n            box-shadow: 0 4px 12px rgba(0,0,0,0.1);\r\n        }\r\n        \r\n        .job-card h2 {\r\n            font-size: 1.3em;\r\n            margin: 0 0 10px 0;\r\n            color: #36705b;\r\n            line-height: 1.3;\r\n        }\r\n        \r\n        .job-card .company {\r\n            font-weight: 600;\r\n            margin-bottom: 8px;\r\n            color: #333;\r\n        }\r\n        \r\n        .job-card .details {\r\n            font-size: 0.9em;\r\n            color: #666;\r\n            margin-bottom: 10px;\r\n            line-height: 1.4;\r\n        }\r\n        \r\n        .job-card .salary {\r\n            background-color: #e6f3f5;\r\n            padding: 6px 12px;\r\n            border-radius: 4px;\r\n            margin-bottom: 12px;\r\n            font-size: 0.9em;\r\n            display: inline-block;\r\n        }\r\n        \r\n        .job-card .description {\r\n            color: #555;\r\n            line-height: 1.5;\r\n            margin-bottom: 15px;\r\n            display: -webkit-box;\r\n            -webkit-line-clamp: 3;\r\n            -webkit-box-orient: vertical;\r\n            overflow: hidden;\r\n        }\r\n        \r\n        .job-card .buttons {\r\n            margin-top: auto;\r\n            display: flex;\r\n            justify-content: flex-end;\r\n        }\r\n        \r\n        .job-card button {\r\n            padding: 8px 16px;\r\n            border: 1px solid #36705b;\r\n            border-radius: 4px;\r\n            background-color: white;\r\n            color: #36705b;\r\n            cursor: pointer;\r\n            font-size: 14px;\r\n            transition: all 0.2s;\r\n        }\r\n        \r\n        .job-card button:hover {\r\n            background-color: #36705b;\r\n            color: white;\r\n        }\r\n        \r\n        \/* List View Specific *\/\r\n        .list-view .job-card {\r\n            flex-direction: row;\r\n            align-items: center;\r\n        }\r\n        \r\n        .list-view .job-info {\r\n            flex: 1;\r\n            margin-right: 20px;\r\n        }\r\n        \r\n        .list-view .job-card .description {\r\n            -webkit-line-clamp: 2;\r\n        }\r\n        \r\n        \/* Messages *\/\r\n        .message {\r\n            padding: 15px;\r\n            border-radius: 4px;\r\n            margin-bottom: 20px;\r\n            animation: slideIn 0.3s ease-out;\r\n        }\r\n        \r\n        @keyframes slideIn {\r\n            from {\r\n                transform: translateX(-20px);\r\n                opacity: 0;\r\n            }\r\n            to {\r\n                transform: translateX(0);\r\n                opacity: 1;\r\n            }\r\n        }\r\n        \r\n        .message.error {\r\n            background-color: #ffebee;\r\n            color: #c62828;\r\n            border-left: 4px solid #c62828;\r\n        }\r\n        \r\n        .message.info {\r\n            background-color: #e3f2fd;\r\n            color: #1565c0;\r\n            border-left: 4px solid #1565c0;\r\n        }\r\n        \r\n        .message.success {\r\n            background-color: #e8f5e9;\r\n            color: #2e7d32;\r\n            border-left: 4px solid #2e7d32;\r\n        }\r\n        \r\n        \/* Hidden Utility *\/\r\n        .hidden {\r\n            display: none !important;\r\n        }\r\n        \r\n        \/* Responsive Design *\/\r\n        @media (max-width: 768px) {\r\n            .search-bar {\r\n                flex-direction: column;\r\n            }\r\n            \r\n            .search-bar label {\r\n                width: 100%;\r\n            }\r\n            \r\n            .button-group {\r\n                width: 100%;\r\n                flex-direction: column;\r\n            }\r\n            \r\n            .search-bar button {\r\n                width: 100%;\r\n            }\r\n            \r\n            .controls-row {\r\n                flex-direction: column;\r\n                align-items: stretch;\r\n            }\r\n            \r\n            .list-view .job-card {\r\n                flex-direction: column;\r\n                align-items: stretch;\r\n            }\r\n            \r\n            .list-view .job-info {\r\n                margin-right: 0;\r\n                margin-bottom: 15px;\r\n            }\r\n            \r\n            h1 {\r\n                font-size: 2em;\r\n            }\r\n            \r\n            .job-listings.grid-view {\r\n                grid-template-columns: 1fr;\r\n            }\r\n        }\r\n        \r\n        @media (max-width: 480px) {\r\n            body {\r\n                padding: 10px;\r\n            }\r\n            \r\n            h1 {\r\n                font-size: 1.5em;\r\n            }\r\n            \r\n            .job-card {\r\n                padding: 15px;\r\n            }\r\n            \r\n            .job-card h2 {\r\n                font-size: 1.1em;\r\n            }\r\n        }\r\n        \r\n        \/* Performance: Use CSS containment *\/\r\n        .job-card {\r\n            contain: layout style;\r\n        }\r\n        \r\n        .job-listings {\r\n            contain: layout;\r\n        }\r\n    <\/style>\r\n<\/head>\r\n<body>\r\n    <div class=\"container\">\r\n        <h1>Avoimet Ty\u00f6paikat<\/h1>\r\n        \r\n        <!-- Search Bar -->\r\n        <div class=\"search-bar\">\r\n            <label>\r\n                <span>Mit\u00e4<\/span>\r\n                <input type=\"text\" id=\"jobSearch\" placeholder=\"Esim: Asiakaspalvelija\">\r\n            <\/label>\r\n            <label>\r\n                <span>Miss\u00e4<\/span>\r\n                <input type=\"text\" id=\"locationSearch\" placeholder=\"Kirjoita hakusana t\u00e4h\u00e4n\">\r\n            <\/label>\r\n            <div class=\"button-group\">\r\n                <button onclick=\"searchJobs()\" id=\"searchButton\">Hae ty\u00f6paikkoja<\/button>\r\n                <button onclick=\"clearSearch()\" id=\"clearButton\">Tyhjenn\u00e4 haku<\/button>\r\n            <\/div>\r\n        <\/div>\r\n        \r\n        <!-- Controls Row -->\r\n        <div class=\"controls-row\">\r\n            <div class=\"view-toggle\">\r\n                <svg viewBox=\"0 0 24 24\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\">\r\n                    <path d=\"M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM14 14h7v7h-7z\"\/>\r\n                <\/svg>\r\n                <label class=\"switch\">\r\n                    <input type=\"checkbox\" id=\"viewToggle\" onchange=\"toggleView()\">\r\n                    <span class=\"slider\"><\/span>\r\n                <\/label>\r\n                <svg viewBox=\"0 0 24 24\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\">\r\n                    <path d=\"M3 4h18v2H3zM3 11h18v2H3zM3 18h18v2H3z\"\/>\r\n                <\/svg>\r\n            <\/div>\r\n            <div class=\"status-bar\">\r\n                <div class=\"cache-status hidden\" id=\"cacheStatus\">\r\n                    <span class=\"cache-indicator\"><\/span>\r\n                    <span id=\"cacheText\">Cache: Fresh<\/span>\r\n                <\/div>\r\n                <span id=\"jobCount\"><\/span>\r\n            <\/div>\r\n        <\/div>\r\n        \r\n        <!-- Messages Container -->\r\n        <div id=\"messageContainer\"><\/div>\r\n        \r\n        <!-- Loading Indicator -->\r\n        <div id=\"loading-indicator\" class=\"hidden\">\r\n            <div class=\"loading-dots\">\r\n                <div class=\"dot\"><\/div>\r\n                <div class=\"dot\"><\/div>\r\n                <div class=\"dot\"><\/div>\r\n            <\/div>\r\n            <p style=\"margin-top: 15px; color: #666;\">Ladataan ty\u00f6paikkoja...<\/p>\r\n        <\/div>\r\n        \r\n        <!-- Job Listings -->\r\n        <div id=\"job-listings\" class=\"job-listings grid-view\"><\/div>\r\n    <\/div>\r\n\r\n    <script>\r\n        \/\/ Configuration - Developers can modify these settings\r\n        const CONFIG = {\r\n            \/\/ API Settings\r\n            API_BASE_URL: 'https:\/\/joy-api.clevry.com\/api\/v1\/publicrole\/browseopportunitiesmodule\/getlistofcandidateopportunities',\r\n            STAGING_API_URL: 'https:\/\/staging-joy-api.clevry.com\/api\/v1\/publicrole\/browseopportunitiesmodule\/getlistofcandidateopportunities',\r\n            CORS_PROXY: 'https:\/\/api.allorigins.win\/raw?url=',\r\n            USE_DIRECT_API: false, \/\/ Set to true to bypass proxy (if CORS is allowed)\r\n            \r\n            \/\/ Performance Settings\r\n            ENABLE_CACHE: true,     \/\/ Enable\/disable 5-minute cache\r\n            CACHE_DURATION: 5 * 60 * 1000, \/\/ Cache duration in milliseconds\r\n            SHOW_CACHE_STATUS: false, \/\/ Show cache status indicator to users\r\n            \r\n            \/\/ API Request Settings\r\n            PAGE_SIZE: 50,\r\n            CLIENT_SUBDOMAIN: 'joy',\r\n            RETRY_ATTEMPTS: 3,\r\n            RETRY_DELAY: 1000,\r\n            \r\n            \/\/ UX Settings\r\n            DEBOUNCE_DELAY: 300,    \/\/ Search input delay in ms\r\n            DEBUG_MODE: false,      \/\/ Enable console logging\r\n            SHOW_PERFORMANCE_MESSAGES: false \/\/ Show loading time messages\r\n        };\r\n        \r\n        \/\/ Global State\r\n        const state = {\r\n            allJobs: [],\r\n            filteredJobs: [],\r\n            currentView: 'grid',\r\n            useDirectAPI: CONFIG.USE_DIRECT_API,\r\n            enableCache: CONFIG.ENABLE_CACHE,\r\n            debugMode: CONFIG.DEBUG_MODE,\r\n            isLoading: false,\r\n            cache: {\r\n                data: null,\r\n                timestamp: null\r\n            }\r\n        };\r\n        \r\n        \/\/ Performance: Use DocumentFragment for batch DOM updates\r\n        const createJobCard = (job, isListView) => {\r\n            const card = document.createElement('div');\r\n            card.className = 'job-card';\r\n            \r\n            \/\/ Only show salary if it's not \"Salary not listed\"\r\n            const salaryHtml = job.salary && job.salary !== 'Salary not listed' \r\n                ? `<div class=\"salary\">${escapeHtml(job.salary)}<\/div>` \r\n                : '';\r\n            \r\n            \/\/ Add logo placeholder (will be filled asynchronously)\r\n            const logoHtml = job.logoUrl \r\n                ? `<img decoding=\"async\" src=\"${escapeHtml(job.logoUrl)}\" alt=\"${escapeHtml(job.company)} logo\" class=\"job-logo\" loading=\"lazy\">`\r\n                : '';\r\n            \r\n            if (isListView) {\r\n                card.innerHTML = `\r\n                    <div class=\"job-info\">\r\n                        ${logoHtml}\r\n                        <h2>${escapeHtml(job.title)}<\/h2>\r\n                        <div class=\"company\">${escapeHtml(job.company)}<\/div>\r\n                        <div class=\"details\">${escapeHtml(job.type)}<br>${escapeHtml(job.location)}<\/div>\r\n                        ${salaryHtml}\r\n                    <\/div>\r\n                    <div class=\"buttons\">\r\n                        <button onclick=\"viewDetails('${job.projectId}')\">Katso lis\u00e4tiedot<\/button>\r\n                    <\/div>\r\n                `;\r\n            } else {\r\n                card.innerHTML = `\r\n                    ${logoHtml}\r\n                    <h2>${escapeHtml(job.title)}<\/h2>\r\n                    <div class=\"company\">${escapeHtml(job.company)}<\/div>\r\n                    <div class=\"details\">${escapeHtml(job.type)}<br>${escapeHtml(job.location)}<\/div>\r\n                    ${salaryHtml}\r\n                    <p class=\"description\">${escapeHtml(job.description)}<\/p>\r\n                    <div class=\"buttons\">\r\n                        <button onclick=\"viewDetails('${job.projectId}')\">Katso lis\u00e4tiedot<\/button>\r\n                    <\/div>\r\n                `;\r\n            }\r\n            \r\n            \/\/ Set data attributes for potential logo fetching\r\n            card.dataset.projectId = job.projectId;\r\n            card.dataset.logoFetched = 'false';\r\n            \r\n            return card;\r\n        };\r\n        \r\n        \/\/ Utility Functions\r\n        const escapeHtml = (text) => {\r\n            const map = {\r\n                '&': '&amp;',\r\n                '<': '&lt;',\r\n                '>': '&gt;',\r\n                '\"': '&quot;',\r\n                \"'\": '&#039;'\r\n            };\r\n            return String(text).replace(\/[&<>\"']\/g, m => map[m]);\r\n        };\r\n        \r\n        const debounce = (func, delay) => {\r\n            let timeoutId;\r\n            return function(...args) {\r\n                clearTimeout(timeoutId);\r\n                timeoutId = setTimeout(() => func.apply(this, args), delay);\r\n            };\r\n        };\r\n        \r\n        const log = (...args) => {\r\n            if (state.debugMode) {\r\n                console.log('[Widget Debug]:', ...args);\r\n            }\r\n        };\r\n        \r\n        const showMessage = (message, type = 'info', duration = 3000) => {\r\n            const container = document.getElementById('messageContainer');\r\n            const messageEl = document.createElement('div');\r\n            messageEl.className = `message ${type}`;\r\n            messageEl.textContent = message;\r\n            container.appendChild(messageEl);\r\n            \r\n            setTimeout(() => {\r\n                messageEl.remove();\r\n            }, duration);\r\n        };\r\n        \r\n        \/\/ Cache Management\r\n        const getCachedData = () => {\r\n            if (!state.enableCache) return null;\r\n            \r\n            const now = Date.now();\r\n            if (state.cache.data && state.cache.timestamp) {\r\n                const age = now - state.cache.timestamp;\r\n                if (age < CONFIG.CACHE_DURATION) {\r\n                    updateCacheStatus('fresh');\r\n                    log('Using cached data', { age: Math.round(age \/ 1000) + 's' });\r\n                    return state.cache.data;\r\n                } else if (age < CONFIG.CACHE_DURATION * 2) {\r\n                    updateCacheStatus('stale');\r\n                    log('Cache is stale but usable');\r\n                    return state.cache.data;\r\n                }\r\n            }\r\n            updateCacheStatus('expired');\r\n            return null;\r\n        };\r\n        \r\n        const setCachedData = (data) => {\r\n            if (state.enableCache) {\r\n                state.cache.data = data;\r\n                state.cache.timestamp = Date.now();\r\n                updateCacheStatus('fresh');\r\n                log('Data cached');\r\n            }\r\n        };\r\n        \r\n        const clearCache = () => {\r\n            state.cache.data = null;\r\n            state.cache.timestamp = null;\r\n            updateCacheStatus('expired');\r\n            log('Cache cleared');\r\n        };\r\n        \r\n        const updateCacheStatus = (status) => {\r\n            const statusEl = document.getElementById('cacheStatus');\r\n            const indicator = statusEl.querySelector('.cache-indicator');\r\n            const text = document.getElementById('cacheText');\r\n            \r\n            if (state.enableCache && CONFIG.SHOW_CACHE_STATUS) {\r\n                statusEl.classList.remove('hidden');\r\n                indicator.className = `cache-indicator ${status === 'stale' ? 'stale' : status === 'expired' ? 'expired' : ''}`;\r\n                text.textContent = `Cache: ${status.charAt(0).toUpperCase() + status.slice(1)}`;\r\n            } else {\r\n                statusEl.classList.add('hidden');\r\n            }\r\n        };\r\n        \r\n        \/\/ Loading States\r\n        const showLoading = () => {\r\n            state.isLoading = true;\r\n            document.getElementById('loading-indicator').classList.remove('hidden');\r\n            document.getElementById('job-listings').classList.add('hidden');\r\n        };\r\n        \r\n        const hideLoading = () => {\r\n            state.isLoading = false;\r\n            document.getElementById('loading-indicator').classList.add('hidden');\r\n            document.getElementById('job-listings').classList.remove('hidden');\r\n        };\r\n        \r\n        \/\/ API Functions\r\n        const fetchWithRetry = async (url, retries = CONFIG.RETRY_ATTEMPTS) => {\r\n            for (let i = 0; i < retries; i++) {\r\n                try {\r\n                    const response = await fetch(url);\r\n                    if (!response.ok) {\r\n                        throw new Error(`HTTP ${response.status}`);\r\n                    }\r\n                    return await response.json();\r\n                } catch (error) {\r\n                    log(`Attempt ${i + 1} failed:`, error);\r\n                    if (i === retries - 1) throw error;\r\n                    await new Promise(resolve => setTimeout(resolve, CONFIG.RETRY_DELAY * (i + 1)));\r\n                }\r\n            }\r\n        };\r\n        \r\n        const fetchJobs = async (forceRefresh = false) => {\r\n            if (state.isLoading) return;\r\n            \r\n            \/\/ Check cache first\r\n            if (!forceRefresh) {\r\n                const cached = getCachedData();\r\n                if (cached) {\r\n                    state.allJobs = cached;\r\n                    state.filteredJobs = cached;\r\n                    displayJobs();\r\n                    return;\r\n                }\r\n            }\r\n            \r\n            showLoading();\r\n            \r\n            try {\r\n                \/\/ Prepare API URL\r\n                const params = new URLSearchParams({\r\n                    PageSize: CONFIG.PAGE_SIZE,\r\n                    clientSubdomain: CONFIG.CLIENT_SUBDOMAIN\r\n                });\r\n                \r\n                let apiUrl = CONFIG.API_BASE_URL;\r\n                let fetchUrl;\r\n                \r\n                if (state.useDirectAPI) {\r\n                    \/\/ Try direct API call\r\n                    fetchUrl = `${apiUrl}?${params}`;\r\n                    log('Using direct API:', fetchUrl);\r\n                } else {\r\n                    \/\/ Use CORS proxy\r\n                    fetchUrl = `${CONFIG.CORS_PROXY}${encodeURIComponent(apiUrl + '?' + params)}`;\r\n                    log('Using proxy:', fetchUrl);\r\n                }\r\n                \r\n                const startTime = performance.now();\r\n                const data = await fetchWithRetry(fetchUrl);\r\n                const loadTime = performance.now() - startTime;\r\n                \r\n                log(`API Response received in ${Math.round(loadTime)}ms`, data);\r\n                \r\n                if (data.details && Array.isArray(data.details.listData)) {\r\n                    const jobs = data.details.listData.map(job => ({\r\n                        title: job.listing__Title || 'No Title',\r\n                        company: job.listing__ClientName || 'Unknown Company',\r\n                        type: `${job.listing__JobType || 'Unknown'}${job.listing__WorkSchedules ? ` (${job.listing__WorkSchedules})` : ''}`,\r\n                        location: job.listing__Location || 'Unknown Location',\r\n                        salary: job.listing__Salary || 'Salary not listed',\r\n                        description: job.listing__ShortDescription || 'No description available',\r\n                        projectId: job.listing__ProjectId || null,\r\n                        raw: job \/\/ Keep raw data for debugging\r\n                    }));\r\n                    \r\n                    \/\/ Filter for Finnish jobs\r\n                    const finnishJobs = jobs.filter(job => \r\n                        job.location.toLowerCase().includes('finland') || \r\n                        job.location.toLowerCase().includes('suomi')\r\n                    );\r\n                    \r\n                    state.allJobs = finnishJobs;\r\n                    state.filteredJobs = finnishJobs;\r\n                    \r\n                    \/\/ Cache the data\r\n                    setCachedData(finnishJobs);\r\n                    \r\n                    displayJobs();\r\n                    \r\n                    if (CONFIG.SHOW_PERFORMANCE_MESSAGES && loadTime > 2000) {\r\n                        showMessage(`Loaded ${finnishJobs.length} jobs (took ${Math.round(loadTime \/ 1000)}s)`, 'info');\r\n                    }\r\n                } else {\r\n                    throw new Error('Invalid API response structure');\r\n                }\r\n            } catch (error) {\r\n                console.error('Error fetching jobs:', error);\r\n                \r\n                \/\/ Try to use stale cache if available\r\n                const staleCache = state.cache.data;\r\n                if (staleCache) {\r\n                    showMessage('Using cached data due to connection error', 'error');\r\n                    state.allJobs = staleCache;\r\n                    state.filteredJobs = staleCache;\r\n                    displayJobs();\r\n                } else {\r\n                    document.getElementById('job-listings').innerHTML = \r\n                        `<div class=\"message error\">Error loading jobs: ${error.message}. Please try again later.<\/div>`;\r\n                    hideLoading();\r\n                }\r\n            }\r\n        };\r\n        \r\n        \/\/ Display Functions\r\n        const displayJobs = () => {\r\n            const container = document.getElementById('job-listings');\r\n            const fragment = document.createDocumentFragment();\r\n            \r\n            \/\/ Clear container\r\n            container.innerHTML = '';\r\n            \r\n            if (state.filteredJobs.length === 0) {\r\n                const message = document.createElement('div');\r\n                message.className = 'message info';\r\n                message.textContent = 'Ty\u00f6paikkoja ei l\u00f6ytynyt hakuehdoillasi.';\r\n                container.appendChild(message);\r\n                hideLoading();\r\n                updateJobCount(0);\r\n                return;\r\n            }\r\n            \r\n            \/\/ Create job cards using DocumentFragment for better performance\r\n            const isListView = state.currentView === 'list';\r\n            state.filteredJobs.forEach(job => {\r\n                fragment.appendChild(createJobCard(job, isListView));\r\n            });\r\n            \r\n            container.appendChild(fragment);\r\n            hideLoading();\r\n            updateJobCount(state.filteredJobs.length);\r\n        };\r\n        \r\n        const updateJobCount = (count) => {\r\n            const countEl = document.getElementById('jobCount');\r\n            countEl.textContent = count > 0 ? `${count} ty\u00f6paikkaa` : '';\r\n        };\r\n        \r\n        \/\/ Search Functions\r\n        const performSearch = () => {\r\n            const jobSearch = document.getElementById('jobSearch').value.toLowerCase().trim();\r\n            const locationSearch = document.getElementById('locationSearch').value.toLowerCase().trim();\r\n            \r\n            if (!jobSearch && !locationSearch) {\r\n                state.filteredJobs = state.allJobs;\r\n            } else {\r\n                state.filteredJobs = state.allJobs.filter(job => {\r\n                    const matchesJob = !jobSearch || \r\n                        job.title.toLowerCase().includes(jobSearch) ||\r\n                        job.company.toLowerCase().includes(jobSearch) ||\r\n                        job.description.toLowerCase().includes(jobSearch);\r\n                    \r\n                    const matchesLocation = !locationSearch || \r\n                        job.location.toLowerCase().includes(locationSearch);\r\n                    \r\n                    return matchesJob && matchesLocation;\r\n                });\r\n            }\r\n            \r\n            displayJobs();\r\n            log(`Search results: ${state.filteredJobs.length} jobs`);\r\n        };\r\n        \r\n        \/\/ Debounced search for input events\r\n        const debouncedSearch = debounce(performSearch, CONFIG.DEBOUNCE_DELAY);\r\n        \r\n        \/\/ Event Handlers\r\n        const searchJobs = () => {\r\n            performSearch();\r\n        };\r\n        \r\n        const clearSearch = () => {\r\n            document.getElementById('jobSearch').value = '';\r\n            document.getElementById('locationSearch').value = '';\r\n            state.filteredJobs = state.allJobs;\r\n            displayJobs();\r\n        };\r\n        \r\n        const toggleView = () => {\r\n            const toggle = document.getElementById('viewToggle');\r\n            state.currentView = toggle.checked ? 'list' : 'grid';\r\n            \r\n            const jobListings = document.getElementById('job-listings');\r\n            jobListings.classList.remove('grid-view', 'list-view');\r\n            jobListings.classList.add(`${state.currentView}-view`);\r\n            \r\n            displayJobs();\r\n            log('View changed to:', state.currentView);\r\n        };\r\n        \r\n        const viewDetails = (projectId) => {\r\n            if (projectId && projectId !== 'null') {\r\n                const url = `https:\/\/joy.clevry.com\/public-job-description\/${projectId}`;\r\n                window.open(url, '_blank');\r\n                log('Opening job details:', projectId);\r\n            } else {\r\n                showMessage('Details not available for this job.', 'info');\r\n            }\r\n        };\r\n        \r\n        \r\n        const refreshJobs = () => {\r\n            fetchJobs(true);\r\n        };\r\n        \r\n        \/\/ Input event listeners for live search\r\n        const initializeEventListeners = () => {\r\n            \/\/ Add input event listeners for live search\r\n            const jobSearchInput = document.getElementById('jobSearch');\r\n            const locationSearchInput = document.getElementById('locationSearch');\r\n            \r\n            if (jobSearchInput) {\r\n                jobSearchInput.addEventListener('input', debouncedSearch);\r\n            }\r\n            \r\n            if (locationSearchInput) {\r\n                locationSearchInput.addEventListener('input', debouncedSearch);\r\n            }\r\n            \r\n            \/\/ Add keyboard shortcuts\r\n            document.addEventListener('keydown', (e) => {\r\n                \/\/ Ctrl\/Cmd + K to focus search\r\n                if ((e.ctrlKey || e.metaKey) && e.key === 'k') {\r\n                    e.preventDefault();\r\n                    jobSearchInput?.focus();\r\n                }\r\n                \r\n                \/\/ Ctrl\/Cmd + R to refresh (force)\r\n                if ((e.ctrlKey || e.metaKey) && e.key === 'r' && !e.shiftKey) {\r\n                    e.preventDefault();\r\n                    refreshJobs();\r\n                }\r\n                \r\n                \/\/ Escape to clear search\r\n                if (e.key === 'Escape') {\r\n                    if (document.activeElement === jobSearchInput || \r\n                        document.activeElement === locationSearchInput) {\r\n                        clearSearch();\r\n                    }\r\n                }\r\n            });\r\n            \r\n            \/\/ Add refresh button if needed\r\n            if (state.debugMode) {\r\n                const refreshBtn = document.createElement('button');\r\n                refreshBtn.textContent = 'Force Refresh';\r\n                refreshBtn.onclick = refreshJobs;\r\n                refreshBtn.style.marginLeft = '10px';\r\n                document.querySelector('.button-group')?.appendChild(refreshBtn);\r\n            }\r\n        };\r\n        \r\n        \/\/ Initialize the widget\r\n        const initWidget = () => {\r\n            log('Widget initializing...');\r\n            \r\n            \/\/ Initialize event listeners\r\n            initializeEventListeners();\r\n            \r\n            \/\/ Initial load\r\n            fetchJobs();\r\n            \r\n            \/\/ Optional: Auto-refresh every 5 minutes if cache is disabled\r\n            if (!state.enableCache) {\r\n                setInterval(() => {\r\n                    if (!state.isLoading) {\r\n                        log('Auto-refresh triggered');\r\n                        fetchJobs(true);\r\n                    }\r\n                }, 5 * 60 * 1000);\r\n            }\r\n            \r\n            \/\/ Performance monitoring in debug mode\r\n            if (state.debugMode && window.performance && window.performance.mark) {\r\n                window.performance.mark('widget-initialized');\r\n                const perfData = window.performance.getEntriesByType('navigation')[0];\r\n                if (perfData) {\r\n                    log('Page load performance:', {\r\n                        domContentLoaded: Math.round(perfData.domContentLoadedEventEnd - perfData.domContentLoadedEventStart) + 'ms',\r\n                        loadComplete: Math.round(perfData.loadEventEnd - perfData.loadEventStart) + 'ms',\r\n                        totalTime: Math.round(perfData.loadEventEnd - perfData.fetchStart) + 'ms'\r\n                    });\r\n                }\r\n            }\r\n        };\r\n        \r\n        \/\/ Start the widget when DOM is ready\r\n        if (document.readyState === 'loading') {\r\n            document.addEventListener('DOMContentLoaded', initWidget);\r\n        } else {\r\n            \/\/ DOM is already ready\r\n            initWidget();\r\n        }\r\n    <\/script>\r\n<\/body>\r\n<\/html>\r\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-7ae3021 e-flex e-con-boxed e-con e-parent\" data-id=\"7ae3021\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-4d0d51d elementor-widget elementor-widget-global elementor-global-13508 elementor-widget-html\" data-id=\"4d0d51d\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<style>\n.elementor-element-5ab4dd3 {\n    display: none;\n}    \n<\/style>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"","protected":false},"author":2,"featured_media":11092,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_acf_changed":false,"footnotes":""},"class_list":["post-44596","page","type-page","status-publish","has-post-thumbnail","hentry"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v25.9 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Avoimet Ty\u00f6paikat - Clevry<\/title>\n<meta name=\"description\" content=\"T\u00e4\u00e4lt\u00e4 l\u00f6yd\u00e4t kaikki avoimet ty\u00f6paikat, joita Clevry t\u00e4ll\u00e4 hetkell\u00e4 tarjoaa Suomessa.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/\" \/>\n<meta property=\"og:locale\" content=\"fi_FI\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Avoimet Ty\u00f6paikat - Clevry\" \/>\n<meta property=\"og:description\" content=\"T\u00e4\u00e4lt\u00e4 l\u00f6yd\u00e4t kaikki avoimet ty\u00f6paikat, joita Clevry t\u00e4ll\u00e4 hetkell\u00e4 tarjoaa Suomessa.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/\" \/>\n<meta property=\"og:site_name\" content=\"Clevry\" \/>\n<meta property=\"article:modified_time\" content=\"2024-08-13T09:50:25+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.clevry.com\/wp-content\/uploads\/2023\/01\/Featured-Image.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"628\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"14 minuuttia\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/\",\"url\":\"https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/\",\"name\":\"Avoimet Ty\u00f6paikat - Clevry\",\"isPartOf\":{\"@id\":\"https:\/\/www.clevry.com\/fi\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.clevry.com\/wp-content\/uploads\/2023\/01\/Featured-Image.jpg\",\"datePublished\":\"2024-08-13T06:42:05+00:00\",\"dateModified\":\"2024-08-13T09:50:25+00:00\",\"description\":\"T\u00e4\u00e4lt\u00e4 l\u00f6yd\u00e4t kaikki avoimet ty\u00f6paikat, joita Clevry t\u00e4ll\u00e4 hetkell\u00e4 tarjoaa Suomessa.\",\"breadcrumb\":{\"@id\":\"https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/#breadcrumb\"},\"inLanguage\":\"fi\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"fi\",\"@id\":\"https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/#primaryimage\",\"url\":\"https:\/\/www.clevry.com\/wp-content\/uploads\/2023\/01\/Featured-Image.jpg\",\"contentUrl\":\"https:\/\/www.clevry.com\/wp-content\/uploads\/2023\/01\/Featured-Image.jpg\",\"width\":1200,\"height\":628,\"caption\":\"Arbetspsykologiska tester\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/www.clevry.com\/fi\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Avoimet Ty\u00f6paikat\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/www.clevry.com\/fi\/#website\",\"url\":\"https:\/\/www.clevry.com\/fi\/\",\"name\":\"Clevry\",\"description\":\"The soft skills platform\",\"publisher\":{\"@id\":\"https:\/\/www.clevry.com\/fi\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/www.clevry.com\/fi\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"fi\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/www.clevry.com\/fi\/#organization\",\"name\":\"Clevry\",\"url\":\"https:\/\/www.clevry.com\/fi\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"fi\",\"@id\":\"https:\/\/www.clevry.com\/fi\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/www.clevry.com\/wp-content\/uploads\/2022\/11\/Group-14-Copy.svg\",\"contentUrl\":\"https:\/\/www.clevry.com\/wp-content\/uploads\/2022\/11\/Group-14-Copy.svg\",\"width\":152,\"height\":48,\"caption\":\"Clevry\"},\"image\":{\"@id\":\"https:\/\/www.clevry.com\/fi\/#\/schema\/logo\/image\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Avoimet Ty\u00f6paikat - Clevry","description":"T\u00e4\u00e4lt\u00e4 l\u00f6yd\u00e4t kaikki avoimet ty\u00f6paikat, joita Clevry t\u00e4ll\u00e4 hetkell\u00e4 tarjoaa Suomessa.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/","og_locale":"fi_FI","og_type":"article","og_title":"Avoimet Ty\u00f6paikat - Clevry","og_description":"T\u00e4\u00e4lt\u00e4 l\u00f6yd\u00e4t kaikki avoimet ty\u00f6paikat, joita Clevry t\u00e4ll\u00e4 hetkell\u00e4 tarjoaa Suomessa.","og_url":"https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/","og_site_name":"Clevry","article_modified_time":"2024-08-13T09:50:25+00:00","og_image":[{"width":1200,"height":628,"url":"https:\/\/www.clevry.com\/wp-content\/uploads\/2023\/01\/Featured-Image.jpg","type":"image\/jpeg"}],"twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"14 minuuttia"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/","url":"https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/","name":"Avoimet Ty\u00f6paikat - Clevry","isPartOf":{"@id":"https:\/\/www.clevry.com\/fi\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/#primaryimage"},"image":{"@id":"https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/#primaryimage"},"thumbnailUrl":"https:\/\/www.clevry.com\/wp-content\/uploads\/2023\/01\/Featured-Image.jpg","datePublished":"2024-08-13T06:42:05+00:00","dateModified":"2024-08-13T09:50:25+00:00","description":"T\u00e4\u00e4lt\u00e4 l\u00f6yd\u00e4t kaikki avoimet ty\u00f6paikat, joita Clevry t\u00e4ll\u00e4 hetkell\u00e4 tarjoaa Suomessa.","breadcrumb":{"@id":"https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/#breadcrumb"},"inLanguage":"fi","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/"]}]},{"@type":"ImageObject","inLanguage":"fi","@id":"https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/#primaryimage","url":"https:\/\/www.clevry.com\/wp-content\/uploads\/2023\/01\/Featured-Image.jpg","contentUrl":"https:\/\/www.clevry.com\/wp-content\/uploads\/2023\/01\/Featured-Image.jpg","width":1200,"height":628,"caption":"Arbetspsykologiska tester"},{"@type":"BreadcrumbList","@id":"https:\/\/www.clevry.com\/fi\/avoimet-tyopaikat\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.clevry.com\/fi\/"},{"@type":"ListItem","position":2,"name":"Avoimet Ty\u00f6paikat"}]},{"@type":"WebSite","@id":"https:\/\/www.clevry.com\/fi\/#website","url":"https:\/\/www.clevry.com\/fi\/","name":"Clevry","description":"The soft skills platform","publisher":{"@id":"https:\/\/www.clevry.com\/fi\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.clevry.com\/fi\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"fi"},{"@type":"Organization","@id":"https:\/\/www.clevry.com\/fi\/#organization","name":"Clevry","url":"https:\/\/www.clevry.com\/fi\/","logo":{"@type":"ImageObject","inLanguage":"fi","@id":"https:\/\/www.clevry.com\/fi\/#\/schema\/logo\/image\/","url":"https:\/\/www.clevry.com\/wp-content\/uploads\/2022\/11\/Group-14-Copy.svg","contentUrl":"https:\/\/www.clevry.com\/wp-content\/uploads\/2022\/11\/Group-14-Copy.svg","width":152,"height":48,"caption":"Clevry"},"image":{"@id":"https:\/\/www.clevry.com\/fi\/#\/schema\/logo\/image\/"}}]}},"_links":{"self":[{"href":"https:\/\/www.clevry.com\/fi\/wp-json\/wp\/v2\/pages\/44596","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.clevry.com\/fi\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.clevry.com\/fi\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.clevry.com\/fi\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.clevry.com\/fi\/wp-json\/wp\/v2\/comments?post=44596"}],"version-history":[{"count":8,"href":"https:\/\/www.clevry.com\/fi\/wp-json\/wp\/v2\/pages\/44596\/revisions"}],"predecessor-version":[{"id":44665,"href":"https:\/\/www.clevry.com\/fi\/wp-json\/wp\/v2\/pages\/44596\/revisions\/44665"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.clevry.com\/fi\/wp-json\/wp\/v2\/media\/11092"}],"wp:attachment":[{"href":"https:\/\/www.clevry.com\/fi\/wp-json\/wp\/v2\/media?parent=44596"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}