nl-d365boilerplate-vite 1.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.
Files changed (46) hide show
  1. package/.env.example +22 -0
  2. package/README.md +75 -0
  3. package/bin/cli.js +84 -0
  4. package/components.json +22 -0
  5. package/eslint.config.js +23 -0
  6. package/getToken.js +197 -0
  7. package/index.html +30 -0
  8. package/package.json +69 -0
  9. package/src/App.tsx +28 -0
  10. package/src/assets/images/novalogica-logo.svg +24 -0
  11. package/src/components/nl-header.tsx +165 -0
  12. package/src/components/page-layout.tsx +78 -0
  13. package/src/components/page-render.tsx +16 -0
  14. package/src/components/ui/alert.tsx +66 -0
  15. package/src/components/ui/badge.tsx +49 -0
  16. package/src/components/ui/button.tsx +165 -0
  17. package/src/components/ui/card.tsx +92 -0
  18. package/src/components/ui/dialog.tsx +156 -0
  19. package/src/components/ui/input.tsx +23 -0
  20. package/src/components/ui/label.tsx +23 -0
  21. package/src/components/ui/separator.tsx +26 -0
  22. package/src/components/ui/table.tsx +116 -0
  23. package/src/components/ui/tabs.tsx +91 -0
  24. package/src/components/ui/theme-toggle.tsx +28 -0
  25. package/src/config/pages.config.ts +34 -0
  26. package/src/contexts/dataverse-context.tsx +12 -0
  27. package/src/contexts/navigation-context.tsx +14 -0
  28. package/src/hooks/useAccounts.ts +194 -0
  29. package/src/hooks/useDataverse.ts +11 -0
  30. package/src/hooks/useNavigation.ts +41 -0
  31. package/src/index.css +147 -0
  32. package/src/lib/nav-items.ts +25 -0
  33. package/src/lib/utils.ts +6 -0
  34. package/src/main.tsx +12 -0
  35. package/src/pages/Demo.tsx +465 -0
  36. package/src/pages/Documentation.tsx +850 -0
  37. package/src/pages/Home.tsx +132 -0
  38. package/src/pages/index.ts +4 -0
  39. package/src/providers/dataverse-provider.tsx +81 -0
  40. package/src/providers/navigation-provider.tsx +33 -0
  41. package/src/providers/theme-provider.tsx +92 -0
  42. package/src/public/novalogica-logo.svg +24 -0
  43. package/tsconfig.app.json +32 -0
  44. package/tsconfig.json +17 -0
  45. package/tsconfig.node.json +26 -0
  46. package/vite.config.ts +26 -0
