diff --git a/servers/airtable/src/apps/automation-monitor/App.tsx b/servers/airtable/src/apps/automation-monitor/App.tsx new file mode 100644 index 0000000..4086345 --- /dev/null +++ b/servers/airtable/src/apps/automation-monitor/App.tsx @@ -0,0 +1 @@ +import React,{useState,useMemo,useTransition,useCallback}from'react';const useDebounce=(value:T,delay:number=300):T=>{const[debouncedValue,setDebouncedValue]=useState(value);React.useEffect(()=>{const handler=setTimeout(()=>setDebouncedValue(value),delay);return()=>clearTimeout(handler)},[value,delay]);return debouncedValue};const useToast=()=>{const[toasts,setToasts]=useState>([]);const showToast=useCallback((message:string,type:string='info')=>{const id=Date.now();setToasts(prev=>[...prev,{id,message,type}]);setTimeout(()=>setToasts(prev=>prev.filter(t=>t.id!==id)),3000)},[]);return{toasts,showToast}};interface AutomationRun{id:string;name:string;status:string;trigger:string;actions:number;runTime:string;duration:number}const mockRuns:AutomationRun[]=[{id:'run1',name:'Send Welcome Email',status:'success',trigger:'Record created',actions:3,runTime:'2024-02-13 10:30',duration:2.5},{id:'run2',name:'Update Slack Channel',status:'success',trigger:'Field updated',actions:2,runTime:'2024-02-13 09:15',duration:1.2},{id:'run3',name:'Create Task in Asana',status:'failed',trigger:'Record created',actions:4,runTime:'2024-02-12 16:45',duration:0.8},{id:'run4',name:'Sync to Google Sheets',status:'success',trigger:'View updated',actions:1,runTime:'2024-02-12 14:20',duration:3.1}];const App:React.FC=()=>{const[searchQuery,setSearchQuery]=useState('');const[selectedRun,setSelectedRun]=useState(null);const[isPending,startTransition]=useTransition();const{toasts,showToast}=useToast();const debouncedSearch=useDebounce(searchQuery,300);const filteredRuns=useMemo(()=>{return mockRuns.filter(run=>run.name.toLowerCase().includes(debouncedSearch.toLowerCase())||run.trigger.toLowerCase().includes(debouncedSearch.toLowerCase()))},[debouncedSearch]);const stats=useMemo(()=>({total:mockRuns.length,success:mockRuns.filter(r=>r.status==='success').length,failed:mockRuns.filter(r=>r.status==='failed').length}),[]);const handleRunClick=(run:AutomationRun)=>{startTransition(()=>{setSelectedRun(run);showToast(`Viewing: ${run.name}`,'info')})};return(

Automation Monitor

View automation runs

{stats.total}
Total Runs
{stats.success}
Successful
{stats.failed}
Failed
setSearchQuery(e.target.value)} className="search-input"/>
{filteredRuns.length===0?(
⚙️

No runs found

Try adjusting your search query

):(
{filteredRuns.map(run=>(
handleRunClick(run)}>

{run.name}

{run.status}
Trigger:{run.trigger}
Actions:{run.actions}
Duration:{run.duration}s
Run Time:{run.runTime}
))}
)}{selectedRun&&(

{selectedRun.name}

Status: {selectedRun.status}

Trigger: {selectedRun.trigger}

Actions: {selectedRun.actions}

Duration: {selectedRun.duration}s

Run Time: {selectedRun.runTime}

)}
{toasts.map(toast=>(
{toast.message}
))}
)};export default App; \ No newline at end of file diff --git a/servers/airtable/src/apps/automation-monitor/index.html b/servers/airtable/src/apps/automation-monitor/index.html new file mode 100644 index 0000000..3fc8fff --- /dev/null +++ b/servers/airtable/src/apps/automation-monitor/index.html @@ -0,0 +1,12 @@ + + + + + + Airtable Automation Monitor + + +
+ + + diff --git a/servers/airtable/src/apps/automation-monitor/main.tsx b/servers/airtable/src/apps/automation-monitor/main.tsx new file mode 100644 index 0000000..5b258cd --- /dev/null +++ b/servers/airtable/src/apps/automation-monitor/main.tsx @@ -0,0 +1 @@ +import React,{lazy,Suspense}from'react';import{createRoot}from'react-dom/client';import'./styles.css';const App=lazy(()=>import('./App'));class ErrorBoundary extends React.Component<{children:React.ReactNode},{hasError:boolean;error?:Error}>{constructor(props:{children:React.ReactNode}){super(props);this.state={hasError:false}}static getDerivedStateFromError(error:Error){return{hasError:true,error}}componentDidCatch(error:Error,errorInfo:React.ErrorInfo){console.error('ErrorBoundary caught:',error,errorInfo)}render(){if(this.state.hasError){return(

Something went wrong

{this.state.error?.message}

)}return this.props.children}}const LoadingSkeleton=()=>(
);const root=createRoot(document.getElementById('root')!);root.render(}>); \ No newline at end of file diff --git a/servers/airtable/src/apps/automation-monitor/styles.css b/servers/airtable/src/apps/automation-monitor/styles.css new file mode 100644 index 0000000..c3a77cd --- /dev/null +++ b/servers/airtable/src/apps/automation-monitor/styles.css @@ -0,0 +1 @@ +:root{--bg-primary:#0f172a;--bg-secondary:#1e293b;--bg-tertiary:#334155;--text-primary:#f1f5f9;--text-secondary:#cbd5e1;--text-muted:#94a3b8;--accent:#3b82f6;--accent-hover:#2563eb;--success:#10b981;--warning:#f59e0b;--error:#ef4444;--border:#334155;--shadow:rgba(0,0,0,0.3)}*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,sans-serif;background:var(--bg-primary);color:var(--text-primary);line-height:1.6}#root{min-height:100vh}.app-container{max-width:1200px;margin:0 auto;padding:2rem}.app-header{margin-bottom:2rem}.app-header h1{font-size:2rem;font-weight:700;margin-bottom:0.5rem}.subtitle{color:var(--text-secondary);font-size:1rem}.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem;margin-bottom:2rem}.stat-card{background:var(--bg-secondary);padding:1.5rem;border-radius:0.75rem;border:1px solid var(--border);transition:transform 0.2s,box-shadow 0.2s}.stat-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px var(--shadow)}.stat-value{font-size:2.5rem;font-weight:700;color:var(--accent)}.stat-label{color:var(--text-secondary);font-size:0.875rem;margin-top:0.5rem}.search-container{margin-bottom:2rem}.search-input{width:100%;padding:0.875rem 1rem;background:var(--bg-secondary);border:1px solid var(--border);border-radius:0.5rem;color:var(--text-primary);font-size:1rem;transition:border-color 0.2s}.search-input:focus{outline:none;border-color:var(--accent)}.runs-list{display:flex;flex-direction:column;gap:1rem;margin-bottom:2rem}.run-card{background:var(--bg-secondary);border:1px solid var(--border);border-radius:0.75rem;padding:1.5rem;cursor:pointer;transition:transform 0.2s,box-shadow 0.2s,border-color 0.2s}.run-card:hover{transform:translateY(-2px);box-shadow:0 8px 16px var(--shadow);border-color:var(--accent)}.run-header{display:flex;justify-content:space-between;align-items:start;margin-bottom:1rem}.run-header h3{font-size:1.125rem;font-weight:600;color:var(--text-primary)}.badge{padding:0.25rem 0.75rem;border-radius:1rem;font-size:0.75rem;font-weight:600;text-transform:uppercase}.badge-success{background:rgba(16,185,129,0.2);color:var(--success)}.badge-failed{background:rgba(239,68,68,0.2);color:var(--error)}.run-body{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:0.75rem}.run-stat{display:flex;flex-direction:column;gap:0.25rem;font-size:0.875rem}.run-stat .label{color:var(--text-secondary)}.run-stat .value{color:var(--text-primary);font-weight:500}.detail-panel{background:var(--bg-secondary);border:1px solid var(--border);border-radius:0.75rem;padding:1.5rem;margin-top:2rem}.detail-panel h2{font-size:1.5rem;margin-bottom:1rem}.empty-state{text-align:center;padding:4rem 2rem}.empty-icon{font-size:4rem;margin-bottom:1rem}.empty-state h3{font-size:1.5rem;margin-bottom:0.5rem}.empty-state p{color:var(--text-secondary)}.toast-container{position:fixed;top:1rem;right:1rem;z-index:1000;display:flex;flex-direction:column;gap:0.5rem}.toast{background:var(--bg-secondary);border:1px solid var(--border);border-radius:0.5rem;padding:1rem 1.5rem;min-width:250px;box-shadow:0 4px 12px var(--shadow);animation:slideIn 0.3s ease-out}.toast-success{border-left:4px solid var(--success)}.toast-error{border-left:4px solid var(--error)}.toast-info{border-left:4px solid var(--accent)}@keyframes slideIn{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}.shimmer{background:linear-gradient(90deg,var(--bg-secondary) 0%,var(--bg-tertiary) 50%,var(--bg-secondary) 100%);background-size:200% 100%;animation:shimmer 1.5s infinite}@keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}.loading-skeleton{padding:2rem}.skeleton-header{height:3rem;border-radius:0.5rem;margin-bottom:2rem}.skeleton-content{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.skeleton-card{height:150px;border-radius:0.75rem}.error-boundary{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;padding:2rem;text-align:center}.error-boundary h1{font-size:2rem;margin-bottom:1rem;color:var(--error)}.error-boundary button{margin-top:1rem;padding:0.75rem 1.5rem;background:var(--accent);color:white;border:none;border-radius:0.5rem;cursor:pointer;font-size:1rem;transition:background 0.2s}.error-boundary button:hover{background:var(--accent-hover)}@media (max-width:768px){.app-container{padding:1rem}.stats-grid{grid-template-columns:1fr}.app-header h1{font-size:1.5rem}.run-body{grid-template-columns:1fr}} \ No newline at end of file diff --git a/servers/airtable/src/apps/base-browser/App.tsx b/servers/airtable/src/apps/base-browser/App.tsx new file mode 100644 index 0000000..77fb44e --- /dev/null +++ b/servers/airtable/src/apps/base-browser/App.tsx @@ -0,0 +1,156 @@ +import React, { useState, useMemo, useTransition, useCallback } from 'react'; + +const useDebounce = (value: T, delay: number = 300): T => { + const [debouncedValue, setDebouncedValue] = useState(value); + + React.useEffect(() => { + const handler = setTimeout(() => setDebouncedValue(value), delay); + return () => clearTimeout(handler); + }, [value, delay]); + + return debouncedValue; +}; + +const useToast = () => { + const [toasts, setToasts] = useState>([]); + + const showToast = useCallback((message: string, type: string = 'info') => { + const id = Date.now(); + setToasts(prev => [...prev, { id, message, type }]); + setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 3000); + }, []); + + return { toasts, showToast }; +}; + +interface Base { + id: string; + name: string; + permissionLevel: string; + tables: number; + lastModified: string; +} + +const mockBases: Base[] = [ + { id: 'app1', name: 'Marketing Hub', permissionLevel: 'owner', tables: 12, lastModified: '2024-02-10' }, + { id: 'app2', name: 'Sales CRM', permissionLevel: 'editor', tables: 8, lastModified: '2024-02-12' }, + { id: 'app3', name: 'Product Roadmap', permissionLevel: 'owner', tables: 5, lastModified: '2024-02-13' }, + { id: 'app4', name: 'HR Operations', permissionLevel: 'viewer', tables: 15, lastModified: '2024-02-11' }, + { id: 'app5', name: 'Content Calendar', permissionLevel: 'editor', tables: 6, lastModified: '2024-02-09' }, +]; + +const App: React.FC = () => { + const [searchQuery, setSearchQuery] = useState(''); + const [selectedBase, setSelectedBase] = useState(null); + const [isPending, startTransition] = useTransition(); + const { toasts, showToast } = useToast(); + + const debouncedSearch = useDebounce(searchQuery, 300); + + const filteredBases = useMemo(() => { + return mockBases.filter(base => + base.name.toLowerCase().includes(debouncedSearch.toLowerCase()) + ); + }, [debouncedSearch]); + + const stats = useMemo(() => ({ + total: mockBases.length, + owned: mockBases.filter(b => b.permissionLevel === 'owner').length, + shared: mockBases.filter(b => b.permissionLevel !== 'owner').length, + }), []); + + const handleBaseClick = (base: Base) => { + startTransition(() => { + setSelectedBase(base); + showToast(`Opened base: ${base.name}`, 'success'); + }); + }; + + return ( +
+
+

Airtable Base Browser

+

Browse and manage your Airtable bases

+
+ +
+
+
{stats.total}
+
Total Bases
+
+
+
{stats.owned}
+
Owned
+
+
+
{stats.shared}
+
Shared
+
+
+ +
+ setSearchQuery(e.target.value)} + className="search-input" + /> +
+ + {filteredBases.length === 0 ? ( +
+
📦
+

No bases found

+

Try adjusting your search query

+
+ ) : ( +
+ {filteredBases.map(base => ( +
handleBaseClick(base)} + > +
+

{base.name}

+ + {base.permissionLevel} + +
+
+
+ Tables: + {base.tables} +
+
+ Last Modified: + {base.lastModified} +
+
+
+ ))} +
+ )} + + {selectedBase && ( +
+

Base Details: {selectedBase.name}

+

ID: {selectedBase.id}

+

Permission: {selectedBase.permissionLevel}

+

Tables: {selectedBase.tables}

+
+ )} + +
+ {toasts.map(toast => ( +
+ {toast.message} +
+ ))} +
+
+ ); +}; + +export default App; diff --git a/servers/airtable/src/apps/base-browser/index.html b/servers/airtable/src/apps/base-browser/index.html new file mode 100644 index 0000000..2b51e4d --- /dev/null +++ b/servers/airtable/src/apps/base-browser/index.html @@ -0,0 +1,12 @@ + + + + + + Airtable Base Browser + + +
+ + + diff --git a/servers/airtable/src/apps/base-browser/main.tsx b/servers/airtable/src/apps/base-browser/main.tsx new file mode 100644 index 0000000..22b087f --- /dev/null +++ b/servers/airtable/src/apps/base-browser/main.tsx @@ -0,0 +1,58 @@ +import React, { lazy, Suspense } from 'react'; +import { createRoot } from 'react-dom/client'; +import './styles.css'; + +const App = lazy(() => import('./App')); + +class ErrorBoundary extends React.Component< + { children: React.ReactNode }, + { hasError: boolean; error?: Error } +> { + constructor(props: { children: React.ReactNode }) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error) { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('ErrorBoundary caught:', error, errorInfo); + } + + render() { + if (this.state.hasError) { + return ( +
+

Something went wrong

+

{this.state.error?.message}

+ +
+ ); + } + return this.props.children; + } +} + +const LoadingSkeleton = () => ( +
+
+
+
+
+
+
+
+); + +const root = createRoot(document.getElementById('root')!); +root.render( + + + }> + + + + +); diff --git a/servers/airtable/src/apps/base-browser/styles.css b/servers/airtable/src/apps/base-browser/styles.css new file mode 100644 index 0000000..6282190 --- /dev/null +++ b/servers/airtable/src/apps/base-browser/styles.css @@ -0,0 +1,350 @@ +:root { + --bg-primary: #0f172a; + --bg-secondary: #1e293b; + --bg-tertiary: #334155; + --text-primary: #f1f5f9; + --text-secondary: #cbd5e1; + --text-muted: #94a3b8; + --accent: #3b82f6; + --accent-hover: #2563eb; + --success: #10b981; + --warning: #f59e0b; + --error: #ef4444; + --border: #334155; + --shadow: rgba(0, 0, 0, 0.3); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + line-height: 1.6; +} + +#root { + min-height: 100vh; +} + +.app-container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.app-header { + margin-bottom: 2rem; +} + +.app-header h1 { + font-size: 2rem; + font-weight: 700; + margin-bottom: 0.5rem; +} + +.subtitle { + color: var(--text-secondary); + font-size: 1rem; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-bottom: 2rem; +} + +.stat-card { + background: var(--bg-secondary); + padding: 1.5rem; + border-radius: 0.75rem; + border: 1px solid var(--border); + transition: transform 0.2s, box-shadow 0.2s; +} + +.stat-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px var(--shadow); +} + +.stat-value { + font-size: 2.5rem; + font-weight: 700; + color: var(--accent); +} + +.stat-label { + color: var(--text-secondary); + font-size: 0.875rem; + margin-top: 0.5rem; +} + +.search-container { + margin-bottom: 2rem; +} + +.search-input { + width: 100%; + padding: 0.875rem 1rem; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 0.5rem; + color: var(--text-primary); + font-size: 1rem; + transition: border-color 0.2s; +} + +.search-input:focus { + outline: none; + border-color: var(--accent); +} + +.data-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 1rem; + margin-bottom: 2rem; +} + +.data-card { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 0.75rem; + padding: 1.5rem; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s; +} + +.data-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 16px var(--shadow); + border-color: var(--accent); +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: start; + margin-bottom: 1rem; +} + +.card-header h3 { + font-size: 1.125rem; + font-weight: 600; + color: var(--text-primary); +} + +.badge { + padding: 0.25rem 0.75rem; + border-radius: 1rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.badge-owner { + background: rgba(16, 185, 129, 0.2); + color: var(--success); +} + +.badge-editor { + background: rgba(59, 130, 246, 0.2); + color: var(--accent); +} + +.badge-viewer { + background: rgba(148, 163, 184, 0.2); + color: var(--text-muted); +} + +.card-body { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.card-stat { + display: flex; + justify-content: space-between; + font-size: 0.875rem; +} + +.card-stat .label { + color: var(--text-secondary); +} + +.card-stat .value { + color: var(--text-primary); + font-weight: 500; +} + +.detail-panel { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 0.75rem; + padding: 1.5rem; + margin-top: 2rem; +} + +.detail-panel h2 { + font-size: 1.5rem; + margin-bottom: 1rem; +} + +.empty-state { + text-align: center; + padding: 4rem 2rem; +} + +.empty-icon { + font-size: 4rem; + margin-bottom: 1rem; +} + +.empty-state h3 { + font-size: 1.5rem; + margin-bottom: 0.5rem; +} + +.empty-state p { + color: var(--text-secondary); +} + +.toast-container { + position: fixed; + top: 1rem; + right: 1rem; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.toast { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 0.5rem; + padding: 1rem 1.5rem; + min-width: 250px; + box-shadow: 0 4px 12px var(--shadow); + animation: slideIn 0.3s ease-out; +} + +.toast-success { + border-left: 4px solid var(--success); +} + +.toast-error { + border-left: 4px solid var(--error); +} + +.toast-info { + border-left: 4px solid var(--accent); +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.shimmer { + background: linear-gradient( + 90deg, + var(--bg-secondary) 0%, + var(--bg-tertiary) 50%, + var(--bg-secondary) 100% + ); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; +} + +@keyframes shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +.loading-skeleton { + padding: 2rem; +} + +.skeleton-header { + height: 3rem; + border-radius: 0.5rem; + margin-bottom: 2rem; +} + +.skeleton-content { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 1rem; +} + +.skeleton-card { + height: 150px; + border-radius: 0.75rem; +} + +.error-boundary { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + padding: 2rem; + text-align: center; +} + +.error-boundary h1 { + font-size: 2rem; + margin-bottom: 1rem; + color: var(--error); +} + +.error-boundary button { + margin-top: 1rem; + padding: 0.75rem 1.5rem; + background: var(--accent); + color: white; + border: none; + border-radius: 0.5rem; + cursor: pointer; + font-size: 1rem; + transition: background 0.2s; +} + +.error-boundary button:hover { + background: var(--accent-hover); +} + +@media (max-width: 768px) { + .app-container { + padding: 1rem; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .data-grid { + grid-template-columns: 1fr; + } + + .app-header h1 { + font-size: 1.5rem; + } +} diff --git a/servers/airtable/src/apps/dashboard/App.tsx b/servers/airtable/src/apps/dashboard/App.tsx new file mode 100644 index 0000000..351ebda --- /dev/null +++ b/servers/airtable/src/apps/dashboard/App.tsx @@ -0,0 +1 @@ +import React,{useState,useMemo,useTransition,useCallback}from'react';const useDebounce=(value:T,delay:number=300):T=>{const[debouncedValue,setDebouncedValue]=useState(value);React.useEffect(()=>{const handler=setTimeout(()=>setDebouncedValue(value),delay);return()=>clearTimeout(handler)},[value,delay]);return debouncedValue};const useToast=()=>{const[toasts,setToasts]=useState>([]);const showToast=useCallback((message:string,type:string='info')=>{const id=Date.now();setToasts(prev=>[...prev,{id,message,type}]);setTimeout(()=>setToasts(prev=>prev.filter(t=>t.id!==id)),3000)},[]);return{toasts,showToast}};interface BaseStats{id:string;name:string;records:number;tables:number;lastActivity:string}const mockBases:BaseStats[]=[{id:'base1',name:'Sales CRM',records:2450,tables:8,lastActivity:'2 hours ago'},{id:'base2',name:'Marketing Hub',records:1850,tables:12,lastActivity:'30 minutes ago'},{id:'base3',name:'Product Roadmap',records:680,tables:5,lastActivity:'1 day ago'},{id:'base4',name:'HR Operations',records:1250,tables:10,lastActivity:'3 hours ago'}];const App:React.FC=()=>{const[bases,setBases]=useState(mockBases);const[selectedBase,setSelectedBase]=useState(null);const[isPending,startTransition]=useTransition();const{toasts,showToast}=useToast();const stats=useMemo(()=>({totalBases:bases.length,totalRecords:bases.reduce((sum,b)=>sum+b.records,0),totalTables:bases.reduce((sum,b)=>sum+b.tables,0),avgRecords:Math.floor(bases.reduce((sum,b)=>sum+b.records,0)/bases.length)}),[bases]);const handleBaseClick=(base:BaseStats)=>{startTransition(()=>{setSelectedBase(base);showToast(`Viewing: ${base.name}`,'info')})};return(

Airtable Dashboard

Cross-base overview, record counts, activity

{stats.totalBases}
Total Bases
{stats.totalRecords.toLocaleString()}
Total Records
{stats.totalTables}
Total Tables
{stats.avgRecords}
Avg Records/Base

Recent Activity

{bases.map(base=>(
handleBaseClick(base)}>

{base.name}

{base.lastActivity}
Records:{base.records.toLocaleString()}
Tables:{base.tables}
))}

