Examples
Practical examples and code snippets for common Teachfloor app patterns.
Backend Integration (No UI)
Apps can run without any UI, listening to platform events and integrating with external services.
Use Case: Intercom Event Tracking
Track all user learning events to Intercom for customer engagement and analytics.
{
"id": "intercom-integration",
"version": "1.0.0",
"name": "Intercom Integration",
"description": "Track user learning events in Intercom",
"distribution_type": "private",
"permissions": [
{
"permission": "user_read",
"purpose": "Send user profile to Intercom"
},
{
"permission": "user_events_read",
"purpose": "Track learning activities in Intercom"
},
{
"permission": "course_read",
"purpose": "Include course context in event tracking"
}
]
}
No
ui_extension
field - this app runs entirely in the background.// src/index.js
import { initialize, subscribeToEvent, useExtensionContext } from '@teachfloor/extension-kit'
// Configuration
const INTERCOM_API_URL = 'https://api.intercom.io/events'
const INTERCOM_TOKEN = 'YOUR_INTERCOM_TOKEN' // In production, store securely
// Initialize the app
initialize()
// Listen to all user events
subscribeToEvent('auth.user.event', async (eventData, objectContext) => {
try {
// Map Teachfloor events to Intercom events
await fetch(INTERCOM_API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${INTERCOM_TOKEN}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
event_name: `teachfloor_${eventData.event}`,
created_at: eventData.timestamp,
user_id: eventData.id,
email: eventData.email,
metadata: {
course_id: objectContext.course?.id,
course_title: objectContext.course?.title,
module_id: objectContext.module?.id,
element_id: objectContext.element?.id,
viewport: eventData.viewport,
// Include event-specific data
...(eventData.status && { status: eventData.status }),
...(eventData.score && { score: eventData.score })
}
})
})
console.log(`Event ${eventData.event} sent to Intercom`)
} catch (error) {
console.error('Failed to send event to Intercom:', error)
}
})
// Track viewport changes for navigation analytics
subscribeToEvent('environment.viewport.changed', async (viewport, objectContext) => {
try {
await fetch(INTERCOM_API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${INTERCOM_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
event_name: 'teachfloor_page_view',
created_at: Math.floor(Date.now() / 1000),
metadata: {
viewport,
course_id: objectContext.course?.id,
module_id: objectContext.module?.id
}
})
})
} catch (error) {
console.error('Failed to track page view:', error)
}
})
console.log('Intercom integration initialized - listening for events')
How It Works
- No UI: The app has no visual components - it runs silently in the background
- Event Listening: Subscribes to
auth.user.event
to track all user activities - Data Forwarding: Sends events to Intercom API with context (course, module, element)
- Always Active: Once installed, it continuously tracks events for all users
Similar Patterns
Use this pattern for:
- Segment/Mixpanel: Analytics event tracking
- HubSpot/Salesforce: CRM activity sync
- Slack/Discord: Event notifications
- Webhooks: Custom integrations
- Data Warehouses: Learning data export
Simple Widget
A minimal widget that displays user information.
Manifest
{
"id": "simple-widget",
"version": "1.0.0",
"name": "Simple Widget",
"description": "Display user greeting",
"distribution_type": "private",
"ui_extension": {
"views": [
{
"viewport": "teachfloor.dashboard.course.detail",
"component": "Widget"
}
]
}
}
// src/views/Widget.jsx
import React from 'react'
import {
Container,
Text,
Avatar,
Group,
useExtensionContext
} from '@teachfloor/extension-kit'
const Widget = () => {
const { userContext } = useExtensionContext()
return (
<Container p="md">
<Group spacing="md">
<Avatar src={userContext.avatar} alt={userContext.full_name} />
<div>
<Text fw={600}>Hello, {userContext.full_name}!</Text>
<Text size="sm" c="dimmed">{userContext.email}</Text>
</div>
</Group>
</Container>
)
}
export default Widget
Note-Taking App
A complete note-taking application using browser storage.
{
"id": "notes-app",
"version": "1.0.0",
"name": "Notes",
"description": "Take notes while learning",
"distribution_type": "private",
"ui_extension": {
"views": [
{
"viewport": "teachfloor.dashboard.course.detail",
"component": "NotesApp"
},
{
"viewport": "teachfloor.dashboard.course.module.detail",
"component": "NotesApp"
}
]
},
"permissions": [
{
"permission": "userdata_write",
"purpose": "Save and load your notes"
}
]
}
// src/hooks/useNotes.jsx
import { useState, useEffect } from 'react'
import { showToast, store, retrieve } from '@teachfloor/extension-kit'
export function useNotes() {
const [notes, setNotes] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
loadNotes()
}, [])
async function loadNotes() {
try {
const data = await retrieve('notes', 'userdata')
setNotes(data || [])
} catch (error) {
showToast('Failed to load notes', { type: 'error' })
} finally {
setLoading(false)
}
}
async function addNote(note) {
try {
const newNote = {
id: Date.now(),
title: note.title,
content: note.content,
createdAt: new Date().toISOString()
}
const updated = [...notes, newNote]
await store('notes', updated, 'userdata')
setNotes(updated)
showToast('Note saved!', { type: 'success' })
} catch (error) {
showToast('Failed to save note', { type: 'error' })
}
}
async function updateNote(id, updates) {
try {
const updated = notes.map(note =>
note.id === id ? { ...note, ...updates } : note
)
await store('notes', updated, 'userdata')
setNotes(updated)
showToast('Note updated!', { type: 'success' })
} catch (error) {
showToast('Failed to update note', { type: 'error' })
}
}
async function deleteNote(id) {
try {
const updated = notes.filter(note => note.id !== id)
await store('notes', updated, 'userdata')
setNotes(updated)
showToast('Note deleted!', { type: 'success' })
} catch (error) {
showToast('Failed to delete note', { type: 'error' })
}
}
return { notes, loading, addNote, updateNote, deleteNote }
}
// src/views/NotesApp.jsx
import React, { useState } from 'react'
import {
Container,
SimpleGrid,
Button,
Text,
Loader,
showDrawer,
hideDrawer
} from '@teachfloor/extension-kit'
import { useNotes } from '../hooks/useNotes'
import NotesList from '../components/NotesList'
import NoteForm from '../components/NoteForm'
const NotesApp = () => {
const { notes, loading, addNote, updateNote, deleteNote } = useNotes()
const [showForm, setShowForm] = useState(false)
const [editingNote, setEditingNote] = useState(null)
React.useEffect(() => {
showDrawer()
return () => hideDrawer()
}, [])
function handleNew() {
setEditingNote(null)
setShowForm(true)
}
function handleEdit(note) {
setEditingNote(note)
setShowForm(true)
}
async function handleSave(note) {
if (editingNote) {
await updateNote(editingNote.id, note)
} else {
await addNote(note)
}
setShowForm(false)
setEditingNote(null)
}
if (loading) {
return <Loader />
}
if (showForm) {
return (
<NoteForm
note={editingNote}
onSave={handleSave}
onCancel={() => setShowForm(false)}
/>
)
}
return (
<Container p="md">
<SimpleGrid verticalSpacing="md">
<Text size="xl" fw={700}>My Notes</Text>
{notes.length === 0 ? (
<div>
<Text c="dimmed">No notes yet. Create your first note!</Text>
<Button onClick={handleNew} mt="md">Create Note</Button>
</div>
) : (
<>
<Button onClick={handleNew}>New Note</Button>
<NotesList
notes={notes}
onEdit={handleEdit}
onDelete={deleteNote}
/>
</>
)}
</SimpleGrid>
</Container>
)
}
export default NotesApp
// src/components/NotesList.jsx
import React from 'react'
import { SimpleGrid, Group, Text, Button } from '@teachfloor/extension-kit'
const NotesList = ({ notes, onEdit, onDelete }) => {
return (
<SimpleGrid verticalSpacing="sm">
{notes.map(note => (
<div key={note.id} style={{ border: '1px solid #e0e0e0', padding: '12px', borderRadius: '4px' }}>
<Text fw={600}>{note.title}</Text>
<Text size="sm" c="dimmed">{note.content}</Text>
<Group spacing="sm" mt="sm">
<Button size="xs" variant="subtle" onClick={() => onEdit(note)}>Edit</Button>
<Button size="xs" variant="subtle" color="red" onClick={() => onDelete(note.id)}>Delete</Button>
</Group>
</div>
))}
</SimpleGrid>
)
}
export default NotesList
// src/components/NoteForm.jsx
import React, { useState } from 'react'
import {
Container,
SimpleGrid,
TextInput,
Textarea,
Button,
Group
} from '@teachfloor/extension-kit'
const NoteForm = ({ note, onSave, onCancel }) => {
const [title, setTitle] = useState(note?.title || '')
const [content, setContent] = useState(note?.content || '')
function handleSubmit(e) {
e.preventDefault()
onSave({ title, content })
}
return (
<Container p="md">
<form onSubmit={handleSubmit}>
<SimpleGrid verticalSpacing="md">
<TextInput
label="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
<Textarea
label="Content"
value={content}
onChange={(e) => setContent(e.target.value)}
minRows={6}
required
/>
<Group spacing="sm">
<Button type="submit">Save</Button>
<Button variant="subtle" onClick={onCancel}>Cancel</Button>
</Group>
</SimpleGrid>
</form>
</Container>
)
}
export default NoteForm
Progress Tracker
Track user's learning progress.
{
"id": "progress-tracker",
"version": "1.0.0",
"name": "Progress Tracker",
"description": "Track your learning progress",
"ui_extension": {
"views": [
{
"viewport": "teachfloor.dashboard.course.detail",
"component": "ProgressTracker"
}
]
},
"permissions": [
{
"permission": "user_events_read",
"purpose": "Track your learning activities"
},
{
"permission": "course_read",
"purpose": "Display course information"
}
]
}
// src/views/ProgressTracker.jsx
import React, { useState, useEffect } from 'react'
import {
Container,
SimpleGrid,
Text,
Progress,
Badge,
Loader
} from '@teachfloor/extension-kit'
const ProgressTracker = () => {
const [stats, setStats] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
loadStats()
}, [])
async function loadStats() {
// Simulated data - replace with actual API calls
const mockStats = {
totalModules: 10,
completedModules: 6,
totalElements: 50,
completedElements: 32,
timeSpent: 1200 // minutes
}
setStats(mockStats)
setLoading(false)
}
if (loading) {
return <Loader />
}
const moduleProgress = (stats.completedModules / stats.totalModules) * 100
const elementProgress = (stats.completedElements / stats.totalElements) * 100
return (
<Container p="md">
<SimpleGrid verticalSpacing="lg">
<Text size="xl" fw={700}>Your Progress</Text>
<div>
<Text size="sm" fw={500} mb="xs">Modules</Text>
<Progress value={moduleProgress} color="blue" />
<Text size="sm" c="dimmed" mt="xs">
{stats.completedModules} of {stats.totalModules} completed
</Text>
</div>
<div>
<Text size="sm" fw={500} mb="xs">Elements</Text>
<Progress value={elementProgress} color="green" />
<Text size="sm" c="dimmed" mt="xs">
{stats.completedElements} of {stats.totalElements} completed
</Text>
</div>
<div>
<Text size="sm" fw={500} mb="xs">Time Spent</Text>
<Badge size="lg">{Math.floor(stats.timeSpent / 60)} hours</Badge>
</div>
</SimpleGrid>
</Container>
)
}
export default ProgressTracker
Settings Page
App configuration interface.
// src/views/AppSettings.jsx
import React, { useState, useEffect } from 'react'
import {
SettingsView,
SimpleGrid,
TextInput,
Select,
Switch,
showToast,
store,
retrieve
} from '@teachfloor/extension-kit'
const AppSettings = () => {
const [status, setStatus] = useState('')
const [settings, setSettings] = useState(null)
useEffect(() => {
loadSettings()
}, [])
async function loadSettings() {
const saved = await retrieve('settings', 'appdata')
setSettings(saved || {
apiKey: '',
language: 'en',
notifications: true
})
}
async function saveSettings(values) {
setStatus('Saving...')
try {
await store('settings', values, 'appdata')
setStatus('Saved successfully!')
showToast('Settings saved!', { type: 'success' })
} catch (error) {
setStatus('Failed to save settings')
showToast('Failed to save', { type: 'error' })
}
}
if (!settings) {
return <div>Loading...</div>
}
return (
<SettingsView
onSave={saveSettings}
statusMessage={status}
initialValues={settings}
>
<SimpleGrid verticalSpacing="md">
<TextInput
name="apiKey"
label="API Key"
placeholder="Enter your API key"
type="password"
/>
<Select
name="language"
label="Language"
placeholder="Select language"
data={[
{ value: 'en', label: 'English' },
{ value: 'es', label: 'Spanish' },
{ value: 'fr', label: 'French' }
]}
/>
<Switch
name="notifications"
label="Enable notifications"
/>
</SimpleGrid>
</SettingsView>
)
}
export default AppSettings
Data Visualization
Display analytics with charts.
// src/views/AnalyticsDashboard.jsx
import React, { useState, useEffect } from 'react'
import {
Container,
SimpleGrid,
Text,
Select,
BarChart,
LineChart,
Loader
} from '@teachfloor/extension-kit'
const AnalyticsDashboard = () => {
const [data, setData] = useState(null)
const [timeframe, setTimeframe] = useState('week')
const [loading, setLoading] = useState(true)
useEffect(() => {
loadData(timeframe)
}, [timeframe])
async function loadData(period) {
setLoading(true)
// Simulated data - replace with actual API calls
const mockData = {
week: [
{ name: 'Mon', completions: 4 },
{ name: 'Tue', completions: 3 },
{ name: 'Wed', completions: 5 },
{ name: 'Thu', completions: 2 },
{ name: 'Fri', completions: 6 }
],
month: [
{ name: 'Week 1', completions: 15 },
{ name: 'Week 2', completions: 22 },
{ name: 'Week 3', completions: 18 },
{ name: 'Week 4', completions: 25 }
]
}
setData(mockData[period])
setLoading(false)
}
if (loading) {
return <Loader />
}
return (
<Container p="md">
<SimpleGrid verticalSpacing="lg">
<div>
<Text size="xl" fw={700}>Analytics</Text>
<Select
value={timeframe}
onChange={setTimeframe}
data={[
{ value: 'week', label: 'This Week' },
{ value: 'month', label: 'This Month' }
]}
style={{ maxWidth: 200, marginTop: 12 }}
/>
</div>
<div>
<Text fw={600} mb="md">Completions Over Time</Text>
<BarChart
data={data}
dataKey="completions"
height={300}
/>
</div>
<div>
<Text fw={600} mb="md">Trend</Text>
<LineChart
data={data}
dataKey="completions"
height={300}
/>
</div>
</SimpleGrid>
</Container>
)
}
export default AnalyticsDashboard
External API Integration
Integrate with external service.
// src/utils/api.js
class ExternalAPI {
constructor(apiKey) {
this.apiKey = apiKey
this.baseURL = 'https://api.example.com'
}
async request(endpoint, options = {}) {
const response = await fetch(`${this.baseURL}${endpoint}`, {
...options,
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
...options.headers
}
})
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`)
}
return response.json()
}
async getData() {
return this.request('/data')
}
async postData(data) {
return this.request('/data', {
method: 'POST',
body: JSON.stringify(data)
})
}
}
export default ExternalAPI
// src/views/IntegrationView.jsx
import React, { useState, useEffect } from 'react'
import {
Container,
SimpleGrid,
Button,
Text,
Loader,
showToast
} from '@teachfloor/extension-kit'
import ExternalAPI from '../utils/api'
const IntegrationView = () => {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const [api, setApi] = useState(null)
useEffect(() => {
initializeAPI()
}, [])
async function initializeAPI() {
try {
const saved = localStorage.getItem('teachfloor-settings')
const settings = saved ? JSON.parse(saved) : null
if (settings?.apiKey) {
setApi(new ExternalAPI(settings.apiKey))
}
} catch (error) {
showToast('Failed to load settings', { type: 'error' })
}
}
async function loadData() {
if (!api) {
showToast('Please configure API key in settings', { type: 'warning' })
return
}
setLoading(true)
try {
const result = await api.getData()
setData(result)
showToast('Data loaded successfully', { type: 'success' })
} catch (error) {
showToast('Failed to load data', { type: 'error' })
} finally {
setLoading(false)
}
}
if (!api) {
return (
<Container p="md">
<Text>Please configure your API key in settings</Text>
</Container>
)
}
return (
<Container p="md">
<SimpleGrid verticalSpacing="md">
<Text size="xl" fw={700}>External Data</Text>
<Button onClick={loadData} loading={loading}>
Load Data
</Button>
{data && (
<div>
<Text>Data loaded!</Text>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)}
</SimpleGrid>
</Container>
)
}
export default IntegrationView
Next Steps
See troubleshooting guide for common issues:
→ Continue to Troubleshooting