helix-lang 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -0
- package/dist/architect.d.ts +14 -0
- package/dist/architect.d.ts.map +1 -0
- package/dist/architect.js +127 -0
- package/dist/architect.js.map +1 -0
- package/dist/bin/helix.d.ts +20 -0
- package/dist/bin/helix.d.ts.map +1 -0
- package/dist/bin/helix.js +921 -0
- package/dist/bin/helix.js.map +1 -0
- package/dist/commands/collaborate/index.d.ts +2 -0
- package/dist/commands/collaborate/index.d.ts.map +1 -0
- package/dist/commands/collaborate/index.js +129 -0
- package/dist/commands/collaborate/index.js.map +1 -0
- package/dist/commands/collaborate/server.d.ts +31 -0
- package/dist/commands/collaborate/server.d.ts.map +1 -0
- package/dist/commands/collaborate/server.js +159 -0
- package/dist/commands/collaborate/server.js.map +1 -0
- package/dist/commands/deploy/index.d.ts +25 -0
- package/dist/commands/deploy/index.d.ts.map +1 -0
- package/dist/commands/deploy/index.js +130 -0
- package/dist/commands/deploy/index.js.map +1 -0
- package/dist/commands/deploy/platforms/fly.d.ts +9 -0
- package/dist/commands/deploy/platforms/fly.d.ts.map +1 -0
- package/dist/commands/deploy/platforms/fly.js +68 -0
- package/dist/commands/deploy/platforms/fly.js.map +1 -0
- package/dist/commands/deploy/platforms/railway.d.ts +9 -0
- package/dist/commands/deploy/platforms/railway.d.ts.map +1 -0
- package/dist/commands/deploy/platforms/railway.js +115 -0
- package/dist/commands/deploy/platforms/railway.js.map +1 -0
- package/dist/commands/deploy/platforms/vercel.d.ts +10 -0
- package/dist/commands/deploy/platforms/vercel.d.ts.map +1 -0
- package/dist/commands/deploy/platforms/vercel.js +126 -0
- package/dist/commands/deploy/platforms/vercel.js.map +1 -0
- package/dist/commands/deploy.d.ts +6 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +56 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/evolve/analyzers/performance.d.ts +13 -0
- package/dist/commands/evolve/analyzers/performance.d.ts.map +1 -0
- package/dist/commands/evolve/analyzers/performance.js +591 -0
- package/dist/commands/evolve/analyzers/performance.js.map +1 -0
- package/dist/commands/evolve/analyzers/security.d.ts +21 -0
- package/dist/commands/evolve/analyzers/security.d.ts.map +1 -0
- package/dist/commands/evolve/analyzers/security.js +280 -0
- package/dist/commands/evolve/analyzers/security.js.map +1 -0
- package/dist/commands/evolve/index.d.ts +2 -0
- package/dist/commands/evolve/index.d.ts.map +1 -0
- package/dist/commands/evolve/index.js +122 -0
- package/dist/commands/evolve/index.js.map +1 -0
- package/dist/commands/generate.d.ts +6 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +277 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +176 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/library/index.d.ts +27 -0
- package/dist/commands/library/index.d.ts.map +1 -0
- package/dist/commands/library/index.js +126 -0
- package/dist/commands/library/index.js.map +1 -0
- package/dist/commands/migrate.d.ts +5 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +258 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/new.d.ts +6 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +195 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/preflight.d.ts +20 -0
- package/dist/commands/preflight.d.ts.map +1 -0
- package/dist/commands/preflight.js +182 -0
- package/dist/commands/preflight.js.map +1 -0
- package/dist/commands/preview.d.ts +13 -0
- package/dist/commands/preview.d.ts.map +1 -0
- package/dist/commands/preview.js +260 -0
- package/dist/commands/preview.js.map +1 -0
- package/dist/commands/run.d.ts +6 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +96 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/spawn.d.ts +11 -0
- package/dist/commands/spawn.d.ts.map +1 -0
- package/dist/commands/spawn.js +916 -0
- package/dist/commands/spawn.js.map +1 -0
- package/dist/compiler.d.ts +12 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +92 -0
- package/dist/compiler.js.map +1 -0
- package/dist/core/file-writer.d.ts +36 -0
- package/dist/core/file-writer.d.ts.map +1 -0
- package/dist/core/file-writer.js +268 -0
- package/dist/core/file-writer.js.map +1 -0
- package/dist/core/registry.d.ts +57 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +222 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/self-healing.d.ts +47 -0
- package/dist/core/self-healing.d.ts.map +1 -0
- package/dist/core/self-healing.js +250 -0
- package/dist/core/self-healing.js.map +1 -0
- package/dist/core/types.d.ts +126 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +7 -0
- package/dist/core/types.js.map +1 -0
- package/dist/generators/databases/mongodb.d.ts +10 -0
- package/dist/generators/databases/mongodb.d.ts.map +1 -0
- package/dist/generators/databases/mongodb.js +83 -0
- package/dist/generators/databases/mongodb.js.map +1 -0
- package/dist/generators/databases/redis.d.ts +2 -0
- package/dist/generators/databases/redis.d.ts.map +1 -0
- package/dist/generators/databases/redis.js +140 -0
- package/dist/generators/databases/redis.js.map +1 -0
- package/dist/generators/flutter.d.ts +32 -0
- package/dist/generators/flutter.d.ts.map +1 -0
- package/dist/generators/flutter.js +628 -0
- package/dist/generators/flutter.js.map +1 -0
- package/dist/openrouter.d.ts +68 -0
- package/dist/openrouter.d.ts.map +1 -0
- package/dist/openrouter.js +241 -0
- package/dist/openrouter.js.map +1 -0
- package/dist/page-generator.d.ts +22 -0
- package/dist/page-generator.d.ts.map +1 -0
- package/dist/page-generator.js +192 -0
- package/dist/page-generator.js.map +1 -0
- package/dist/parser.d.ts +76 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +691 -0
- package/dist/parser.js.map +1 -0
- package/dist/prompts/master-architect.d.ts +9 -0
- package/dist/prompts/master-architect.d.ts.map +1 -0
- package/dist/prompts/master-architect.js +150 -0
- package/dist/prompts/master-architect.js.map +1 -0
- package/dist/researcher.d.ts +12 -0
- package/dist/researcher.d.ts.map +1 -0
- package/dist/researcher.js +85 -0
- package/dist/researcher.js.map +1 -0
- package/dist/self-heal.d.ts +29 -0
- package/dist/self-heal.d.ts.map +1 -0
- package/dist/self-heal.js +260 -0
- package/dist/self-heal.js.map +1 -0
- package/dist/services/SupabaseDeployer.d.ts +9 -0
- package/dist/services/SupabaseDeployer.d.ts.map +1 -0
- package/dist/services/SupabaseDeployer.js +50 -0
- package/dist/services/SupabaseDeployer.js.map +1 -0
- package/dist/test-generator.d.ts +18 -0
- package/dist/test-generator.d.ts.map +1 -0
- package/dist/test-generator.js +180 -0
- package/dist/test-generator.js.map +1 -0
- package/dist/themes/index.d.ts +52 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/index.js +273 -0
- package/dist/themes/index.js.map +1 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +81 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/constitutional-validator.d.ts +73 -0
- package/dist/utils/constitutional-validator.d.ts.map +1 -0
- package/dist/utils/constitutional-validator.js +249 -0
- package/dist/utils/constitutional-validator.js.map +1 -0
- package/library/auth-flow/app/api/auth/[...nextauth]/route.ts +31 -0
- package/library/auth-flow/app/api/auth/login/route.ts +90 -0
- package/library/auth-flow/app/api/auth/register/route.ts +91 -0
- package/library/auth-flow/components/auth/AuthMiddleware.tsx +139 -0
- package/library/auth-flow/components/auth/LoginForm.tsx +125 -0
- package/library/auth-flow/components/auth/RegisterForm.tsx +168 -0
- package/library/auth-flow/components/auth/nextauth-config.ts +99 -0
- package/library/auth-flow/manifest.json +29 -0
- package/library/auth-flow/schema.prisma +45 -0
- package/library/dashboard-analytics/components/dashboard/ActivityFeed.tsx +109 -0
- package/library/dashboard-analytics/components/dashboard/LineChart.tsx +180 -0
- package/library/dashboard-analytics/components/dashboard/StatsCard.tsx +47 -0
- package/library/dashboard-analytics/components/dashboard/SummaryGrid.tsx +39 -0
- package/library/dashboard-analytics/manifest.json +19 -0
- package/library/data-table/components/table/BulkActions.tsx +59 -0
- package/library/data-table/components/table/ColumnToggle.tsx +65 -0
- package/library/data-table/components/table/DataTable.tsx +318 -0
- package/library/data-table/components/table/ExportCSV.tsx +52 -0
- package/library/data-table/components/table/SearchFilter.tsx +48 -0
- package/library/data-table/manifest.json +20 -0
- package/library/file-upload/app/api/upload/route.ts +107 -0
- package/library/file-upload/components/upload/DropZone.tsx +268 -0
- package/library/file-upload/components/upload/FilePreview.tsx +82 -0
- package/library/file-upload/components/upload/UploadProgress.tsx +92 -0
- package/library/file-upload/components/upload/fileStorage.ts +142 -0
- package/library/file-upload/manifest.json +21 -0
- package/library/notification-system/app/api/notifications/route.ts +121 -0
- package/library/notification-system/components/notifications/NotificationBell.tsx +154 -0
- package/library/notification-system/components/notifications/NotificationProvider.tsx +161 -0
- package/library/notification-system/components/notifications/Toast.tsx +112 -0
- package/library/notification-system/manifest.json +20 -0
- package/package.json +66 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo, useCallback } from 'react';
|
|
4
|
+
import SearchFilter from './SearchFilter';
|
|
5
|
+
import ColumnToggle from './ColumnToggle';
|
|
6
|
+
import ExportCSV from './ExportCSV';
|
|
7
|
+
import BulkActions from './BulkActions';
|
|
8
|
+
|
|
9
|
+
export interface Column<T> {
|
|
10
|
+
key: string;
|
|
11
|
+
label: string;
|
|
12
|
+
sortable?: boolean;
|
|
13
|
+
render?: (value: unknown, row: T) => React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface DataTableProps<T extends Record<string, unknown>> {
|
|
17
|
+
data: T[];
|
|
18
|
+
columns: Column<T>[];
|
|
19
|
+
pageSize?: number;
|
|
20
|
+
searchable?: boolean;
|
|
21
|
+
exportable?: boolean;
|
|
22
|
+
selectable?: boolean;
|
|
23
|
+
onBulkDelete?: (ids: string[]) => void;
|
|
24
|
+
rowKey?: string;
|
|
25
|
+
exportFilename?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type SortDirection = 'asc' | 'desc' | null;
|
|
29
|
+
|
|
30
|
+
export default function DataTable<T extends Record<string, unknown>>({
|
|
31
|
+
data,
|
|
32
|
+
columns,
|
|
33
|
+
pageSize = 10,
|
|
34
|
+
searchable = true,
|
|
35
|
+
exportable = true,
|
|
36
|
+
selectable = true,
|
|
37
|
+
onBulkDelete,
|
|
38
|
+
rowKey = 'id',
|
|
39
|
+
exportFilename = 'data',
|
|
40
|
+
}: DataTableProps<T>) {
|
|
41
|
+
const [search, setSearch] = useState('');
|
|
42
|
+
const [sortKey, setSortKey] = useState<string | null>(null);
|
|
43
|
+
const [sortDir, setSortDir] = useState<SortDirection>(null);
|
|
44
|
+
const [page, setPage] = useState(0);
|
|
45
|
+
const [visibleCols, setVisibleCols] = useState<Set<string>>(
|
|
46
|
+
() => new Set(columns.map((c) => c.key))
|
|
47
|
+
);
|
|
48
|
+
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());
|
|
49
|
+
|
|
50
|
+
// Filter
|
|
51
|
+
const filtered = useMemo(() => {
|
|
52
|
+
if (!search.trim()) return data;
|
|
53
|
+
const q = search.toLowerCase();
|
|
54
|
+
return data.filter((row) =>
|
|
55
|
+
columns.some((col) => {
|
|
56
|
+
const val = row[col.key];
|
|
57
|
+
return val !== null && val !== undefined && String(val).toLowerCase().includes(q);
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
}, [data, search, columns]);
|
|
61
|
+
|
|
62
|
+
// Sort
|
|
63
|
+
const sorted = useMemo(() => {
|
|
64
|
+
if (!sortKey || !sortDir) return filtered;
|
|
65
|
+
return [...filtered].sort((a, b) => {
|
|
66
|
+
const aVal = a[sortKey];
|
|
67
|
+
const bVal = b[sortKey];
|
|
68
|
+
if (aVal === bVal) return 0;
|
|
69
|
+
if (aVal === null || aVal === undefined) return 1;
|
|
70
|
+
if (bVal === null || bVal === undefined) return -1;
|
|
71
|
+
const cmp = String(aVal).localeCompare(String(bVal), undefined, { numeric: true });
|
|
72
|
+
return sortDir === 'asc' ? cmp : -cmp;
|
|
73
|
+
});
|
|
74
|
+
}, [filtered, sortKey, sortDir]);
|
|
75
|
+
|
|
76
|
+
// Paginate
|
|
77
|
+
const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize));
|
|
78
|
+
const currentPage = Math.min(page, totalPages - 1);
|
|
79
|
+
const paginated = sorted.slice(currentPage * pageSize, (currentPage + 1) * pageSize);
|
|
80
|
+
|
|
81
|
+
const activeColumns = columns.filter((c) => visibleCols.has(c.key));
|
|
82
|
+
|
|
83
|
+
const handleSort = useCallback(
|
|
84
|
+
(key: string) => {
|
|
85
|
+
if (sortKey === key) {
|
|
86
|
+
if (sortDir === 'asc') setSortDir('desc');
|
|
87
|
+
else if (sortDir === 'desc') {
|
|
88
|
+
setSortKey(null);
|
|
89
|
+
setSortDir(null);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
setSortKey(key);
|
|
93
|
+
setSortDir('asc');
|
|
94
|
+
}
|
|
95
|
+
setPage(0);
|
|
96
|
+
},
|
|
97
|
+
[sortKey, sortDir]
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const toggleRow = (id: string) => {
|
|
101
|
+
setSelectedRows((prev) => {
|
|
102
|
+
const next = new Set(prev);
|
|
103
|
+
if (next.has(id)) next.delete(id);
|
|
104
|
+
else next.add(id);
|
|
105
|
+
return next;
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const selectAll = () => {
|
|
110
|
+
setSelectedRows(new Set(sorted.map((r) => String(r[rowKey]))));
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const deselectAll = () => {
|
|
114
|
+
setSelectedRows(new Set());
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const handleBulkDelete = () => {
|
|
118
|
+
if (onBulkDelete) {
|
|
119
|
+
onBulkDelete(Array.from(selectedRows));
|
|
120
|
+
setSelectedRows(new Set());
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const toggleColumn = (key: string) => {
|
|
125
|
+
setVisibleCols((prev) => {
|
|
126
|
+
const next = new Set(prev);
|
|
127
|
+
if (next.has(key) && next.size > 1) next.delete(key);
|
|
128
|
+
else next.add(key);
|
|
129
|
+
return next;
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div className="backdrop-blur-xl bg-white/5 border border-white/10 rounded-2xl overflow-hidden">
|
|
135
|
+
{/* Toolbar */}
|
|
136
|
+
<div className="p-4 border-b border-white/5 flex flex-col sm:flex-row gap-3">
|
|
137
|
+
{searchable && (
|
|
138
|
+
<div className="flex-1 max-w-sm">
|
|
139
|
+
<SearchFilter
|
|
140
|
+
value={search}
|
|
141
|
+
onChange={(v) => {
|
|
142
|
+
setSearch(v);
|
|
143
|
+
setPage(0);
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
<div className="flex items-center gap-2 ml-auto">
|
|
149
|
+
<ColumnToggle
|
|
150
|
+
columns={columns}
|
|
151
|
+
visibleColumns={visibleCols}
|
|
152
|
+
onToggle={toggleColumn}
|
|
153
|
+
/>
|
|
154
|
+
{exportable && (
|
|
155
|
+
<ExportCSV
|
|
156
|
+
data={sorted as Record<string, unknown>[]}
|
|
157
|
+
columns={activeColumns}
|
|
158
|
+
filename={exportFilename}
|
|
159
|
+
/>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{/* Bulk actions */}
|
|
165
|
+
{selectable && selectedRows.size > 0 && (
|
|
166
|
+
<div className="px-4 pt-3">
|
|
167
|
+
<BulkActions
|
|
168
|
+
selectedCount={selectedRows.size}
|
|
169
|
+
totalCount={sorted.length}
|
|
170
|
+
onSelectAll={selectAll}
|
|
171
|
+
onDeselectAll={deselectAll}
|
|
172
|
+
onDeleteSelected={handleBulkDelete}
|
|
173
|
+
allSelected={selectedRows.size === sorted.length}
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
{/* Table */}
|
|
179
|
+
<div className="overflow-x-auto">
|
|
180
|
+
<table className="w-full">
|
|
181
|
+
<thead>
|
|
182
|
+
<tr className="border-b border-white/5">
|
|
183
|
+
{selectable && (
|
|
184
|
+
<th className="w-12 px-4 py-3">
|
|
185
|
+
<input
|
|
186
|
+
type="checkbox"
|
|
187
|
+
checked={paginated.length > 0 && paginated.every((r) => selectedRows.has(String(r[rowKey])))}
|
|
188
|
+
onChange={() => {
|
|
189
|
+
const pageIds = paginated.map((r) => String(r[rowKey]));
|
|
190
|
+
const allChecked = pageIds.every((id) => selectedRows.has(id));
|
|
191
|
+
setSelectedRows((prev) => {
|
|
192
|
+
const next = new Set(prev);
|
|
193
|
+
pageIds.forEach((id) => {
|
|
194
|
+
if (allChecked) next.delete(id);
|
|
195
|
+
else next.add(id);
|
|
196
|
+
});
|
|
197
|
+
return next;
|
|
198
|
+
});
|
|
199
|
+
}}
|
|
200
|
+
className="w-4 h-4 rounded border-white/20 bg-white/5 text-cyan-500 focus:ring-cyan-500/50 focus:ring-offset-0"
|
|
201
|
+
/>
|
|
202
|
+
</th>
|
|
203
|
+
)}
|
|
204
|
+
{activeColumns.map((col) => (
|
|
205
|
+
<th
|
|
206
|
+
key={col.key}
|
|
207
|
+
className="px-4 py-3 text-left text-xs font-medium text-white/40 uppercase tracking-wider"
|
|
208
|
+
>
|
|
209
|
+
{col.sortable !== false ? (
|
|
210
|
+
<button
|
|
211
|
+
onClick={() => handleSort(col.key)}
|
|
212
|
+
className="flex items-center gap-1 hover:text-white/70 transition-colors"
|
|
213
|
+
>
|
|
214
|
+
{col.label}
|
|
215
|
+
<span className="text-[10px]">
|
|
216
|
+
{sortKey === col.key
|
|
217
|
+
? sortDir === 'asc'
|
|
218
|
+
? '\u25B2'
|
|
219
|
+
: '\u25BC'
|
|
220
|
+
: '\u25B4\u25BE'}
|
|
221
|
+
</span>
|
|
222
|
+
</button>
|
|
223
|
+
) : (
|
|
224
|
+
col.label
|
|
225
|
+
)}
|
|
226
|
+
</th>
|
|
227
|
+
))}
|
|
228
|
+
</tr>
|
|
229
|
+
</thead>
|
|
230
|
+
<tbody>
|
|
231
|
+
{paginated.length === 0 ? (
|
|
232
|
+
<tr>
|
|
233
|
+
<td
|
|
234
|
+
colSpan={activeColumns.length + (selectable ? 1 : 0)}
|
|
235
|
+
className="px-4 py-12 text-center text-white/30 text-sm"
|
|
236
|
+
>
|
|
237
|
+
No results found
|
|
238
|
+
</td>
|
|
239
|
+
</tr>
|
|
240
|
+
) : (
|
|
241
|
+
paginated.map((row, idx) => {
|
|
242
|
+
const id = String(row[rowKey]);
|
|
243
|
+
const isSelected = selectedRows.has(id);
|
|
244
|
+
return (
|
|
245
|
+
<tr
|
|
246
|
+
key={id || idx}
|
|
247
|
+
className={`border-b border-white/5 hover:bg-white/[0.03] transition-colors ${
|
|
248
|
+
isSelected ? 'bg-cyan-500/5' : ''
|
|
249
|
+
}`}
|
|
250
|
+
>
|
|
251
|
+
{selectable && (
|
|
252
|
+
<td className="px-4 py-3">
|
|
253
|
+
<input
|
|
254
|
+
type="checkbox"
|
|
255
|
+
checked={isSelected}
|
|
256
|
+
onChange={() => toggleRow(id)}
|
|
257
|
+
className="w-4 h-4 rounded border-white/20 bg-white/5 text-cyan-500 focus:ring-cyan-500/50 focus:ring-offset-0"
|
|
258
|
+
/>
|
|
259
|
+
</td>
|
|
260
|
+
)}
|
|
261
|
+
{activeColumns.map((col) => (
|
|
262
|
+
<td key={col.key} className="px-4 py-3 text-sm text-white/70">
|
|
263
|
+
{col.render
|
|
264
|
+
? col.render(row[col.key], row)
|
|
265
|
+
: String(row[col.key] ?? '')}
|
|
266
|
+
</td>
|
|
267
|
+
))}
|
|
268
|
+
</tr>
|
|
269
|
+
);
|
|
270
|
+
})
|
|
271
|
+
)}
|
|
272
|
+
</tbody>
|
|
273
|
+
</table>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
{/* Pagination */}
|
|
277
|
+
<div className="px-4 py-3 border-t border-white/5 flex items-center justify-between">
|
|
278
|
+
<p className="text-sm text-white/40">
|
|
279
|
+
Showing {currentPage * pageSize + 1} to{' '}
|
|
280
|
+
{Math.min((currentPage + 1) * pageSize, sorted.length)} of {sorted.length} results
|
|
281
|
+
</p>
|
|
282
|
+
<div className="flex items-center gap-1">
|
|
283
|
+
<button
|
|
284
|
+
onClick={() => setPage(0)}
|
|
285
|
+
disabled={currentPage === 0}
|
|
286
|
+
className="px-2 py-1 text-sm text-white/50 hover:text-white disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
|
287
|
+
>
|
|
288
|
+
First
|
|
289
|
+
</button>
|
|
290
|
+
<button
|
|
291
|
+
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
|
292
|
+
disabled={currentPage === 0}
|
|
293
|
+
className="px-3 py-1.5 bg-white/5 border border-white/10 rounded text-sm text-white/70 hover:bg-white/10 disabled:opacity-30 disabled:cursor-not-allowed transition-all"
|
|
294
|
+
>
|
|
295
|
+
Prev
|
|
296
|
+
</button>
|
|
297
|
+
<span className="px-3 py-1.5 text-sm text-white/50">
|
|
298
|
+
{currentPage + 1} / {totalPages}
|
|
299
|
+
</span>
|
|
300
|
+
<button
|
|
301
|
+
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
|
|
302
|
+
disabled={currentPage >= totalPages - 1}
|
|
303
|
+
className="px-3 py-1.5 bg-white/5 border border-white/10 rounded text-sm text-white/70 hover:bg-white/10 disabled:opacity-30 disabled:cursor-not-allowed transition-all"
|
|
304
|
+
>
|
|
305
|
+
Next
|
|
306
|
+
</button>
|
|
307
|
+
<button
|
|
308
|
+
onClick={() => setPage(totalPages - 1)}
|
|
309
|
+
disabled={currentPage >= totalPages - 1}
|
|
310
|
+
className="px-2 py-1 text-sm text-white/50 hover:text-white disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
|
311
|
+
>
|
|
312
|
+
Last
|
|
313
|
+
</button>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
interface ExportCSVProps<T extends Record<string, unknown>> {
|
|
4
|
+
data: T[];
|
|
5
|
+
columns: { key: string; label: string }[];
|
|
6
|
+
filename?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function escapeCSVField(value: unknown): string {
|
|
10
|
+
const str = String(value ?? '');
|
|
11
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
12
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
13
|
+
}
|
|
14
|
+
return str;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function ExportCSV<T extends Record<string, unknown>>({
|
|
18
|
+
data,
|
|
19
|
+
columns,
|
|
20
|
+
filename = 'export',
|
|
21
|
+
}: ExportCSVProps<T>) {
|
|
22
|
+
const handleExport = () => {
|
|
23
|
+
const header = columns.map((col) => escapeCSVField(col.label)).join(',');
|
|
24
|
+
const rows = data.map((row) =>
|
|
25
|
+
columns.map((col) => escapeCSVField(row[col.key])).join(',')
|
|
26
|
+
);
|
|
27
|
+
const csv = [header, ...rows].join('\n');
|
|
28
|
+
|
|
29
|
+
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
30
|
+
const url = URL.createObjectURL(blob);
|
|
31
|
+
const link = document.createElement('a');
|
|
32
|
+
link.href = url;
|
|
33
|
+
link.download = `${filename}-${new Date().toISOString().slice(0, 10)}.csv`;
|
|
34
|
+
document.body.appendChild(link);
|
|
35
|
+
link.click();
|
|
36
|
+
document.body.removeChild(link);
|
|
37
|
+
URL.revokeObjectURL(url);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<button
|
|
42
|
+
onClick={handleExport}
|
|
43
|
+
disabled={data.length === 0}
|
|
44
|
+
className="flex items-center gap-2 px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-sm text-white/70 hover:bg-white/10 hover:text-white disabled:opacity-40 disabled:cursor-not-allowed transition-all"
|
|
45
|
+
>
|
|
46
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
47
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
48
|
+
</svg>
|
|
49
|
+
Export CSV
|
|
50
|
+
</button>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
interface SearchFilterProps {
|
|
4
|
+
value: string;
|
|
5
|
+
onChange: (value: string) => void;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function SearchFilter({
|
|
10
|
+
value,
|
|
11
|
+
onChange,
|
|
12
|
+
placeholder = 'Search...',
|
|
13
|
+
}: SearchFilterProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="relative">
|
|
16
|
+
<svg
|
|
17
|
+
className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-white/30"
|
|
18
|
+
fill="none"
|
|
19
|
+
viewBox="0 0 24 24"
|
|
20
|
+
stroke="currentColor"
|
|
21
|
+
strokeWidth={2}
|
|
22
|
+
>
|
|
23
|
+
<path
|
|
24
|
+
strokeLinecap="round"
|
|
25
|
+
strokeLinejoin="round"
|
|
26
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
27
|
+
/>
|
|
28
|
+
</svg>
|
|
29
|
+
<input
|
|
30
|
+
type="text"
|
|
31
|
+
value={value}
|
|
32
|
+
onChange={(e) => onChange(e.target.value)}
|
|
33
|
+
placeholder={placeholder}
|
|
34
|
+
className="w-full pl-10 pr-4 py-2 bg-white/5 border border-white/10 rounded-lg text-white text-sm placeholder-white/30 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 transition-all"
|
|
35
|
+
/>
|
|
36
|
+
{value && (
|
|
37
|
+
<button
|
|
38
|
+
onClick={() => onChange('')}
|
|
39
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-white/30 hover:text-white/60 transition-colors"
|
|
40
|
+
>
|
|
41
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
42
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
43
|
+
</svg>
|
|
44
|
+
</button>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "data-table",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Full-featured data table with sorting, pagination, search, column visibility, CSV export, and bulk actions",
|
|
5
|
+
"author": "AD AI Engine",
|
|
6
|
+
"category": "data",
|
|
7
|
+
"dependencies": [],
|
|
8
|
+
"files": {
|
|
9
|
+
"components": [
|
|
10
|
+
"components/table/DataTable.tsx",
|
|
11
|
+
"components/table/SearchFilter.tsx",
|
|
12
|
+
"components/table/ColumnToggle.tsx",
|
|
13
|
+
"components/table/ExportCSV.tsx",
|
|
14
|
+
"components/table/BulkActions.tsx"
|
|
15
|
+
],
|
|
16
|
+
"api": []
|
|
17
|
+
},
|
|
18
|
+
"schema": null,
|
|
19
|
+
"instructions": "1. Import DataTable and pass your columns and data\n2. DataTable integrates SearchFilter, ColumnToggle, ExportCSV, and BulkActions automatically\n3. Define columns with { key, label, sortable?, render? }\n4. Provide onBulkDelete callback for bulk delete functionality"
|
|
20
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as crypto from 'crypto';
|
|
5
|
+
|
|
6
|
+
const UPLOAD_DIR = path.join(process.cwd(), 'public', 'uploads');
|
|
7
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
8
|
+
const ALLOWED_TYPES = new Set([
|
|
9
|
+
'image/jpeg',
|
|
10
|
+
'image/png',
|
|
11
|
+
'image/gif',
|
|
12
|
+
'image/webp',
|
|
13
|
+
'application/pdf',
|
|
14
|
+
'text/csv',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function ensureUploadDir() {
|
|
18
|
+
if (!fs.existsSync(UPLOAD_DIR)) {
|
|
19
|
+
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function POST(request: NextRequest) {
|
|
24
|
+
try {
|
|
25
|
+
const formData = await request.formData();
|
|
26
|
+
const file = formData.get('file');
|
|
27
|
+
|
|
28
|
+
if (!file || !(file instanceof Blob)) {
|
|
29
|
+
return NextResponse.json(
|
|
30
|
+
{ error: 'No file provided' },
|
|
31
|
+
{ status: 400 }
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Validate size
|
|
36
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
37
|
+
return NextResponse.json(
|
|
38
|
+
{ error: `File too large. Maximum size: ${MAX_FILE_SIZE / 1024 / 1024}MB` },
|
|
39
|
+
{ status: 400 }
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Validate type
|
|
44
|
+
if (ALLOWED_TYPES.size > 0 && !ALLOWED_TYPES.has(file.type)) {
|
|
45
|
+
return NextResponse.json(
|
|
46
|
+
{ error: `File type ${file.type} is not allowed` },
|
|
47
|
+
{ status: 400 }
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
ensureUploadDir();
|
|
52
|
+
|
|
53
|
+
// Generate safe filename
|
|
54
|
+
const originalName = file instanceof File ? file.name : 'upload';
|
|
55
|
+
const ext = path.extname(originalName);
|
|
56
|
+
const hash = crypto.randomUUID().replace(/-/g, '').slice(0, 16);
|
|
57
|
+
const filename = `${Date.now()}-${hash}${ext}`;
|
|
58
|
+
const filePath = path.join(UPLOAD_DIR, filename);
|
|
59
|
+
|
|
60
|
+
// Write file
|
|
61
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
62
|
+
await fs.promises.writeFile(filePath, buffer);
|
|
63
|
+
|
|
64
|
+
return NextResponse.json({
|
|
65
|
+
message: 'File uploaded successfully',
|
|
66
|
+
filename,
|
|
67
|
+
originalName,
|
|
68
|
+
size: file.size,
|
|
69
|
+
type: file.type,
|
|
70
|
+
url: `/uploads/${filename}`,
|
|
71
|
+
});
|
|
72
|
+
} catch (error: unknown) {
|
|
73
|
+
const message = error instanceof Error ? error.message : 'Upload failed';
|
|
74
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function DELETE(request: NextRequest) {
|
|
79
|
+
try {
|
|
80
|
+
const { filename } = await request.json();
|
|
81
|
+
|
|
82
|
+
if (!filename || typeof filename !== 'string') {
|
|
83
|
+
return NextResponse.json(
|
|
84
|
+
{ error: 'Filename is required' },
|
|
85
|
+
{ status: 400 }
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Prevent path traversal
|
|
90
|
+
const safeName = path.basename(filename);
|
|
91
|
+
const filePath = path.join(UPLOAD_DIR, safeName);
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(filePath)) {
|
|
94
|
+
return NextResponse.json(
|
|
95
|
+
{ error: 'File not found' },
|
|
96
|
+
{ status: 404 }
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await fs.promises.unlink(filePath);
|
|
101
|
+
|
|
102
|
+
return NextResponse.json({ message: 'File deleted successfully' });
|
|
103
|
+
} catch (error: unknown) {
|
|
104
|
+
const message = error instanceof Error ? error.message : 'Delete failed';
|
|
105
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
106
|
+
}
|
|
107
|
+
}
|