References
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": "course_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 that displays user information.
Manifest
Code{ "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" } ] } }
Code// 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.
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": "course_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
Additional Resources
Last modified on