@everystack/admin 0.1.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.
Files changed (38) hide show
  1. package/README.md +282 -0
  2. package/package.json +53 -0
  3. package/src/AdminRoot.tsx +34 -0
  4. package/src/adapters/EverystackAdapter.ts +218 -0
  5. package/src/adapters/types.ts +78 -0
  6. package/src/components/AvatarFieldInput.tsx +100 -0
  7. package/src/components/ImageDisplay.tsx +57 -0
  8. package/src/components/ImageFieldInput.tsx +140 -0
  9. package/src/components/dashboard/Brush.tsx +139 -0
  10. package/src/components/dashboard/DimensionBars.tsx +67 -0
  11. package/src/components/dashboard/DimensionDonut.tsx +98 -0
  12. package/src/components/dashboard/EventTable.tsx +248 -0
  13. package/src/components/dashboard/StatDials.tsx +67 -0
  14. package/src/components/dashboard/TimeHistogram.tsx +158 -0
  15. package/src/context.ts +75 -0
  16. package/src/hooks/useAdminConfig.ts +22 -0
  17. package/src/hooks/useAdminData.ts +132 -0
  18. package/src/hooks/useAggregates.ts +290 -0
  19. package/src/hooks/useRpc.ts +124 -0
  20. package/src/hooks/useStorageConfig.ts +16 -0
  21. package/src/index.ts +138 -0
  22. package/src/presets/twitter.ts +267 -0
  23. package/src/providers/AdminConfigProvider.tsx +18 -0
  24. package/src/providers/DataAdapterProvider.tsx +18 -0
  25. package/src/rpc/resource-dashboard-rpc.ts +416 -0
  26. package/src/screens/AdminDashboard.tsx +217 -0
  27. package/src/screens/AdminLayout.tsx +97 -0
  28. package/src/screens/ResourceCreate.tsx +243 -0
  29. package/src/screens/ResourceDashboard.tsx +278 -0
  30. package/src/screens/ResourceEdit.tsx +283 -0
  31. package/src/screens/ResourceList.tsx +153 -0
  32. package/src/screens/ResourceShow.tsx +171 -0
  33. package/src/screens/index.ts +14 -0
  34. package/src/types.ts +358 -0
  35. package/src/utils/naming.ts +105 -0
  36. package/src/utils/pick-image-web.ts +81 -0
  37. package/src/utils/resolve-image-url.ts +43 -0
  38. package/src/utils/upload-file.ts +93 -0
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Twitter Admin Preset
3
+ *
4
+ * Pre-built admin configuration for the example Twitter clone app.
5
+ * Covers profiles, posts, likes, and follows resources matching
6
+ * the example app's Drizzle schema.
7
+ */
8
+
9
+ import type { AdminConfig, ResourceConfig, ResourceDashboardConfig, DashboardConfig } from '../types';
10
+
11
+ // =============================================================================
12
+ // Resources
13
+ // =============================================================================
14
+
15
+ const profilesDashboard: ResourceDashboardConfig = {
16
+ title: 'User Growth',
17
+ timestampColumn: 'created_at',
18
+ defaultHours: 720, // 30 days
19
+ bucketSize: 'day',
20
+ stats: [
21
+ { id: 'total', label: 'Users', color: '#1d9bf0', value: { type: 'count' } },
22
+ { id: 'posts', label: 'Posts', color: '#00ba7c', value: { type: 'table_count', table: 'posts', timestampColumn: 'created_at' } },
23
+ { id: 'engagement', label: 'Likes', color: '#f91880', value: { type: 'table_count', table: 'likes', timestampColumn: 'created_at' } },
24
+ { id: 'connections', label: 'Follows', color: '#7856ff', value: { type: 'table_count', table: 'follows', timestampColumn: 'created_at' } },
25
+ ],
26
+ dimensions: [],
27
+ listColumns: ['created_at', 'username', 'display_name'],
28
+ searchColumns: ['username', 'display_name'],
29
+ linkToDetail: true,
30
+ };
31
+
32
+ const profilesResource: ResourceConfig = {
33
+ name: 'profiles',
34
+ label: 'Profile',
35
+ labelPlural: 'Profiles',
36
+ icon: 'person',
37
+ primaryKey: 'id',
38
+ actions: { list: true, show: true, create: false, edit: true, delete: false },
39
+ fields: [
40
+ { name: 'id', type: 'string', label: 'ID' },
41
+ { name: 'username', type: 'string', label: 'Username', sortable: true, searchable: true },
42
+ { name: 'display_name', type: 'string', label: 'Display Name', sortable: true, searchable: true },
43
+ { name: 'bio', type: 'string', label: 'Bio' },
44
+ { name: 'avatar_url', type: 'image', label: 'Avatar', renderer: 'avatar' },
45
+ { name: 'created_at', type: 'datetime', label: 'Joined', sortable: true },
46
+ ],
47
+ listFields: ['username', 'display_name', 'bio', 'created_at'],
48
+ editFields: ['username', 'display_name', 'bio', 'avatar_url'],
49
+ defaultSort: { field: 'created_at', order: 'desc' },
50
+ dashboard: profilesDashboard,
51
+ };
52
+
53
+ const postsDashboard: ResourceDashboardConfig = {
54
+ title: 'Content Velocity',
55
+ timestampColumn: 'created_at',
56
+ defaultHours: 720, // 30 days
57
+ bucketSize: 'day',
58
+ stats: [
59
+ { id: 'total', label: 'Posts', color: '#0f172a', value: { type: 'count' } },
60
+ { id: 'authors', label: 'Authors', color: '#1d9bf0', value: { type: 'distinct', column: 'author_id' } },
61
+ { id: 'engagement', label: 'Likes', color: '#f91880', value: { type: 'table_count', table: 'likes', timestampColumn: 'created_at' } },
62
+ ],
63
+ dimensions: [],
64
+ listColumns: ['created_at', 'author_id', 'body'],
65
+ searchColumns: ['body'],
66
+ linkToDetail: true,
67
+ };
68
+
69
+ const postsResource: ResourceConfig = {
70
+ name: 'posts',
71
+ label: 'Post',
72
+ labelPlural: 'Posts',
73
+ icon: 'chatbubble',
74
+ primaryKey: 'id',
75
+ actions: { list: true, show: true, create: false, edit: false, delete: true },
76
+ fields: [
77
+ { name: 'id', type: 'string', label: 'ID' },
78
+ {
79
+ name: 'author_id',
80
+ type: 'relation',
81
+ label: 'Author',
82
+ relation: { resource: 'profiles', field: 'id', displayField: 'username' },
83
+ },
84
+ { name: 'body', type: 'string', label: 'Body', searchable: true },
85
+ { name: 'created_at', type: 'datetime', label: 'Posted', sortable: true },
86
+ ],
87
+ listFields: ['author_id', 'body', 'created_at'],
88
+ defaultSort: { field: 'created_at', order: 'desc' },
89
+ dashboard: postsDashboard,
90
+ };
91
+
92
+ const likesDashboard: ResourceDashboardConfig = {
93
+ title: 'Engagement',
94
+ timestampColumn: 'created_at',
95
+ defaultHours: 720, // 30 days
96
+ bucketSize: 'day',
97
+ primaryKey: ['user_id', 'post_id'],
98
+ stats: [
99
+ { id: 'total', label: 'Likes', color: '#f91880', value: { type: 'count' } },
100
+ { id: 'users', label: 'Likers', color: '#1d9bf0', value: { type: 'distinct', column: 'user_id' } },
101
+ { id: 'posts', label: 'Posts Liked', color: '#00ba7c', value: { type: 'distinct', column: 'post_id' } },
102
+ ],
103
+ dimensions: [],
104
+ listColumns: ['created_at', 'user_id', 'post_id'],
105
+ linkToDetail: false,
106
+ };
107
+
108
+ const likesResource: ResourceConfig = {
109
+ name: 'likes',
110
+ label: 'Like',
111
+ labelPlural: 'Likes',
112
+ icon: 'heart',
113
+ primaryKey: 'user_id',
114
+ actions: { list: true, show: false, create: false, edit: false, delete: false },
115
+ fields: [
116
+ {
117
+ name: 'user_id',
118
+ type: 'relation',
119
+ label: 'User',
120
+ relation: { resource: 'profiles', field: 'id', displayField: 'username' },
121
+ },
122
+ {
123
+ name: 'post_id',
124
+ type: 'relation',
125
+ label: 'Post',
126
+ relation: { resource: 'posts', field: 'id' },
127
+ },
128
+ { name: 'created_at', type: 'datetime', label: 'Liked At', sortable: true },
129
+ ],
130
+ listFields: ['user_id', 'post_id', 'created_at'],
131
+ defaultSort: { field: 'created_at', order: 'desc' },
132
+ dashboard: likesDashboard,
133
+ };
134
+
135
+ const followsDashboard: ResourceDashboardConfig = {
136
+ title: 'Network Growth',
137
+ timestampColumn: 'created_at',
138
+ defaultHours: 720, // 30 days
139
+ bucketSize: 'day',
140
+ primaryKey: ['follower_id', 'following_id'],
141
+ stats: [
142
+ { id: 'total', label: 'Follows', color: '#7856ff', value: { type: 'count' } },
143
+ { id: 'followers', label: 'Followers', color: '#1d9bf0', value: { type: 'distinct', column: 'follower_id' } },
144
+ { id: 'following', label: 'Following', color: '#00ba7c', value: { type: 'distinct', column: 'following_id' } },
145
+ ],
146
+ dimensions: [],
147
+ listColumns: ['created_at', 'follower_id', 'following_id'],
148
+ linkToDetail: false,
149
+ };
150
+
151
+ const followsResource: ResourceConfig = {
152
+ name: 'follows',
153
+ label: 'Follow',
154
+ labelPlural: 'Follows',
155
+ icon: 'people',
156
+ primaryKey: 'follower_id',
157
+ actions: { list: true, show: false, create: false, edit: false, delete: false },
158
+ fields: [
159
+ {
160
+ name: 'follower_id',
161
+ type: 'relation',
162
+ label: 'Follower',
163
+ relation: { resource: 'profiles', field: 'id', displayField: 'username' },
164
+ },
165
+ {
166
+ name: 'following_id',
167
+ type: 'relation',
168
+ label: 'Following',
169
+ relation: { resource: 'profiles', field: 'id', displayField: 'username' },
170
+ },
171
+ { name: 'created_at', type: 'datetime', label: 'Followed At', sortable: true },
172
+ ],
173
+ listFields: ['follower_id', 'following_id', 'created_at'],
174
+ defaultSort: { field: 'created_at', order: 'desc' },
175
+ dashboard: followsDashboard,
176
+ };
177
+
178
+ // =============================================================================
179
+ // Dashboard
180
+ // =============================================================================
181
+
182
+ const overviewDashboard: DashboardConfig = {
183
+ id: 'overview',
184
+ title: 'Overview',
185
+ description: 'Platform activity at a glance',
186
+ refreshInterval: 60,
187
+ widgets: [
188
+ {
189
+ id: 'total-users',
190
+ type: 'stat',
191
+ title: 'Total Users',
192
+ data: { resource: 'profiles', aggregation: 'count' },
193
+ grid: { x: 0, y: 0, width: 1, height: 1 },
194
+ },
195
+ {
196
+ id: 'total-posts',
197
+ type: 'stat',
198
+ title: 'Total Posts',
199
+ data: { resource: 'posts', aggregation: 'count' },
200
+ grid: { x: 1, y: 0, width: 1, height: 1 },
201
+ },
202
+ {
203
+ id: 'total-likes',
204
+ type: 'stat',
205
+ title: 'Total Likes',
206
+ data: { resource: 'likes', aggregation: 'count' },
207
+ grid: { x: 2, y: 0, width: 1, height: 1 },
208
+ },
209
+ {
210
+ id: 'total-follows',
211
+ type: 'stat',
212
+ title: 'Total Follows',
213
+ data: { resource: 'follows', aggregation: 'count' },
214
+ grid: { x: 3, y: 0, width: 1, height: 1 },
215
+ },
216
+ ],
217
+ };
218
+
219
+ // =============================================================================
220
+ // Exported Config
221
+ // =============================================================================
222
+
223
+ /**
224
+ * Twitter Admin Configuration
225
+ *
226
+ * Pre-built configuration for the example Twitter clone app.
227
+ * Pass this to AdminRoot or AdminConfigProvider.
228
+ *
229
+ * @example
230
+ * ```tsx
231
+ * import { AdminRoot, EverystackAdapter } from 'everystack/admin';
232
+ * import { twitterAdminConfig } from 'everystack/admin/presets/twitter';
233
+ *
234
+ * const adapter = new EverystackAdapter({ baseUrl: '/api' });
235
+ *
236
+ * export default function AdminLayout() {
237
+ * return (
238
+ * <AdminRoot config={twitterAdminConfig} adapter={adapter}>
239
+ * <Slot />
240
+ * </AdminRoot>
241
+ * );
242
+ * }
243
+ * ```
244
+ */
245
+ export const twitterAdminConfig: AdminConfig = {
246
+ title: 'Twitter Admin',
247
+ subtitle: 'Content & User Management',
248
+ basePath: '/admin',
249
+ theme: {
250
+ primaryColor: '#1d9bf0',
251
+ backgroundColor: '#f7f9f9',
252
+ textColor: '#0f1419',
253
+ accentColor: '#1d9bf0',
254
+ },
255
+ resources: [profilesResource, postsResource, likesResource, followsResource],
256
+ dashboards: [overviewDashboard],
257
+ menu: [
258
+ { id: 'overview', label: 'Overview', dashboard: 'overview' },
259
+ { id: 'profiles', label: 'Users', icon: 'person', resource: 'profiles' },
260
+ { id: 'posts', label: 'Posts', icon: 'chatbubble', resource: 'posts' },
261
+ { id: 'likes', label: 'Likes', icon: 'heart', resource: 'likes' },
262
+ { id: 'follows', label: 'Follows', icon: 'people', resource: 'follows' },
263
+ ],
264
+ };
265
+
266
+ /** Individual resource configs for selective use */
267
+ export { profilesResource, postsResource, likesResource, followsResource };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * AdminConfigProvider
3
+ *
4
+ * Provides AdminConfig to the admin component tree.
5
+ */
6
+
7
+ import React from 'react';
8
+ import type { AdminConfig } from '../types';
9
+ import { AdminConfigContext } from '../hooks/useAdminConfig';
10
+
11
+ export interface AdminConfigProviderProps {
12
+ config: AdminConfig;
13
+ children: React.ReactNode;
14
+ }
15
+
16
+ export function AdminConfigProvider({ config, children }: AdminConfigProviderProps) {
17
+ return <AdminConfigContext.Provider value={{ config }}>{children}</AdminConfigContext.Provider>;
18
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * DataAdapterProvider
3
+ *
4
+ * Provides the DataAdapter to the admin component tree.
5
+ */
6
+
7
+ import React from 'react';
8
+ import type { DataAdapter } from '../adapters/types';
9
+ import { DataAdapterContext } from '../hooks/useAdminData';
10
+
11
+ export interface DataAdapterProviderProps {
12
+ adapter: DataAdapter;
13
+ children: React.ReactNode;
14
+ }
15
+
16
+ export function DataAdapterProvider({ adapter, children }: DataAdapterProviderProps) {
17
+ return <DataAdapterContext.Provider value={{ adapter }}>{children}</DataAdapterContext.Provider>;
18
+ }