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.
Code{ "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": "courses:read", "purpose": "Include course context in event tracking" } ] }
No ui_extension field - this app runs entirely in the background.
Code// 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.eventto 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-surface app that greets the current user. Demonstrates the widget manifest shape, <WidgetView> auto-height, and how a widget reads platform context. See Surfaces for the concepts.
Manifest
Note the three parts specific to the widget surface: surface: "widget", a widget-hosting viewport, and the nested widget: { id, name, description } block. The id is what identifies this widget internally + on the composite key stored in dashboard rows; name + description appear in the admin's widget picker.
Code{ "id": "simple-widget", "version": "1.0.0", "name": "Simple Widget", "description": "Greet the current user from a dashboard widget slot.", "distribution_type": "private", "ui_extension": { "views": [ { "surface": "widget", "viewport": "teachfloor.dashboard.dashboard.detail", "component": "GreetingWidget", "widget": { "id": "greeting", "name": "Greeting", "description": "Says hello to the current user." } } ] } }
Component
Wrap the widget content in <WidgetView> so the dashboard slot auto-fits the widget's natural height — no aspect ratio needed. The rest is standard Extension Kit + useExtensionContext().
Code// src/views/GreetingWidget.jsx import React from 'react' import { WidgetView, Text, Avatar, Group, useExtensionContext } from '@teachfloor/extension-kit' const GreetingWidget = () => { const { userContext } = useExtensionContext() return ( <WidgetView 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> </WidgetView> ) } export default GreetingWidget
Scaffold it from the CLI
Codeteachfloor apps add widget # Prompts: viewport → id → name → description → component name
See teachfloor apps add widget for the full flag list.
Note-Taking App
A complete note-taking application using browser storage.
Code{ "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" } ] }
Code// 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', { color: 'red' }) } 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!', { color: 'green' }) } catch (error) { showToast('Failed to save note', { color: 'red' }) } } 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!', { color: 'green' }) } catch (error) { showToast('Failed to update note', { color: 'red' }) } } async function deleteNote(id) { try { const updated = notes.filter(note => note.id !== id) await store('notes', updated, 'userdata') setNotes(updated) showToast('Note deleted!', { color: 'green' }) } catch (error) { showToast('Failed to delete note', { color: 'red' }) } } return { notes, loading, addNote, updateNote, deleteNote } }
Code// 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
Code// 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
Code// 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.
Code{ "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": "courses:read", "purpose": "Display course information" } ] }
Code// 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.
Code// 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!', { color: 'green' }) } catch (error) { setStatus('Failed to save settings') showToast('Failed to save', { color: 'red' }) } } 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.
Code// 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.
Code// 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
Code// 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', { color: 'red' }) } } async function loadData() { if (!api) { showToast('Please configure API key in settings', { color: 'orange' }) return } setLoading(true) try { const result = await api.getData() setData(result) showToast('Data loaded successfully', { color: 'green' }) } catch (error) { showToast('Failed to load data', { color: 'red' }) } 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