Quick Stats

Most Active:Marketing Hub
Largest:Sales CRM
Recent Update:30 min ago

System Health

API Status: Operational
Sync Status: Up to date
Storage: 45% used
{selectedBase&&(

{selectedBase.name}

Records: {selectedBase.records.toLocaleString()}

Tables: {selectedBase.tables}

Last Activity: {selectedBase.lastActivity}

)}
{toasts.map(toast=>(
{toast.message}
))}
)};export default App; \ No newline at end of file diff --git a/servers/airtable/src/apps/dashboard/index.html b/servers/airtable/src/apps/dashboard/index.html new file mode 100644 index 0000000..9927873 --- /dev/null +++ b/servers/airtable/src/apps/dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Airtable Dashboard + + +
+ + + diff --git a/servers/airtable/src/apps/dashboard/main.tsx b/servers/airtable/src/apps/dashboard/main.tsx new file mode 100644 index 0000000..5b258cd --- /dev/null +++ b/servers/airtable/src/apps/dashboard/main.tsx @@ -0,0 +1 @@ +import React,{lazy,Suspense}from'react';import{createRoot}from'react-dom/client';import'./styles.css';const App=lazy(()=>import('./App'));class ErrorBoundary extends React.Component<{children:React.ReactNode},{hasError:boolean;error?:Error}>{constructor(props:{children:React.ReactNode}){super(props);this.state={hasError:false}}static getDerivedStateFromError(error:Error){return{hasError:true,error}}componentDidCatch(error:Error,errorInfo:React.ErrorInfo){console.error('ErrorBoundary caught:',error,errorInfo)}render(){if(this.state.hasError){return(

Something went wrong

{this.state.error?.message}

)}return this.props.children}}const LoadingSkeleton=()=>(
);const root=createRoot(document.getElementById('root')!);root.render(}>); \ No newline at end of file diff --git a/servers/airtable/src/apps/dashboard/styles.css b/servers/airtable/src/apps/dashboard/styles.css new file mode 100644 index 0000000..f252c8e --- /dev/null +++ b/servers/airtable/src/apps/dashboard/styles.css @@ -0,0 +1 @@ +:root{--bg-primary:#0f172a;--bg-secondary:#1e293b;--bg-tertiary:#334155;--text-primary:#f1f5f9;--text-secondary:#cbd5e1;--text-muted:#94a3b8;--accent:#3b82f6;--accent-hover:#2563eb;--success:#10b981;--warning:#f59e0b;--error:#ef4444;--border:#334155;--shadow:rgba(0,0,0,0.3)}*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,sans-serif;background:var(--bg-primary);color:var(--text-primary);line-height:1.6}#root{min-height:100vh}.app-container{max-width:1600px;margin:0 auto;padding:2rem}.app-header{margin-bottom:2rem;text-align:center}.app-header h1{font-size:2.5rem;font-weight:700;margin-bottom:0.5rem}.subtitle{color:var(--text-secondary);font-size:1.125rem}.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1.5rem;margin-bottom:3rem}.stat-card{background:var(--bg-secondary);padding:2rem;border-radius:0.75rem;border:1px solid var(--border);transition:transform 0.2s,box-shadow 0.2s;text-align:center}.stat-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px var(--shadow)}.stat-highlight{border:2px solid var(--accent);background:linear-gradient(135deg,var(--bg-secondary) 0%,rgba(59,130,246,0.1) 100%)}.stat-value{font-size:3rem;font-weight:700;color:var(--accent);margin-bottom:0.5rem}.stat-label{color:var(--text-secondary);font-size:1rem;text-transform:uppercase;letter-spacing:0.05em}.activity-section{margin-bottom:3rem}.activity-section h2{font-size:1.75rem;margin-bottom:1.5rem}.activity-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.activity-card{background:var(--bg-secondary);border:1px solid var(--border);border-radius:0.75rem;padding:1.5rem;cursor:pointer;transition:transform 0.2s,box-shadow 0.2s,border-color 0.2s}.activity-card:hover{transform:translateY(-2px);box-shadow:0 8px 16px var(--shadow);border-color:var(--accent)}.activity-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem}.activity-header h3{font-size:1.125rem;font-weight:600}.activity-time{color:var(--text-muted);font-size:0.875rem}.activity-stats{display:flex;gap:2rem}.activity-stat{display:flex;flex-direction:column;gap:0.25rem;font-size:0.875rem}.activity-stat .label{color:var(--text-secondary)}.activity-stat .value{color:var(--text-primary);font-weight:600;font-size:1.125rem}.overview-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(350px,1fr));gap:1.5rem;margin-bottom:2rem}.overview-card{background:var(--bg-secondary);border:1px solid var(--border);border-radius:0.75rem;padding:2rem}.overview-card h3{font-size:1.25rem;margin-bottom:1.5rem;padding-bottom:1rem;border-bottom:2px solid var(--border)}.quick-stats{display:flex;flex-direction:column;gap:1rem}.quick-stat{display:flex;justify-content:space-between;padding:0.75rem;background:var(--bg-tertiary);border-radius:0.5rem}.quick-stat span{color:var(--text-secondary)}.quick-stat strong{color:var(--text-primary)}.health-indicators{display:flex;flex-direction:column;gap:1rem}.health-item{display:flex;align-items:center;gap:0.75rem;padding:0.75rem;background:var(--bg-tertiary);border-radius:0.5rem}.health-dot{width:12px;height:12px;border-radius:50%;flex-shrink:0}.health-good{background:var(--success);box-shadow:0 0 8px var(--success)}.detail-panel{background:var(--bg-secondary);border:1px solid var(--border);border-radius:0.75rem;padding:1.5rem}.detail-panel h2{font-size:1.5rem;margin-bottom:1rem}.toast-container{position:fixed;top:1rem;right:1rem;z-index:1000;display:flex;flex-direction:column;gap:0.5rem}.toast{background:var(--bg-secondary);border:1px solid var(--border);border-radius:0.5rem;padding:1rem 1.5rem;min-width:250px;box-shadow:0 4px 12px var(--shadow);animation:slideIn 0.3s ease-out}.toast-success{border-left:4px solid var(--success)}.toast-error{border-left:4px solid var(--error)}.toast-info{border-left:4px solid var(--accent)}@keyframes slideIn{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}.shimmer{background:linear-gradient(90deg,var(--bg-secondary) 0%,var(--bg-tertiary) 50%,var(--bg-secondary) 100%);background-size:200% 100%;animation:shimmer 1.5s infinite}@keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}.loading-skeleton{padding:2rem}.skeleton-header{height:3rem;border-radius:0.5rem;margin-bottom:2rem}.skeleton-content{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.skeleton-card{height:150px;border-radius:0.75rem}.error-boundary{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;padding:2rem;text-align:center}.error-boundary h1{font-size:2rem;margin-bottom:1rem;color:var(--error)}.error-boundary button{margin-top:1rem;padding:0.75rem 1.5rem;background:var(--accent);color:white;border:none;border-radius:0.5rem;cursor:pointer;font-size:1rem;transition:background 0.2s}.error-boundary button:hover{background:var(--accent-hover)}@media (max-width:768px){.app-container{padding:1rem}.stats-grid{grid-template-columns:1fr}.activity-list{grid-template-columns:1fr}.overview-grid{grid-template-columns:1fr}.app-header h1{font-size:1.75rem}} \ No newline at end of file diff --git a/servers/airtable/src/apps/field-manager/App.tsx b/servers/airtable/src/apps/field-manager/App.tsx new file mode 100644 index 0000000..b2def54 --- /dev/null +++ b/servers/airtable/src/apps/field-manager/App.tsx @@ -0,0 +1,157 @@ +import React, { useState, useMemo, useTransition, useCallback } from 'react'; + +const useDebounce = (value: T, delay: number = 300): T => { + const [debouncedValue, setDebouncedValue] = useState(value); + + React.useEffect(() => { + const handler = setTimeout(() => setDebouncedValue(value), delay); + return () => clearTimeout(handler); + }, [value, delay]); + + return debouncedValue; +}; + +const useToast = () => { + const [toasts, setToasts] = useState>([]); + + const showToast = useCallback((message: string, type: string = 'info') => { + const id = Date.now(); + setToasts(prev => [...prev, { id, message, type }]); + setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 3000); + }, []); + + return { toasts, showToast }; +}; + +interface Field { + id: string; + name: string; + type: string; + required: boolean; + description: string; +} + +const mockFields: Field[] = [ + { id: 'fld1', name: 'Name', type: 'singleLineText', required: true, description: 'Contact name' }, + { id: 'fld2', name: 'Email', type: 'email', required: true, description: 'Email address' }, + { id: 'fld3', name: 'Phone', type: 'phoneNumber', required: false, description: 'Phone number' }, + { id: 'fld4', name: 'Status', type: 'singleSelect', required: true, description: 'Current status' }, + { id: 'fld5', name: 'Notes', type: 'multilineText', required: false, description: 'Additional notes' }, + { id: 'fld6', name: 'Created', type: 'dateTime', required: true, description: 'Creation date' }, +]; + +const App: React.FC = () => { + const [searchQuery, setSearchQuery] = useState(''); + const [selectedField, setSelectedField] = useState(null); + const [isPending, startTransition] = useTransition(); + const { toasts, showToast } = useToast(); + + const debouncedSearch = useDebounce(searchQuery, 300); + + const filteredFields = useMemo(() => { + return mockFields.filter(field => + field.name.toLowerCase().includes(debouncedSearch.toLowerCase()) || + field.type.toLowerCase().includes(debouncedSearch.toLowerCase()) + ); + }, [debouncedSearch]); + + const stats = useMemo(() => ({ + total: mockFields.length, + required: mockFields.filter(f => f.required).length, + optional: mockFields.filter(f => !f.required).length, + }), []); + + const handleFieldClick = (field: Field) => { + startTransition(() => { + setSelectedField(field); + showToast(`Selected field: ${field.name}`, 'success'); + }); + }; + + return ( +
+
+

Field Manager

+

View and configure table fields

+
+ +
+
+
{stats.total}
+
Total Fields
+
+
+
{stats.required}
+
Required
+
+
+
{stats.optional}
+
Optional
+
+
+ +
+ setSearchQuery(e.target.value)} + className="search-input" + /> +
+ + {filteredFields.length === 0 ? ( +
+
🔍
+

No fields found

+

Try adjusting your search query

+
+ ) : ( +
+ {filteredFields.map(field => ( +
handleFieldClick(field)} + > +
+

{field.name}

+ {field.required && Required} +
+
+
+ Type: + {field.type} +
+
+ Description: + {field.description} +
+
+
+ ))} +
+ )} + + {selectedField && ( +
+

Field Details: {selectedField.name}

+

ID: {selectedField.id}

+

Type: {selectedField.type}

+

Required: {selectedField.required ? 'Yes' : 'No'}

+

Description: {selectedField.description}

+
+ )} + +
+ {toasts.map(toast => ( +
+ {toast.message} +
+ ))} +
+
+ ); +}; + +export default App; diff --git a/servers/airtable/src/apps/field-manager/index.html b/servers/airtable/src/apps/field-manager/index.html new file mode 100644 index 0000000..4bf9ae7 --- /dev/null +++ b/servers/airtable/src/apps/field-manager/index.html @@ -0,0 +1,12 @@ + + + + + + Airtable Field Manager + + +
+ + + diff --git a/servers/airtable/src/apps/field-manager/main.tsx b/servers/airtable/src/apps/field-manager/main.tsx new file mode 100644 index 0000000..22b087f --- /dev/null +++ b/servers/airtable/src/apps/field-manager/main.tsx @@ -0,0 +1,58 @@ +import React, { lazy, Suspense } from 'react'; +import { createRoot } from 'react-dom/client'; +import './styles.css'; + +const App = lazy(() => import('./App')); + +class ErrorBoundary extends React.Component< + { children: React.ReactNode }, + { hasError: boolean; error?: Error } +> { + constructor(props: { children: React.ReactNode }) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error) { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('ErrorBoundary caught:', error, errorInfo); + } + + render() { + if (this.state.hasError) { + return ( +
+

Something went wrong

+

{this.state.error?.message}

+ +
+ ); + } + return this.props.children; + } +} + +const LoadingSkeleton = () => ( +
+
+
+
+
+
+
+
+); + +const root = createRoot(document.getElementById('root')!); +root.render( + + + }> + + + + +); diff --git a/servers/airtable/src/apps/field-manager/styles.css b/servers/airtable/src/apps/field-manager/styles.css new file mode 100644 index 0000000..ef40b08 --- /dev/null +++ b/servers/airtable/src/apps/field-manager/styles.css @@ -0,0 +1 @@ +:root{--bg-primary:#0f172a;--bg-secondary:#1e293b;--bg-tertiary:#334155;--text-primary:#f1f5f9;--text-secondary:#cbd5e1;--text-muted:#94a3b8;--accent:#3b82f6;--accent-hover:#2563eb;--success:#10b981;--warning:#f59e0b;--error:#ef4444;--border:#334155;--shadow:rgba(0,0,0,0.3)}*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,sans-serif;background:var(--bg-primary);color:var(--text-primary);line-height:1.6}#root{min-height:100vh}.app-container{max-width:1400px;margin:0 auto;padding:2rem}.app-header{margin-bottom:2rem}.app-header h1{font-size:2rem;font-weight:700;margin-bottom:0.5rem}.subtitle{color:var(--text-secondary);font-size:1rem}.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem;margin-bottom:2rem}.stat-card{background:var(--bg-secondary);padding:1.5rem;border-radius:0.75rem;border:1px solid var(--border);transition:transform 0.2s,box-shadow 0.2s}.stat-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px var(--shadow)}.stat-value{font-size:2.5rem;font-weight:700;color:var(--accent)}.stat-label{color:var(--text-secondary);font-size:0.875rem;margin-top:0.5rem}.search-container{margin-bottom:2rem}.search-input{width:100%;padding:0.875rem 1rem;background:var(--bg-secondary);border:1px solid var(--border);border-radius:0.5rem;color:var(--text-primary);font-size:1rem;transition:border-color 0.2s}.search-input:focus{outline:none;border-color:var(--accent)}.data-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem;margin-bottom:2rem}.data-card{background:var(--bg-secondary);border:1px solid var(--border);border-radius:0.75rem;padding:1.5rem;cursor:pointer;transition:transform 0.2s,box-shadow 0.2s,border-color 0.2s}.data-card:hover{transform:translateY(-2px);box-shadow:0 8px 16px var(--shadow);border-color:var(--accent)}.card-header{display:flex;justify-content:space-between;align-items:start;margin-bottom:1rem}.card-header h3{font-size:1.125rem;font-weight:600;color:var(--text-primary)}.badge{padding:0.25rem 0.75rem;border-radius:1rem;font-size:0.75rem;font-weight:600;text-transform:uppercase}.badge-required{background:rgba(239,68,68,0.2);color:var(--error)}.card-body{display:flex;flex-direction:column;gap:0.5rem}.card-stat{display:flex;justify-content:space-between;font-size:0.875rem}.card-stat .label{color:var(--text-secondary)}.card-stat .value{color:var(--text-primary);font-weight:500}.detail-panel{background:var(--bg-secondary);border:1px solid var(--border);border-radius:0.75rem;padding:1.5rem;margin-top:2rem}.detail-panel h2{font-size:1.5rem;margin-bottom:1rem}.empty-state{text-align:center;padding:4rem 2rem}.empty-icon{font-size:4rem;margin-bottom:1rem}.empty-state h3{font-size:1.5rem;margin-bottom:0.5rem}.empty-state p{color:var(--text-secondary)}.toast-container{position:fixed;top:1rem;right:1rem;z-index:1000;display:flex;flex-direction:column;gap:0.5rem}.toast{background:var(--bg-secondary);border:1px solid var(--border);border-radius:0.5rem;padding:1rem 1.5rem;min-width:250px;box-shadow:0 4px 12px var(--shadow);animation:slideIn 0.3s ease-out}.toast-success{border-left:4px solid var(--success)}.toast-error{border-left:4px solid var(--error)}.toast-info{border-left:4px solid var(--accent)}@keyframes slideIn{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}.shimmer{background:linear-gradient(90deg,var(--bg-secondary) 0%,var(--bg-tertiary) 50%,var(--bg-secondary) 100%);background-size:200% 100%;animation:shimmer 1.5s infinite}@keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}.loading-skeleton{padding:2rem}.skeleton-header{height:3rem;border-radius:0.5rem;margin-bottom:2rem}.skeleton-content{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.skeleton-card{height:150px;border-radius:0.75rem}.error-boundary{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;padding:2rem;text-align:center}.error-boundary h1{font-size:2rem;margin-bottom:1rem;color:var(--error)}.error-boundary button{margin-top:1rem;padding:0.75rem 1.5rem;background:var(--accent);color:white;border:none;border-radius:0.5rem;cursor:pointer;font-size:1rem;transition:background 0.2s}.error-boundary button:hover{background:var(--accent-hover)}@media (max-width:768px){.app-container{padding:1rem}.stats-grid{grid-template-columns:1fr}.data-grid{grid-template-columns:1fr}.app-header h1{font-size:1.5rem}} \ No newline at end of file diff --git a/servers/airtable/src/apps/form-builder/App.tsx b/servers/airtable/src/apps/form-builder/App.tsx new file mode 100644 index 0000000..a2bb2c4 --- /dev/null +++ b/servers/airtable/src/apps/form-builder/App.tsx @@ -0,0 +1 @@ +import React,{useState,useMemo,useTransition,useCallback}from'react';const useDebounce=(value:T,delay:number=300):T=>{const[debouncedValue,setDebouncedValue]=useState(value);React.useEffect(()=>{const handler=setTimeout(()=>setDebouncedValue(value),delay);return()=>clearTimeout(handler)},[value,delay]);return debouncedValue};const useToast=()=>{const[toasts,setToasts]=useState>([]);const showToast=useCallback((message:string,type:string='info')=>{const id=Date.now();setToasts(prev=>[...prev,{id,message,type}]);setTimeout(()=>setToasts(prev=>prev.filter(t=>t.id!==id)),3000)},[]);return{toasts,showToast}};interface FormField{id:string;label:string;type:string;required:boolean}const mockFields:FormField[]=[{id:'f1',label:'Name',type:'text',required:true},{id:'f2',label:'Email',type:'email',required:true},{id:'f3',label:'Phone',type:'tel',required:false},{id:'f4',label:'Message',type:'textarea',required:false}];const App:React.FC=()=>{const[fields,setFields]=useState(mockFields);const[formData,setFormData]=useState>({});const[isPending,startTransition]=useTransition();const{toasts,showToast}=useToast();const stats=useMemo(()=>({total:fields.length,required:fields.filter(f=>f.required).length,filled:Object.keys(formData).length}),[fields,formData]);const handleFieldChange=(id:string,value:string)=>{startTransition(()=>{setFormData(prev=>({...prev,[id]:value}))})};const handleSubmit=()=>{showToast('Form submitted successfully!','success')};const handleAddField=()=>{const newField={id:`f${fields.length+1}`,label:'New Field',type:'text',required:false};setFields([...fields,newField]);showToast('Field added','success')};return(

Form Builder

Create Airtable forms

{stats.total}
Fields
{stats.required}
Required
{stats.filled}
Filled

Form Preview

{fields.map(field=>(
{field.type==='textarea'?(