Code Snippets
Copy-paste code snippets for common patterns in NaaP plugin development.
Authentication Check#
Guard a component or action behind authentication:
TypeScript
| 1 | import { useAuth } from '@naap/plugin-sdk'; |
| 2 | |
| 3 | function ProtectedAction() { |
| 4 | const auth = useAuth(); |
| 5 | |
| 6 | if (!auth.isAuthenticated()) { |
| 7 | return ( |
| 8 | <div className="text-center p-8 text-muted-foreground"> |
| 9 | Please log in to access this feature. |
| 10 | </div> |
| 11 | ); |
| 12 | } |
| 13 | |
| 14 | return <div>Welcome, {auth.getUser()?.displayName}</div>; |
| 15 | } |
Role-Based UI#
Show or hide UI elements based on user roles:
TypeScript
| 1 | import { useAuth } from '@naap/plugin-sdk'; |
| 2 | |
| 3 | function AdminPanel() { |
| 4 | const auth = useAuth(); |
| 5 | const isAdmin = auth.hasRole('system:admin'); |
| 6 | const canEdit = auth.hasPermission('items', 'write'); |
| 7 | |
| 8 | return ( |
| 9 | <div> |
| 10 | <h2>Dashboard</h2> |
| 11 | {canEdit && <button>Edit Item</button>} |
| 12 | {isAdmin && ( |
| 13 | <div className="mt-4 p-4 border rounded-lg"> |
| 14 | <h3>Admin Controls</h3> |
| 15 | <button>Manage Users</button> |
| 16 | <button>System Settings</button> |
| 17 | </div> |
| 18 | )} |
| 19 | </div> |
| 20 | ); |
| 21 | } |
Fetch Data with Loading State#
TypeScript
| 1 | import { useState, useEffect } from 'react'; |
| 2 | import { usePluginApi, useNotify } from '@naap/plugin-sdk'; |
| 3 | |
| 4 | function DataList() { |
| 5 | const api = usePluginApi(); |
| 6 | const notify = useNotify(); |
| 7 | const [items, setItems] = useState([]); |
| 8 | const [loading, setLoading] = useState(true); |
| 9 | const [error, setError] = useState(null); |
| 10 | |
| 11 | useEffect(() => { |
| 12 | async function load() { |
| 13 | try { |
| 14 | const data = await api.get('/items'); |
| 15 | setItems(data.items); |
| 16 | } catch (err) { |
| 17 | setError(err.message); |
| 18 | notify.error('Failed to load items'); |
| 19 | } finally { |
| 20 | setLoading(false); |
| 21 | } |
| 22 | } |
| 23 | load(); |
| 24 | }, []); |
| 25 | |
| 26 | if (loading) return <div className="animate-pulse">Loading...</div>; |
| 27 | if (error) return <div className="text-red-500">{error}</div>; |
| 28 | |
| 29 | return ( |
| 30 | <ul> |
| 31 | {items.map(item => ( |
| 32 | <li key={item.id}>{item.name}</li> |
| 33 | ))} |
| 34 | </ul> |
| 35 | ); |
| 36 | } |
Form with API Submission#
TypeScript
| 1 | import { useState } from 'react'; |
| 2 | import { usePluginApi, useNotify } from '@naap/plugin-sdk'; |
| 3 | |
| 4 | function CreateItemForm() { |
| 5 | const api = usePluginApi(); |
| 6 | const notify = useNotify(); |
| 7 | const [name, setName] = useState(''); |
| 8 | const [submitting, setSubmitting] = useState(false); |
| 9 | |
| 10 | const handleSubmit = async (e: React.FormEvent) => { |
| 11 | e.preventDefault(); |
| 12 | if (!name.trim()) return; |
| 13 | |
| 14 | setSubmitting(true); |
| 15 | try { |
| 16 | await api.post('/items', { name }); |
| 17 | notify.success('Item created!'); |
| 18 | setName(''); |
| 19 | } catch (err) { |
| 20 | notify.error('Failed to create item'); |
| 21 | } finally { |
| 22 | setSubmitting(false); |
| 23 | } |
| 24 | }; |
| 25 | |
| 26 | return ( |
| 27 | <form onSubmit={handleSubmit} className="flex gap-2"> |
| 28 | <input |
| 29 | value={name} |
| 30 | onChange={(e) => setName(e.target.value)} |
| 31 | placeholder="Item name..." |
| 32 | className="flex-1 px-3 py-2 border rounded-lg" |
| 33 | disabled={submitting} |
| 34 | /> |
| 35 | <button |
| 36 | type="submit" |
| 37 | disabled={submitting} |
| 38 | className="px-4 py-2 bg-primary text-white rounded-lg disabled:opacity-50" |
| 39 | > |
| 40 | {submitting ? 'Creating...' : 'Create'} |
| 41 | </button> |
| 42 | </form> |
| 43 | ); |
| 44 | } |
Theme-Aware Component#
TypeScript
| 1 | import { useThemeService } from '@naap/plugin-sdk'; |
| 2 | |
| 3 | function StatusCard({ title, value, trend }) { |
| 4 | const theme = useThemeService(); |
| 5 | |
| 6 | return ( |
| 7 | <div className="p-4 rounded-xl border border-border bg-card"> |
| 8 | <p className="text-sm text-muted-foreground">{title}</p> |
| 9 | <p className="text-2xl font-bold mt-1">{value}</p> |
| 10 | <p className={`text-sm mt-1 ${ |
| 11 | trend > 0 ? 'text-emerald-500' : 'text-red-500' |
| 12 | }`}> |
| 13 | {trend > 0 ? '+' : ''}{trend}% |
| 14 | </p> |
| 15 | </div> |
| 16 | ); |
| 17 | } |
Event-Driven Refresh#
Listen for events from other plugins to refresh data:
TypeScript
| 1 | import { useEffect, useState } from 'react'; |
| 2 | import { useEvents, usePluginApi } from '@naap/plugin-sdk'; |
| 3 | |
| 4 | function LiveList() { |
| 5 | const events = useEvents(); |
| 6 | const api = usePluginApi(); |
| 7 | const [items, setItems] = useState([]); |
| 8 | |
| 9 | const refresh = async () => { |
| 10 | const data = await api.get('/items'); |
| 11 | setItems(data.items); |
| 12 | }; |
| 13 | |
| 14 | useEffect(() => { |
| 15 | refresh(); |
| 16 | |
| 17 | // Refresh when related events fire |
| 18 | const unsub1 = events.on('my-plugin:item-created', refresh); |
| 19 | const unsub2 = events.on('my-plugin:item-updated', refresh); |
| 20 | const unsub3 = events.on('team:change', refresh); |
| 21 | |
| 22 | return () => { |
| 23 | unsub1(); |
| 24 | unsub2(); |
| 25 | unsub3(); |
| 26 | }; |
| 27 | }, []); |
| 28 | |
| 29 | return ( |
| 30 | <ul> |
| 31 | {items.map(item => ( |
| 32 | <li key={item.id}>{item.name}</li> |
| 33 | ))} |
| 34 | </ul> |
| 35 | ); |
| 36 | } |
Confirmation Dialog#
TypeScript
| 1 | import { useState } from 'react'; |
| 2 | import { usePluginApi, useNotify } from '@naap/plugin-sdk'; |
| 3 | |
| 4 | function DeleteButton({ itemId, onDeleted }) { |
| 5 | const api = usePluginApi(); |
| 6 | const notify = useNotify(); |
| 7 | const [confirming, setConfirming] = useState(false); |
| 8 | |
| 9 | const handleDelete = async () => { |
| 10 | try { |
| 11 | await api.delete(`/items/${itemId}`); |
| 12 | notify.success('Item deleted'); |
| 13 | onDeleted(); |
| 14 | } catch { |
| 15 | notify.error('Failed to delete item'); |
| 16 | } |
| 17 | setConfirming(false); |
| 18 | }; |
| 19 | |
| 20 | if (confirming) { |
| 21 | return ( |
| 22 | <div className="flex gap-2"> |
| 23 | <button |
| 24 | onClick={handleDelete} |
| 25 | className="px-3 py-1 text-sm bg-red-500 text-white rounded" |
| 26 | > |
| 27 | Confirm |
| 28 | </button> |
| 29 | <button |
| 30 | onClick={() => setConfirming(false)} |
| 31 | className="px-3 py-1 text-sm border rounded" |
| 32 | > |
| 33 | Cancel |
| 34 | </button> |
| 35 | </div> |
| 36 | ); |
| 37 | } |
| 38 | |
| 39 | return ( |
| 40 | <button |
| 41 | onClick={() => setConfirming(true)} |
| 42 | className="px-3 py-1 text-sm text-red-500 hover:bg-red-500/10 rounded" |
| 43 | > |
| 44 | Delete |
| 45 | </button> |
| 46 | ); |
| 47 | } |
Plugin Mount Function#
The standard mount function pattern:
TypeScript
| 1 | import React from 'react'; |
| 2 | import { createRoot } from 'react-dom/client'; |
| 3 | import { ShellProvider } from '@naap/plugin-sdk'; |
| 4 | import type { ShellContext } from '@naap/plugin-sdk'; |
| 5 | import App from './App'; |
| 6 | |
| 7 | let root: ReturnType<typeof createRoot> | null = null; |
| 8 | |
| 9 | export function mount(container: HTMLElement, context: ShellContext) { |
| 10 | root = createRoot(container); |
| 11 | root.render( |
| 12 | <ShellProvider value={context}> |
| 13 | <App /> |
| 14 | </ShellProvider> |
| 15 | ); |
| 16 | |
| 17 | return () => { |
| 18 | if (root) { |
| 19 | root.unmount(); |
| 20 | root = null; |
| 21 | } |
| 22 | }; |
| 23 | } |