@@ -0,0 +1,465 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useAccounts, type Account } from "@/hooks/useAccounts";
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from "@/components/ui/card";
10
+ import { Button } from "@/components/ui/button";
11
+ import { Input } from "@/components/ui/input";
12
+ import { Label } from "@/components/ui/label";
13
+ import {
14
+ Table,
15
+ TableBody,
16
+ TableCell,
17
+ TableHead,
18
+ TableHeader,
19
+ TableRow,
20
+ } from "@/components/ui/table";
21
+ import {
22
+ Dialog,
23
+ DialogContent,
24
+ DialogDescription,
25
+ DialogFooter,
26
+ DialogHeader,
27
+ DialogTitle,
28
+ } from "@/components/ui/dialog";
29
+ import { Alert, AlertDescription } from "@/components/ui/alert";
30
+ import {
31
+ Loading03Icon,
32
+ Add01Icon,
33
+ PencilEdit01Icon,
34
+ Delete02Icon,
35
+ Cancel01Icon,
36
+ ReloadIcon,
37
+ TestTube01Icon,
38
+ } from "hugeicons-react";
39
+ import { PageLayout } from "@/components/page-layout";
40
+
41
+ const Demo = () => {
42
+ const {
43
+ accounts,
44
+ loading,
45
+ error,
46
+ isReady,
47
+ fetchAccounts,
48
+ createAccount,
49
+ updateAccount,
50
+ deleteAccount,
51
+ clearError,
52
+ } = useAccounts();
53
+
54
+ const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
55
+ const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
56
+ const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
57
+ const [searchTerm, setSearchTerm] = useState("");
58
+
59
+ const [formData, setFormData] = useState<Partial<Account>>({
60
+ name: "",
61
+ emailaddress1: "",
62
+ telephone1: "",
63
+ websiteurl: "",
64
+ address1_city: "",
65
+ });
66
+
67
+ useEffect(() => {
68
+ if (isReady) {
69
+ fetchAccounts();
70
+ }
71
+ }, [isReady, fetchAccounts]);
72
+
73
+ const handleInputChange = (field: keyof Account, value: string) => {
74
+ setFormData((prev: Partial<Account>) => ({ ...prev, [field]: value }));
75
+ };
76
+
77
+ const handleCreate = async () => {
78
+ if (!formData.name) {
79
+ alert("Account name is required");
80
+ return;
81
+ }
82
+
83
+ const accountId = await createAccount(formData);
84
+ if (accountId) {
85
+ setIsCreateDialogOpen(false);
86
+ resetForm();
87
+ }
88
+ };
89
+
90
+ const handleUpdate = async () => {
91
+ if (!selectedAccount?.accountid) return;
92
+
93
+ const success = await updateAccount(selectedAccount.accountid, formData);
94
+ if (success) {
95
+ setIsEditDialogOpen(false);
96
+ setSelectedAccount(null);
97
+ resetForm();
98
+ }
99
+ };
100
+
101
+ const handleDelete = async (accountId: string) => {
102
+ if (!confirm("Are you sure you want to delete this account?")) return;
103
+
104
+ await deleteAccount(accountId);
105
+ };
106
+
107
+ const filteredAccounts = searchTerm
108
+ ? accounts.filter((account: Account) =>
109
+ account.name?.toLowerCase().includes(searchTerm.toLowerCase()),
110
+ )
111
+ : accounts;
112
+
113
+ const resetForm = () => {
114
+ setFormData({
115
+ name: "",
116
+ emailaddress1: "",
117
+ telephone1: "",
118
+ websiteurl: "",
119
+ address1_city: "",
120
+ });
121
+ };
122
+
123
+ const openEditDialog = (account: Account) => {
124
+ setSelectedAccount(account);
125
+ setFormData({
126
+ name: account.name || "",
127
+ emailaddress1: account.emailaddress1 || "",
128
+ telephone1: account.telephone1 || "",
129
+ websiteurl: account.websiteurl || "",
130
+ address1_city: account.address1_city || "",
131
+ });
132
+ setIsEditDialogOpen(true);
133
+ };
134
+
135
+ const formatDate = (dateString?: string) => {
136
+ if (!dateString) return "N/A";
137
+ return new Date(dateString).toLocaleDateString("en-US", {
138
+ year: "numeric",
139
+ month: "short",
140
+ day: "numeric",
141
+ });
142
+ };
143
+
144
+ return (
145
+ <PageLayout
146
+ title="Accounts Management"
147
+ description="Comprehensive CRUD operations demo"
148
+ icon={TestTube01Icon}
149
+ toolbar={
150
+ <Button
151
+ leftIcon={Add01Icon}
152
+ onClick={() => setIsCreateDialogOpen(true)}
153
+ disabled={!isReady}
154
+ >
155
+ Create Account
156
+ </Button>
157
+ }
158
+ >
159
+ <div className="space-y-6">
160
+ {error && (
161
+ <Alert variant="destructive">
162
+ <AlertDescription className="flex justify-between items-center">
163
+ <span>{error}</span>
164
+ <Button variant="ghost" size="sm" onClick={clearError}>
165
+ Dismiss
166
+ </Button>
167
+ </AlertDescription>
168
+ </Alert>
169
+ )}
170
+
171
+ <Card>
172
+ <CardHeader>
173
+ <CardTitle>Search Accounts</CardTitle>
174
+ <CardDescription>
175
+ Search accounts by name or view all accounts
176
+ </CardDescription>
177
+ </CardHeader>
178
+ <CardContent>
179
+ <div className="flex gap-2">
180
+ <div className="flex-1">
181
+ <Input
182
+ placeholder="Search by account name..."
183
+ value={searchTerm}
184
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
185
+ setSearchTerm(e.target.value)
186
+ }
187
+ />
188
+ </div>
189
+ <Button
190
+ variant="outline"
191
+ leftIcon={Cancel01Icon}
192
+ onClick={() => setSearchTerm("")}
193
+ >
194
+ Clear
195
+ </Button>
196
+ <Button
197
+ leftIcon={ReloadIcon}
198
+ isLoading={loading}
199
+ onClick={fetchAccounts}
200
+ >
201
+ Refresh
202
+ </Button>
203
+ </div>
204
+ </CardContent>
205
+ </Card>
206
+
207
+ <Card>
208
+ <CardHeader>
209
+ <CardTitle>
210
+ Accounts ({filteredAccounts.length}
211
+ {searchTerm && ` of ${accounts.length}`})
212
+ </CardTitle>
213
+ <CardDescription>
214
+ {isReady
215
+ ? "Active accounts in your Dynamics 365 environment"
216
+ : "Connecting to Dynamics 365..."}
217
+ </CardDescription>
218
+ </CardHeader>
219
+ <CardContent>
220
+ {loading ? (
221
+ <div className="flex justify-center items-center py-8">
222
+ <Loading03Icon className="h-8 w-8 animate-spin text-primary" />
223
+ </div>
224
+ ) : filteredAccounts.length === 0 ? (
225
+ <div className="text-center py-8 text-muted-foreground">
226
+ {searchTerm
227
+ ? "No accounts match your search. Try a different term."
228
+ : "No accounts found. Create your first account to get started."}
229
+ </div>
230
+ ) : (
231
+ <div className="overflow-x-auto">
232
+ <Table>
233
+ <TableHeader>
234
+ <TableRow>
235
+ <TableHead>Name</TableHead>
236
+ <TableHead>Email</TableHead>
237
+ <TableHead>Phone</TableHead>
238
+ <TableHead>City</TableHead>
239
+ <TableHead>Created On</TableHead>
240
+ <TableHead className="text-right">Actions</TableHead>
241
+ </TableRow>
242
+ </TableHeader>
243
+ <TableBody>
244
+ {filteredAccounts.map((account: Account) => (
245
+ <TableRow key={account.accountid}>
246
+ <TableCell className="font-medium">
247
+ {account.name || "N/A"}
248
+ </TableCell>
249
+ <TableCell>{account.emailaddress1 || "N/A"}</TableCell>
250
+ <TableCell>{account.telephone1 || "N/A"}</TableCell>
251
+ <TableCell>{account.address1_city || "N/A"}</TableCell>
252
+ <TableCell>{formatDate(account.createdon)}</TableCell>
253
+ <TableCell className="text-right">
254
+ <div className="flex justify-end gap-2">
255
+ <Button
256
+ variant="outline"
257
+ size="icon"
258
+ onClick={() => openEditDialog(account)}
259
+ >
260
+ <PencilEdit01Icon />
261
+ </Button>
262
+ <Button
263
+ variant="destructive"
264
+ size="icon"
265
+ onClick={() =>
266
+ handleDelete(account.accountid || "")
267
+ }
268
+ >
269
+ <Delete02Icon />
270
+ </Button>
271
+ </div>
272
+ </TableCell>
273
+ </TableRow>
274
+ ))}
275
+ </TableBody>
276
+ </Table>
277
+ </div>
278
+ )}
279
+ </CardContent>
280
+ </Card>
281
+
282
+ <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
283
+ <DialogContent className="sm:max-w-[600px]">
284
+ <DialogHeader>
285
+ <DialogTitle>Create New Account</DialogTitle>
286
+ <DialogDescription>
287
+ Fill in the details to create a new account in Dynamics 365.
288
+ </DialogDescription>
289
+ </DialogHeader>
290
+ <div className="grid gap-4 py-4">
291
+ <div className="grid gap-2">
292
+ <Label htmlFor="create-name">
293
+ Account Name <span className="text-destructive">*</span>
294
+ </Label>
295
+ <Input
296
+ id="create-name"
297
+ value={formData.name}
298
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
299
+ handleInputChange("name", e.target.value)
300
+ }
301
+ placeholder="Enter account name"
302
+ />
303
+ </div>
304
+ <div className="grid gap-2">
305
+ <Label htmlFor="create-email">Email Address</Label>
306
+ <Input
307
+ id="create-email"
308
+ type="email"
309
+ value={formData.emailaddress1}
310
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
311
+ handleInputChange("emailaddress1", e.target.value)
312
+ }
313
+ placeholder="email@example.com"
314
+ />
315
+ </div>
316
+ <div className="grid gap-2">
317
+ <Label htmlFor="create-phone">Phone Number</Label>
318
+ <Input
319
+ id="create-phone"
320
+ type="tel"
321
+ value={formData.telephone1}
322
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
323
+ handleInputChange("telephone1", e.target.value)
324
+ }
325
+ placeholder="+1 (555) 000-0000"
326
+ />
327
+ </div>
328
+ <div className="grid gap-2">
329
+ <Label htmlFor="create-website">Website URL</Label>
330
+ <Input
331
+ id="create-website"
332
+ type="url"
333
+ value={formData.websiteurl}
334
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
335
+ handleInputChange("websiteurl", e.target.value)
336
+ }
337
+ placeholder="https://example.com"
338
+ />
339
+ </div>
340
+ <div className="grid gap-2">
341
+ <Label htmlFor="create-city">City</Label>
342
+ <Input
343
+ id="create-city"
344
+ value={formData.address1_city}
345
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
346
+ handleInputChange("address1_city", e.target.value)
347
+ }
348
+ placeholder="Enter city"
349
+ />
350
+ </div>
351
+ </div>
352
+ <DialogFooter>
353
+ <Button
354
+ variant="outline"
355
+ onClick={() => {
356
+ setIsCreateDialogOpen(false);
357
+ resetForm();
358
+ }}
359
+ >
360
+ Cancel
361
+ </Button>
362
+ <Button
363
+ onClick={handleCreate}
364
+ isLoading={loading}
365
+ leftIcon={Add01Icon}
366
+ >
367
+ Create Account
368
+ </Button>
369
+ </DialogFooter>
370
+ </DialogContent>
371
+ </Dialog>
372
+
373
+ <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
374
+ <DialogContent className="sm:max-w-[600px]">
375
+ <DialogHeader>
376
+ <DialogTitle>Edit Account</DialogTitle>
377
+ <DialogDescription>
378
+ Update the account details in Dynamics 365.
379
+ </DialogDescription>
380
+ </DialogHeader>
381
+ <div className="grid gap-4 py-4">
382
+ <div className="grid gap-2">
383
+ <Label htmlFor="edit-name">
384
+ Account Name <span className="text-destructive">*</span>
385
+ </Label>
386
+ <Input
387
+ id="edit-name"
388
+ value={formData.name}
389
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
390
+ handleInputChange("name", e.target.value)
391
+ }
392
+ placeholder="Enter account name"
393
+ />
394
+ </div>
395
+ <div className="grid gap-2">
396
+ <Label htmlFor="edit-email">Email Address</Label>
397
+ <Input
398
+ id="edit-email"
399
+ type="email"
400
+ value={formData.emailaddress1}
401
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
402
+ handleInputChange("emailaddress1", e.target.value)
403
+ }
404
+ placeholder="email@example.com"
405
+ />
406
+ </div>
407
+ <div className="grid gap-2">
408
+ <Label htmlFor="edit-phone">Phone Number</Label>
409
+ <Input
410
+ id="edit-phone"
411
+ type="tel"
412
+ value={formData.telephone1}
413
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
414
+ handleInputChange("telephone1", e.target.value)
415
+ }
416
+ placeholder="+1 (555) 000-0000"
417
+ />
418
+ </div>
419
+ <div className="grid gap-2">
420
+ <Label htmlFor="edit-website">Website URL</Label>
421
+ <Input
422
+ id="edit-website"
423
+ type="url"
424
+ value={formData.websiteurl}
425
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
426
+ handleInputChange("websiteurl", e.target.value)
427
+ }
428
+ placeholder="https://example.com"
429
+ />
430
+ </div>
431
+ <div className="grid gap-2">
432
+ <Label htmlFor="edit-city">City</Label>
433
+ <Input
434
+ id="edit-city"
435
+ value={formData.address1_city}
436
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
437
+ handleInputChange("address1_city", e.target.value)
438
+ }
439
+ placeholder="Enter city"
440
+ />
441
+ </div>
442
+ </div>
443
+ <DialogFooter>
444
+ <Button
445
+ variant="outline"
446
+ onClick={() => {
447
+ setIsEditDialogOpen(false);
448
+ setSelectedAccount(null);
449
+ resetForm();
450
+ }}
451
+ >
452
+ Cancel
453
+ </Button>
454
+ <Button onClick={handleUpdate} isLoading={loading}>
455
+ Update Account
456
+ </Button>
457
+ </DialogFooter>
458
+ </DialogContent>
459
+ </Dialog>
460
+ </div>
461
+ </PageLayout>
462
+ );
463
+ };
464
+
465
+ export default Demo;