Last Updated: 2025-11-28
LaunchPad is a project planning tool for educators to design high-quality, personalized project-based learning experiences. This is an Ember.js web application that provides a visual storyboard interface for creating and managing educational projects with various learning activities (tiles).
Status: This repository has been moved into a monorepo at https://github.com/savilabs/launchpad, but is still maintained as a standalone repository.
- Framework: Ember.js 3.3 (Classic syntax, pre-Octane)
- Data Layer: Ember Data 3.3 with ActiveModelAdapter
- Authentication: OAuth2 via ember-simple-auth
- UI Framework: Bootstrap 3
- Styling: LESS preprocessor
- Testing: QUnit with ember-cli-qunit
- Node Version: 10.x (managed via nvm)
- Deployment: Firebase Hosting (staging and production)
- Production: https://launchpad-ui-production.firebaseapp.com
- Staging: https://launchpad-ui-staging.firebaseapp.com
- API (dev): http://localhost:3090
- API (staging): https://launchpad-api-staging.herokuapp.com
- API (production): https://launchpad-api-production.herokuapp.com
launchpad-ui/
├── app/ # Main application code
│ ├── adapters/ # Ember Data API adapters
│ ├── authenticators/ # OAuth2 authentication
│ ├── authorizers/ # Request authorization
│ ├── components/ # UI components (51 total)
│ ├── controllers/ # Route-bound state
│ ├── helpers/ # Handlebars template helpers
│ ├── initializers/ # App startup hooks
│ ├── mixins/ # Shared behavior (9 mixins)
│ ├── models/ # Ember Data models (15 models)
│ ├── routes/ # Route handlers (33 routes)
│ ├── serializers/ # Data serialization
│ ├── services/ # Singleton services
│ ├── styles/ # LESS stylesheets
│ ├── templates/ # Handlebars templates (105 files)
│ ├── transforms/ # Custom data transforms
│ ├── app.js # Application entry point
│ └── router.js # Route definitions
├── config/ # Configuration files
│ ├── environment.js # Environment-specific config
│ ├── optional-features.json # Feature flags
│ └── targets.js # Browser targets
├── tests/ # Test suite
│ ├── integration/ # Integration tests
│ ├── unit/ # Unit tests
│ └── helpers/ # Test utilities
├── public/ # Static assets
├── vendor/ # Third-party code
├── ember-cli-build.js # Build configuration
└── package.json # Dependencies and scripts
This application uses Ember.js 3.3 with Classic syntax (not Octane):
- Components:
Component.extend()with lifecycle hooks - Properties: Use
set()andget()for updates - Computed Properties:
computed()with dependent keys - Actions: Defined in
actionshash - Lifecycle:
init(),didInsertElement(),willDestroyElement()
Location: app/adapters/application.js, app/models/, app/serializers/
The application uses Ember Data with the ActiveModelAdapter for Rails-compatible API communication:
// API Configuration
API_NAMESPACE: 'v1'
API_HOST: varies by environment
API_ENDPOINT: {API_HOST}/v1Core Models:
project.js- Learning projects with tiles and designerstile.js- Individual learning activities (polymorphic: Launch, Experience, Artifact, Checkpoint, Exhibition)user.js/current-user.js- User accountsproject-member.js- Team members on projectsproject-invite.js- Pending invitations (custom primaryKey:invite_token)performance-indicator.js- Learning standards/proficienciesapplied-performance-indicator.js- Indicators applied to tiles
Model Relationships:
Project
→ hasMany: tiles, appliedPerformanceIndicators, designers, sourceDesigners
Tile
→ belongsTo: project (sync)
→ hasMany: appliedPerformanceIndicators (async)
User
→ hasMany: projects (async, inverse: null)Location: app/authenticators/oauth2.js, app/authorizers/oauth2.js, app/services/current-user.js
- OAuth2 password grant flow
- Bearer token in Authorization header
- Session management via ember-simple-auth
- Route protection via
RequireCurrentUsermixin - Automatic redirect to signin on auth failure
Key Service: current-user
- Methods:
load(),login(),logout() - Injected into most routes and components
- Tracks authenticated user state
Location: app/router.js, app/routes/
The application uses deeply nested routes with 33 total route handlers:
/ (home)
├── auth/{signin,signup}
├── invite-accept (/invites/:token)
├── forgot-password/:reset_token
├── gallery (public project browsing)
└── main (authenticated area)
├── dashboard (user's projects)
├── user-profile (/profile)
├── projects
│ ├── new
│ └── show (:id)
│ ├── overview/edit
│ ├── storyboard
│ │ ├── launches/:tile_id/{edit,move}
│ │ ├── experiences/:tile_id/{edit,move}
│ │ ├── artifacts/:tile_id/{edit,move}
│ │ ├── checkpoints/:tile_id/{edit,move}
│ │ └── exhibitions/:tile_id/{edit,move}
│ ├── proficiencies
│ └── team
└── users/:id
Route Mixins:
ApplicationRoute- Base auth handling + redirectsRequireCurrentUser- Protected route enforcementEditTileRoute- Permission checking for tile editingResetScroll- Reset scroll on transition
Location: app/components/, app/templates/components/
51 reusable components organized by feature area:
Authentication & User:
user-profile,user-profile-form,user-cardpassword-reset-form,password-reset-request-forminvite-accept
Project Management:
project-card,project-form,project-view,project-galleryproject-copy,new-project-member-formapp-dashboard
Tile/Storyboard (Core Feature):
tile-card- Individual tile displaytile-form- Generic tile form with validationtile-form-{launch,experience,artifact,checkpoint,exhibition}- Type-specific formstile-modal- Modal wrappertile-nav,tile-nav-item- Navigation between tilestile-move- Move tile to different projectgrouped-tiles- Group tiles by week
Performance Indicators:
indicator-input,indicator-with-scaleapplied-performance-indicatortile-indicator-select
UI Utilities:
file-upload- Cloudinary image uploadsfile-editable- Inline file editingflash-messages- Toast notificationsform-group,form-item-errors- Form helperswysiwyg-editor- Rich text (Trix editor)main-navigation- Top nav bar- Icon components for each tile type
Component Mixins:
TileForm- Form validation and submissionTileModal- Modal behaviorTileModalEdit- Edit modal behaviorRichText- Format rich text with target="_blank"
Location: app/services/
Core Services:
current-user- Authenticated user managementsession- OAuth2 session (ember-simple-auth)flash-messages- Notifications (3s auto-dismiss)store- Ember Data persistenceajax/authorized-ajax- HTTP requestsmetrics- Google Analytics tracking
Location: app/styles/
- LESS preprocessor with Bootstrap 3
- Component-scoped LESS files in
styles/components/ - Shared variables in
styles/variables/ - BEM-like naming:
.ClassName-element--modifier - Custom Bootstrap theme with brand colors
The application centers around a tile-based storyboard for organizing learning activities in a project.
Five Tile Types:
- Launch - Project kickoff activities
- Experience - Learning experiences
- Artifact - Student work products
- Checkpoint - Assessment points
- Exhibition - Final presentations
Tile Properties (from app/models/tile.js):
type- Tile type (polymorphic)position- Order within weekweek- Week number in projecttitle,description- ContentlearningSteps- Rich text instructionsisMilestone- Flag for important tilesstartDate- Scheduled dateimageId- Cloudinary image referenceappliedPerformanceIndicators- Tagged learning standards
Polymorphic Routing:
- Each tile type has its own nested route
- Routes follow pattern:
/projects/:id/storyboard/{type}s/:tile_id - Type-specific edit forms via components
Computed Properties (key pattern):
typeForRoute: computed('type', function() { ... })
routeForType: computed('type', function() { ... })
editRouteForType: computed('type', function() { ... })# Install and use Node 10.x
nvm install 10.24.1
nvm use 10.24.1
# Install dependencies
npm install
# Install Ember CLI globally (optional)
npm install -g ember-cli@3.3.0# Start development server (runs at localhost:4200)
ember serve
# or
npm start
# The app expects the API to be running at localhost:3090
# ember-cli-build.js configures a proxy for development# Run all tests once
ember test
# or
npm test
# Run tests in watch mode
ember test --server
# Linting
npm run lint:js
npm run lint:js -- --fixTest Structure:
- Unit tests:
tests/unit/(models, transforms, mixins) - Integration tests:
tests/integration/components/ - Uses QUnit with ember-qunit helpers
- Test runner: Testem with Chrome headless
# Development build
ember build
# Production build
ember build --environment production
# Production build with asset fingerprinting
FINGERPRINT_ASSETS=true ember build --environment productionEmber CLI provides generators:
ember generate component my-component
ember generate route my-route
ember generate model my-model
ember generate service my-service
ember generate helper my-helper
ember generate mixin my-mixin-
Components: Kebab-case files → camelCase in code
- File:
app/components/project-card.js - Template:
app/templates/components/project-card.hbs - Usage:
{{project-card}}
- File:
-
Routes: Directory structure mirrors route nesting
- Route:
main.projects.show.storyboard.launches.launch.edit - File:
app/routes/main/projects/show/storyboard/launches/launch/edit.js
- Route:
-
Models: Singular names
- File:
app/models/project.js - Usage:
store.findRecord('project', id)
- File:
- Props passed from parent via template
- Actions passed as strings, triggered via
send() - Two-way binding via
set() - Avoid direct model mutation in components
Example:
// Component actions
actions: {
handleEdit() {
this.sendAction('onEdit', this.get('project'));
}
}Standard Route:
export default Route.extend({
currentUser: service(),
beforeModel() {
// Auth checks, redirects
},
model(params) {
return this.store.findRecord('project', params.id);
},
afterModel(model) {
// Additional setup
}
});Always inject services needed in routes/components:
import { inject as service } from '@ember/service';
export default Component.extend({
currentUser: service(),
flashMessages: service(),
actions: {
doSomething() {
const user = this.get('currentUser.user');
this.get('flashMessages').success('Done!');
}
}
});- Route-level error actions catch failures
- 404 responses redirect to
not-foundroute - User feedback via
flash-messagesservice - Validation errors displayed via
errors-forhelper
Example:
- Configured in
app/initializers/cloudinary.js - Upload preset:
launchpad - Cloudinary name:
agilion - Integration via
file-uploadcomponent - Image URLs generated with transformations
- Uses
ember-modal-dialogaddon - Tile editing uses modal overlays
- Modals rendered via outlets
- Close actions bubble up to parent routes
- Model validations in
app/models/ TileFormmixin provides validation helpers- Errors displayed per-field via
form-item-errors - Flash messages for success/failure
Location: config/environment.js
Environments:
development- Local development, API at localhost:3090test- Testing mode with disabled analyticsstaging- Staging deployment with debug GAproduction- Production deployment
Key Config Values:
modulePrefix: 'launch-pad'
API_NAMESPACE: 'v1'
API_HOST: varies by environment
CLOUDINARY_NAME: 'agilion'
CLOUDINARY_UPLOAD_PRESET: 'launchpad'
GA_ID: varies by environmentCSP headers allow:
- Fonts: Google Fonts, data URIs
- Scripts: Google Analytics, inline scripts
- Images: Cloudinary, Gravatar, S3
- Connections: API hosts, Cloudinary, GA
Location: ember-cli-build.js
- Asset fingerprinting via
FINGERPRINT_ASSETSenv var - Bootstrap 3 integration with fonts
- Cloudinary upload library imports
- Development proxy to API server
Location: .github/workflows/ember.yml
Workflow:
- Checkout code
- Setup Node 10.x with npm cache
- Install dependencies:
npm ci - Run tests:
npm test - Run linting:
npm run lint:js
Triggers:
- Push to
masterbranch - Pull requests to
master
Deployment to Firebase Hosting (TBD - see README)
- Staging: launchpad-ui-staging.firebaseapp.com
- Production: launchpad-ui-production.firebaseapp.com
ember-source~3.3.0ember-cli~3.3.0ember-data~3.3.0
active-model-adapter2.2.0 - Rails-compatible adapterember-ajax^3.0.0 - AJAX requestsember-inflector^2.3.0 - String pluralization
ember-simple-auth^1.7.0
ember-bootstrap^2.0.0 +bootstrap^3.3.7ember-modal-dialog2.4.3ember-trix-editor- Rich text editingemberx-select^3.1.1ember-pikaday^2.2.4 - Date pickerember-drag-drop^0.5.1ember-sortable^1.11.2
ember-moment7.7.0 - Date/timeember-cli-flash1.7.0 - Notificationsember-metrics^0.13.0 - Analyticsember-keyboard^3.0.2cloudinary-jquery-file-upload^2.5.0
eslint+eslint-plugin-ember- Lintingember-cli-less1.5.5ember-cli-qunit^4.3.2qunit-dom^0.6.2
- Generate component:
ember generate component my-component - Implement component logic in
app/components/my-component.js - Create template in
app/templates/components/my-component.hbs - Add component styles to
app/styles/components/my-component.less - Import LESS in
app/styles/app.less - Write tests in
tests/integration/components/my-component-test.js
- Add route to
app/router.js - Generate handler:
ember generate route my-route - Implement model hook and actions
- Create template in
app/templates/my-route.hbs - Add navigation link in appropriate template
- Consider authentication requirements (mixins)
- Generate model:
ember generate model my-model - Define attributes and relationships
- Create serializer if needed:
ember generate serializer my-model - Write unit tests in
tests/unit/models/my-model-test.js - Update related models for relationships
- Check adapter:
app/adapters/application.js(or model-specific) - Check serializer for data transformation
- Update model attributes/relationships
- Test with API endpoints
- Handle errors appropriately
- Locate relevant LESS file in
app/styles/components/ - Use Bootstrap 3 classes and variables
- Follow BEM-like naming conventions
- Import new LESS files in
app/styles/app.less - Test across different screen sizes
Test pure functions, computed properties, model logic:
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Model | project', function(hooks) {
setupTest(hooks);
test('it has correct attributes', function(assert) {
let model = this.owner.lookup('service:store').createRecord('project');
assert.ok(model);
});
});Test component rendering and user interaction:
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | project-card', function(hooks) {
setupRenderingTest(hooks);
test('it renders', async function(assert) {
await render(hbs`{{project-card}}`);
assert.dom(this.element).exists();
});
});@ember/test-helpers- Modern Ember test helpersqunit-dom- DOM assertion helpers- Custom helpers in
tests/helpers/
Node Version Mismatch:
nvm use 10.24.1Dependency Issues:
rm -rf node_modules package-lock.json
npm installAPI Connection Errors:
- Ensure API is running at localhost:3090
- Check
config/environment.jsfor correct API_HOST - Verify OAuth2 credentials
Build Failures:
- Check for LESS syntax errors
- Verify all imports are correct
- Clear
tmp/anddist/directories
Test Failures:
- Check for missing test setup
- Verify test helpers are imported
- Ensure test environment is configured correctly
Location: .eslintrc.js
- Extends
eslint:recommendedandplugin:ember/recommended - ECMAScript 2017 support
- Browser environment
- Node environment for config files
- Use
set()andget()for property access - Computed properties for derived state
- Actions for event handling
- Services for shared state
- Mixins for shared behavior
- Avoid direct DOM manipulation
- Components: kebab-case
- Services: kebab-case
- Models: singular, kebab-case
- Routes: dot-separated paths
- Variables: camelCase
- Classes: PascalCase
- Add comments for complex logic
- Document public APIs
- Explain "why" not "what"
- Keep comments up-to-date
- Project: A learning expedition with multiple activities
- Tile: Individual learning activity or milestone
- Storyboard: Visual timeline of project activities
- Week: Organizational unit (projects span multiple weeks)
- Performance Indicator: Learning standard or proficiency
- Designer: Educator who created the project
- Team Member: Collaborator on a project
- Designer: Can create and edit projects
- Team Member: Can collaborate on projects
- Guest: Can view public gallery
Projects can be:
- Private (only team members)
- Public (visible in gallery)
- Can be applied to any tile
- Track learning standards alignment
- Multiple indicators per tile
- System-wide catalog of indicators
- Always use Classic Ember syntax - This is a pre-Octane application
- Read before editing - Always read files before making changes
- Respect Ember conventions - Follow the framework's patterns
- Test your changes - Run tests after modifications
- Use mixins for shared behavior - Don't duplicate code
- Inject services - Don't access global state
- Handle errors gracefully - Use flash messages for user feedback
- Follow the data flow - Props down, actions up
- Check authentication - Many routes require login
- Use Ember CLI generators - They ensure correct structure
- Read relevant files first using Read tool
- Use Edit tool for modifications (preserve formatting)
- Run tests to verify changes
- Check linting with
npm run lint:js - Update tests if adding new functionality
- Consider backward compatibility
- Document significant changes
When referencing code locations, use the pattern:
file_path:line_number(e.g.,app/models/project.js:42)
This allows users to navigate directly to the source code.