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,850 @@
1
+ import { useState } from "react";
2
+ import { Button } from "@/components/ui/button";
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from "@/components/ui/card";
10
+ import { Badge } from "@/components/ui/badge";
11
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
12
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
13
+ import {
14
+ ArrowLeft01Icon,
15
+ Copy01Icon,
16
+ Tick01Icon,
17
+ File01Icon,
18
+ Navigation01Icon,
19
+ Database01Icon,
20
+ PuzzleIcon,
21
+ Add01Icon,
22
+ Delete02Icon,
23
+ PencilEdit01Icon,
24
+ PlayIcon,
25
+ BulbIcon,
26
+ CodeIcon,
27
+ BookOpen01Icon,
28
+ FolderOpenIcon,
29
+ } from "hugeicons-react";
30
+ import { Separator } from "@/components/ui/separator";
31
+ import { useNavigation } from "@/hooks/useNavigation";
32
+ import { PAGES } from "@/config/pages.config";
33
+ import { PageLayout } from "@/components/page-layout";
34
+
35
+ interface CodeBlockProps {
36
+ code: string;
37
+ title?: string;
38
+ onCopy: (code: string, id: string) => void;
39
+ isCopied: boolean;
40
+ id: string;
41
+ }
42
+
43
+ const CodeBlock = ({ code, title, onCopy, isCopied, id }: CodeBlockProps) => (
44
+ <div className="relative group">
45
+ <div className="bg-muted/30 rounded-lg border">
46
+ {title && (
47
+ <div className="px-4 pt-3 pb-2 border-b">
48
+ <span className="text-sm font-medium text-muted-foreground">
49
+ {title}
50
+ </span>
51
+ </div>
52
+ )}
53
+ <div className="p-4 font-mono text-sm overflow-x-auto">
54
+ <pre className="whitespace-pre-wrap">{code}</pre>
55
+ </div>
56
+ <Button
57
+ size="icon"
58
+ variant="ghost"
59
+ className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity h-8 w-8"
60
+ onClick={() => onCopy(code, id)}
61
+ >
62
+ {isCopied ? (
63
+ <Tick01Icon className="w-4 h-4 text-green-500" />
64
+ ) : (
65
+ <Copy01Icon className="w-4 h-4" />
66
+ )}
67
+ </Button>
68
+ </div>
69
+ </div>
70
+ );
71
+
72
+ interface StepCardProps {
73
+ number: number;
74
+ title: string;
75
+ children: React.ReactNode;
76
+ }
77
+
78
+ const StepCard = ({ number, title, children }: StepCardProps) => (
79
+ <Card>
80
+ <CardHeader className="pb-3">
81
+ <div className="flex items-center gap-3">
82
+ <div className="flex items-center justify-center w-7 h-7 rounded-full bg-primary text-primary-foreground text-sm font-semibold">
83
+ {number}
84
+ </div>
85
+ <CardTitle className="text-base">{title}</CardTitle>
86
+ </div>
87
+ </CardHeader>
88
+ <CardContent>{children}</CardContent>
89
+ </Card>
90
+ );
91
+
92
+ const Documentation = () => {
93
+ const [copiedCode, setCopiedCode] = useState("");
94
+ const { navigateTo } = useNavigation();
95
+
96
+ const copyCode = (code: string, id: string) => {
97
+ navigator.clipboard.writeText(code);
98
+ setCopiedCode(id);
99
+ setTimeout(() => setCopiedCode(""), 2000);
100
+ };
101
+
102
+ return (
103
+ <PageLayout
104
+ title="Documentation"
105
+ description="Learn how to build with this boilerplate"
106
+ icon={BookOpen01Icon}
107
+ toolbar={
108
+ <div className="flex gap-2">
109
+ <Button
110
+ variant="outline"
111
+ size="icon"
112
+ onClick={() => navigateTo(PAGES.home)}
113
+ >
114
+ <ArrowLeft01Icon className="w-4 h-4" />
115
+ </Button>
116
+ <Button onClick={() => navigateTo(PAGES.demo)} className="gap-2">
117
+ <PlayIcon className="w-4 h-4" />
118
+ Try Demo
119
+ </Button>
120
+ </div>
121
+ }
122
+ >
123
+ <div className="space-y-6">
124
+ <Tabs defaultValue="structure" className="w-full">
125
+ <TabsList className="grid w-full grid-cols-5">
126
+ <TabsTrigger value="structure" className="gap-2">
127
+ <FolderOpenIcon className="w-4 h-4" />
128
+ Structure
129
+ </TabsTrigger>
130
+ <TabsTrigger value="pages" className="gap-2">
131
+ <File01Icon className="w-4 h-4" />
132
+ Pages
133
+ </TabsTrigger>
134
+ <TabsTrigger value="navigation" className="gap-2">
135
+ <Navigation01Icon className="w-4 h-4" />
136
+ Navigation
137
+ </TabsTrigger>
138
+ <TabsTrigger value="dataverse" className="gap-2">
139
+ <Database01Icon className="w-4 h-4" />
140
+ Dataverse
141
+ </TabsTrigger>
142
+ <TabsTrigger value="components" className="gap-2">
143
+ <PuzzleIcon className="w-4 h-4" />
144
+ Components
145
+ </TabsTrigger>
146
+ </TabsList>
147
+
148
+ {/* Project Structure */}
149
+ <TabsContent value="structure" className="space-y-6 mt-6">
150
+ <Card>
151
+ <CardHeader>
152
+ <CardTitle className="flex items-center gap-2">
153
+ <FolderOpenIcon className="w-5 h-5 text-primary" />
154
+ Project Structure
155
+ </CardTitle>
156
+ <CardDescription>
157
+ Overview of the boilerplate folder organization
158
+ </CardDescription>
159
+ </CardHeader>
160
+ <CardContent>
161
+ <CodeBlock
162
+ id="structure"
163
+ code={`src/
164
+ ├── assets/
165
+ │ └── images/ # Static images and SVGs
166
+ ├── components/
167
+ │ ├── ui/ # shadcn/ui components
168
+ │ │ ├── alert.tsx
169
+ │ │ ├── badge.tsx
170
+ │ │ ├── button.tsx
171
+ │ │ ├── card.tsx
172
+ │ │ ├── dialog.tsx
173
+ │ │ ├── input.tsx
174
+ │ │ ├── label.tsx
175
+ │ │ ├── separator.tsx
176
+ │ │ ├── table.tsx
177
+ │ │ ├── tabs.tsx
178
+ │ │ └── theme-toggle.tsx
179
+ │ ├── nl-header.tsx # Application header
180
+ │ ├── page-layout.tsx # Reusable page layout
181
+ │ └── page-render.tsx # Dynamic page renderer
182
+ ├── config/
183
+ │ └── pages.config.ts # Page registration
184
+ ├── contexts/
185
+ │ ├── dataverse-context.tsx
186
+ │ └── navigation-context.tsx
187
+ ├── hooks/
188
+ │ ├── useAccounts.ts # Entity-specific hook example
189
+ │ ├── useDataverse.ts # Dataverse API hook
190
+ │ └── useNavigation.ts # Navigation hook
191
+ ├── lib/
192
+ │ ├── nav-items.ts # Navigation menu items
193
+ │ └── utils.ts # Utility functions (cn)
194
+ ├── pages/
195
+ │ ├── Demo.tsx
196
+ │ ├── Documentation.tsx
197
+ │ ├── Home.tsx
198
+ │ └── index.ts # Page exports
199
+ ├── providers/
200
+ │ ├── dataverse-provider.tsx
201
+ │ ├── navigation-provider.tsx
202
+ │ └── theme-provider.tsx
203
+ ├── App.tsx # Root component
204
+ ├── index.css # Global styles
205
+ └── main.tsx # Entry point`}
206
+ onCopy={copyCode}
207
+ isCopied={copiedCode === "structure"}
208
+ />
209
+ </CardContent>
210
+ </Card>
211
+
212
+ <Card>
213
+ <CardHeader>
214
+ <CardTitle>Tech Stack</CardTitle>
215
+ <CardDescription>
216
+ Technologies used in this boilerplate
217
+ </CardDescription>
218
+ </CardHeader>
219
+ <CardContent>
220
+ <div className="grid grid-cols-2 md:grid-cols-3 gap-4">
221
+ {[
222
+ { name: "Vite 7", desc: "Build tool & dev server" },
223
+ { name: "React 19", desc: "UI library" },
224
+ { name: "TypeScript", desc: "Type safety" },
225
+ { name: "Tailwind CSS 4", desc: "Utility-first CSS" },
226
+ { name: "shadcn/ui", desc: "UI components" },
227
+ { name: "dynamics-web-api", desc: "Dataverse integration" },
228
+ ].map((tech) => (
229
+ <div
230
+ key={tech.name}
231
+ className="p-3 rounded-lg border bg-muted/30"
232
+ >
233
+ <p className="font-medium text-sm">{tech.name}</p>
234
+ <p className="text-xs text-muted-foreground">
235
+ {tech.desc}
236
+ </p>
237
+ </div>
238
+ ))}
239
+ </div>
240
+ </CardContent>
241
+ </Card>
242
+ </TabsContent>
243
+
244
+ {/* Pages Management */}
245
+ <TabsContent value="pages" className="space-y-6 mt-6">
246
+ <Card>
247
+ <CardHeader>
248
+ <CardTitle className="flex items-center gap-2">
249
+ <Add01Icon className="w-5 h-5 text-green-600" />
250
+ Creating New Pages
251
+ </CardTitle>
252
+ <CardDescription>
253
+ Add new pages to your application in three steps
254
+ </CardDescription>
255
+ </CardHeader>
256
+ <CardContent className="space-y-6">
257
+ <div className="grid gap-4">
258
+ <StepCard number={1} title="Create the Page Component">
259
+ <div className="space-y-3">
260
+ <p className="text-sm text-muted-foreground">
261
+ Create a new file in{" "}
262
+ <code className="bg-muted px-1.5 py-0.5 rounded text-xs">
263
+ src/pages/
264
+ </code>
265
+ </p>
266
+ <CodeBlock
267
+ id="new-page"
268
+ title="src/pages/Contacts.tsx"
269
+ code={`import { PageLayout } from "@/components/page-layout";
270
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
271
+ import { UserIcon } from "hugeicons-react";
272
+
273
+ const Contacts = () => {
274
+ return (
275
+ <PageLayout
276
+ title="Contacts"
277
+ description="Manage your contacts"
278
+ icon={UserIcon}
279
+ >
280
+ <Card>
281
+ <CardHeader>
282
+ <CardTitle>Contact List</CardTitle>
283
+ </CardHeader>
284
+ <CardContent>
285
+ <p>Your contacts content goes here...</p>
286
+ </CardContent>
287
+ </Card>
288
+ </PageLayout>
289
+ );
290
+ };
291
+
292
+ export default Contacts;`}
293
+ onCopy={copyCode}
294
+ isCopied={copiedCode === "new-page"}
295
+ />
296
+ </div>
297
+ </StepCard>
298
+
299
+ <StepCard number={2} title="Export from Index">
300
+ <div className="space-y-3">
301
+ <p className="text-sm text-muted-foreground">
302
+ Add your page export to{" "}
303
+ <code className="bg-muted px-1.5 py-0.5 rounded text-xs">
304
+ src/pages/index.ts
305
+ </code>
306
+ </p>
307
+ <CodeBlock
308
+ id="export-page"
309
+ title="src/pages/index.ts"
310
+ code={`import Home from "./Home";
311
+ import Documentation from "./Documentation";
312
+ import Demo from "./Demo";
313
+ import Contacts from "./Contacts"; // Add this
314
+
315
+ export { Home, Documentation, Demo, Contacts };`}
316
+ onCopy={copyCode}
317
+ isCopied={copiedCode === "export-page"}
318
+ />
319
+ </div>
320
+ </StepCard>
321
+
322
+ <StepCard number={3} title="Register in Pages Config">
323
+ <div className="space-y-3">
324
+ <p className="text-sm text-muted-foreground">
325
+ Add your page to{" "}
326
+ <code className="bg-muted px-1.5 py-0.5 rounded text-xs">
327
+ src/config/pages.config.ts
328
+ </code>
329
+ </p>
330
+ <CodeBlock
331
+ id="config-page"
332
+ title="src/config/pages.config.ts"
333
+ code={`import { Home, Documentation, Demo, Contacts } from "@/pages";
334
+ import type { ComponentType } from "react";
335
+
336
+ export interface PageConfig {
337
+ id: string;
338
+ title: string;
339
+ component: ComponentType;
340
+ }
341
+
342
+ export const PAGES: Record<string, PageConfig> = {
343
+ home: {
344
+ id: "home",
345
+ title: "Home",
346
+ component: Home,
347
+ },
348
+ documentation: {
349
+ id: "documentation",
350
+ title: "Documentation",
351
+ component: Documentation,
352
+ },
353
+ demo: {
354
+ id: "demo",
355
+ title: "Demo",
356
+ component: Demo,
357
+ },
358
+ contacts: { // Add your new page
359
+ id: "contacts",
360
+ title: "Contacts",
361
+ component: Contacts,
362
+ },
363
+ };
364
+
365
+ export const DEFAULT_PAGE = PAGES.home;`}
366
+ onCopy={copyCode}
367
+ isCopied={copiedCode === "config-page"}
368
+ />
369
+ </div>
370
+ </StepCard>
371
+ </div>
372
+
373
+ <Alert>
374
+ <BulbIcon className="h-4 w-4" />
375
+ <AlertTitle>Done!</AlertTitle>
376
+ <AlertDescription>
377
+ Navigate to your new page using{" "}
378
+ <code className="bg-muted px-1.5 py-0.5 rounded text-xs">
379
+ navigateTo(PAGES.contacts)
380
+ </code>
381
+ </AlertDescription>
382
+ </Alert>
383
+ </CardContent>
384
+ </Card>
385
+
386
+ <div className="grid md:grid-cols-2 gap-6">
387
+ <Card>
388
+ <CardHeader>
389
+ <CardTitle className="flex items-center gap-2">
390
+ <PencilEdit01Icon className="w-5 h-5 text-blue-600" />
391
+ Updating Pages
392
+ </CardTitle>
393
+ </CardHeader>
394
+ <CardContent>
395
+ <ul className="space-y-2 text-sm text-muted-foreground">
396
+ <li className="flex items-center gap-2">
397
+ <div className="w-1.5 h-1.5 bg-primary rounded-full" />
398
+ Edit the component file directly
399
+ </li>
400
+ <li className="flex items-center gap-2">
401
+ <div className="w-1.5 h-1.5 bg-primary rounded-full" />
402
+ Update title in pages.config.ts
403
+ </li>
404
+ <li className="flex items-center gap-2">
405
+ <div className="w-1.5 h-1.5 bg-primary rounded-full" />
406
+ Changes reflect with hot reload
407
+ </li>
408
+ </ul>
409
+ </CardContent>
410
+ </Card>
411
+
412
+ <Card>
413
+ <CardHeader>
414
+ <CardTitle className="flex items-center gap-2">
415
+ <Delete02Icon className="w-5 h-5 text-red-600" />
416
+ Removing Pages
417
+ </CardTitle>
418
+ </CardHeader>
419
+ <CardContent>
420
+ <ul className="space-y-2 text-sm text-muted-foreground">
421
+ <li className="flex items-center gap-2">
422
+ <div className="w-1.5 h-1.5 bg-destructive rounded-full" />
423
+ Delete the component file
424
+ </li>
425
+ <li className="flex items-center gap-2">
426
+ <div className="w-1.5 h-1.5 bg-destructive rounded-full" />
427
+ Remove from pages/index.ts
428
+ </li>
429
+ <li className="flex items-center gap-2">
430
+ <div className="w-1.5 h-1.5 bg-destructive rounded-full" />
431
+ Remove from pages.config.ts
432
+ </li>
433
+ </ul>
434
+ </CardContent>
435
+ </Card>
436
+ </div>
437
+ </TabsContent>
438
+
439
+ {/* Navigation System */}
440
+ <TabsContent value="navigation" className="space-y-6 mt-6">
441
+ <Card>
442
+ <CardHeader>
443
+ <CardTitle>Navigation Hook</CardTitle>
444
+ <CardDescription>
445
+ Use the useNavigation hook to navigate between pages
446
+ </CardDescription>
447
+ </CardHeader>
448
+ <CardContent className="space-y-6">
449
+ <div className="space-y-4">
450
+ <h4 className="font-semibold text-sm">Basic Usage</h4>
451
+ <CodeBlock
452
+ id="nav-basic"
453
+ code={`import { useNavigation } from "@/hooks/useNavigation";
454
+ import { PAGES } from "@/config/pages.config";
455
+
456
+ const MyComponent = () => {
457
+ const { currentPage, navigateTo, canGoBack, goBack } = useNavigation();
458
+
459
+ return (
460
+ <div>
461
+ <p>Current: {currentPage.title}</p>
462
+
463
+ {/* Navigate using PAGES config */}
464
+ <Button onClick={() => navigateTo(PAGES.demo)}>
465
+ Go to Demo
466
+ </Button>
467
+
468
+ {/* Go back to previous page */}
469
+ {canGoBack && (
470
+ <Button onClick={goBack}>Go Back</Button>
471
+ )}
472
+ </div>
473
+ );
474
+ };`}
475
+ onCopy={copyCode}
476
+ isCopied={copiedCode === "nav-basic"}
477
+ />
478
+ </div>
479
+
480
+ <Separator />
481
+
482
+ <div className="space-y-4">
483
+ <h4 className="font-semibold text-sm">Hook API</h4>
484
+ <CodeBlock
485
+ id="nav-api"
486
+ code={`const {
487
+ currentPage, // Current PageConfig object
488
+ navigateTo, // Navigate to a PageConfig
489
+ navigate, // Navigate by page id string
490
+ canGoBack, // Boolean if history exists
491
+ goBack, // Navigate to previous page
492
+ } = useNavigation();
493
+
494
+ // Examples:
495
+ navigateTo(PAGES.home); // Using PageConfig`}
496
+ onCopy={copyCode}
497
+ isCopied={copiedCode === "nav-api"}
498
+ />
499
+ </div>
500
+ </CardContent>
501
+ </Card>
502
+
503
+ <Card>
504
+ <CardHeader>
505
+ <CardTitle>App Structure</CardTitle>
506
+ <CardDescription>
507
+ How providers are organized in App.tsx
508
+ </CardDescription>
509
+ </CardHeader>
510
+ <CardContent>
511
+ <CodeBlock
512
+ id="app-structure"
513
+ title="src/App.tsx"
514
+ code={`import { ThemeProvider } from "@/providers/theme-provider";
515
+ import { NavigationProvider } from "./providers/navigation-provider";
516
+ import { PageRenderer } from "./components/page-render";
517
+ import NlHeader from "./components/nl-header";
518
+ import { DataverseProvider } from "./providers/dataverse-provider";
519
+
520
+ function App() {
521
+ return (
522
+ <ThemeProvider defaultTheme="light" storageKey="novalogica-ui-theme">
523
+ <DataverseProvider>
524
+ <NavigationProvider>
525
+ <div className="flex min-h-screen w-full flex-col bg-background">
526
+ <NlHeader />
527
+ <main className="flex-1 flex flex-col">
528
+ <PageRenderer />
529
+ </main>
530
+ </div>
531
+ </NavigationProvider>
532
+ </DataverseProvider>
533
+ </ThemeProvider>
534
+ );
535
+ }
536
+
537
+ export default App;`}
538
+ onCopy={copyCode}
539
+ isCopied={copiedCode === "app-structure"}
540
+ />
541
+ </CardContent>
542
+ </Card>
543
+ </TabsContent>
544
+
545
+ {/* Dataverse API */}
546
+ <TabsContent value="dataverse" className="space-y-6 mt-6">
547
+ <Card>
548
+ <CardHeader>
549
+ <CardTitle>Using the Dataverse Hook</CardTitle>
550
+ <CardDescription>
551
+ Connect to Dataverse and perform CRUD operations
552
+ </CardDescription>
553
+ </CardHeader>
554
+ <CardContent className="space-y-6">
555
+ <div className="space-y-4">
556
+ <h4 className="font-semibold text-sm">Basic Connection</h4>
557
+ <CodeBlock
558
+ id="dataverse-basic"
559
+ code={`import { useDataverse } from "@/hooks/useDataverse";
560
+
561
+ const MyComponent = () => {
562
+ const { api, isReady, error } = useDataverse();
563
+
564
+ if (!isReady) return <div>Connecting...</div>;
565
+ if (error) return <div>Error: {error}</div>;
566
+
567
+ return <div>Connected to Dataverse!</div>;
568
+ };`}
569
+ onCopy={copyCode}
570
+ isCopied={copiedCode === "dataverse-basic"}
571
+ />
572
+ </div>
573
+
574
+ <Separator />
575
+
576
+ <div className="space-y-4">
577
+ <h4 className="font-semibold text-sm">CRUD Operations</h4>
578
+ <div className="grid gap-4">
579
+ <div>
580
+ <Badge variant="outline" className="mb-2">
581
+ READ
582
+ </Badge>
583
+ <CodeBlock
584
+ id="dataverse-read"
585
+ code={`// Multiple records
586
+ const response = await api.retrieveMultiple({
587
+ collection: "accounts",
588
+ select: ["accountid", "name", "emailaddress1"],
589
+ filter: "statecode eq 0",
590
+ orderBy: ["name asc"],
591
+ maxPageSize: 10
592
+ });
593
+
594
+ // Single record
595
+ const account = await api.retrieve({
596
+ collection: "accounts",
597
+ key: "account-guid-here",
598
+ select: ["accountid", "name"]
599
+ });`}
600
+ onCopy={copyCode}
601
+ isCopied={copiedCode === "dataverse-read"}
602
+ />
603
+ </div>
604
+
605
+ <div>
606
+ <Badge variant="outline" className="mb-2">
607
+ CREATE
608
+ </Badge>
609
+ <CodeBlock
610
+ id="dataverse-create"
611
+ code={`const newId = await api.create({
612
+ collection: "accounts",
613
+ data: {
614
+ name: "New Account",
615
+ emailaddress1: "email@example.com"
616
+ }
617
+ });`}
618
+ onCopy={copyCode}
619
+ isCopied={copiedCode === "dataverse-create"}
620
+ />
621
+ </div>
622
+
623
+ <div>
624
+ <Badge variant="outline" className="mb-2">
625
+ UPDATE
626
+ </Badge>
627
+ <CodeBlock
628
+ id="dataverse-update"
629
+ code={`await api.update({
630
+ collection: "accounts",
631
+ key: "account-guid-here",
632
+ data: { name: "Updated Name" }
633
+ });`}
634
+ onCopy={copyCode}
635
+ isCopied={copiedCode === "dataverse-update"}
636
+ />
637
+ </div>
638
+
639
+ <div>
640
+ <Badge variant="outline" className="mb-2">
641
+ DELETE
642
+ </Badge>
643
+ <CodeBlock
644
+ id="dataverse-delete"
645
+ code={`await api.deleteRecord({
646
+ collection: "accounts",
647
+ key: "account-guid-here"
648
+ });`}
649
+ onCopy={copyCode}
650
+ isCopied={copiedCode === "dataverse-delete"}
651
+ />
652
+ </div>
653
+ </div>
654
+ </div>
655
+ </CardContent>
656
+ </Card>
657
+
658
+ <Card>
659
+ <CardHeader>
660
+ <CardTitle>Entity Hook Pattern</CardTitle>
661
+ <CardDescription>
662
+ Create reusable hooks for specific entities (see
663
+ hooks/useAccounts.ts)
664
+ </CardDescription>
665
+ </CardHeader>
666
+ <CardContent>
667
+ <CodeBlock
668
+ id="entity-hook"
669
+ title="src/hooks/useContacts.ts"
670
+ code={`import { useState, useCallback } from "react";
671
+ import { useDataverse } from "@/hooks/useDataverse";
672
+
673
+ export interface Contact {
674
+ contactid?: string;
675
+ fullname?: string;
676
+ emailaddress1?: string;
677
+ }
678
+
679
+ export const useContacts = () => {
680
+ const { api, isReady } = useDataverse();
681
+ const [contacts, setContacts] = useState<Contact[]>([]);
682
+ const [loading, setLoading] = useState(false);
683
+ const [error, setError] = useState<string | null>(null);
684
+
685
+ const fetchContacts = useCallback(async () => {
686
+ if (!api || !isReady) return;
687
+
688
+ setLoading(true);
689
+ setError(null);
690
+
691
+ try {
692
+ const response = await api.retrieveMultiple({
693
+ collection: "contacts",
694
+ select: ["contactid", "fullname", "emailaddress1"],
695
+ filter: "statecode eq 0"
696
+ });
697
+ setContacts(response?.value || []);
698
+ } catch (err) {
699
+ setError(err instanceof Error ? err.message : "Failed to fetch");
700
+ } finally {
701
+ setLoading(false);
702
+ }
703
+ }, [api, isReady]);
704
+
705
+ return { contacts, loading, error, fetchContacts, isReady };
706
+ };`}
707
+ onCopy={copyCode}
708
+ isCopied={copiedCode === "entity-hook"}
709
+ />
710
+ </CardContent>
711
+ </Card>
712
+ </TabsContent>
713
+
714
+ {/* UI Components */}
715
+ <TabsContent value="components" className="space-y-6 mt-6">
716
+ <Card>
717
+ <CardHeader>
718
+ <CardTitle>Included Components</CardTitle>
719
+ <CardDescription>
720
+ shadcn/ui components available in this boilerplate
721
+ </CardDescription>
722
+ </CardHeader>
723
+ <CardContent>
724
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-2">
725
+ {[
726
+ "Alert",
727
+ "Badge",
728
+ "Button",
729
+ "Card",
730
+ "Dialog",
731
+ "Input",
732
+ "Label",
733
+ "Separator",
734
+ "Table",
735
+ "Tabs",
736
+ "Theme Toggle",
737
+ ].map((component) => (
738
+ <Badge
739
+ key={component}
740
+ variant="secondary"
741
+ className="justify-center py-2"
742
+ >
743
+ {component}
744
+ </Badge>
745
+ ))}
746
+ </div>
747
+ </CardContent>
748
+ </Card>
749
+
750
+ <div className="grid md:grid-cols-2 gap-6">
751
+ <Card>
752
+ <CardHeader>
753
+ <CardTitle className="flex items-center gap-2">
754
+ <Add01Icon className="w-5 h-5 text-blue-600" />
755
+ Adding Components
756
+ </CardTitle>
757
+ </CardHeader>
758
+ <CardContent className="space-y-4">
759
+ <p className="text-sm text-muted-foreground">
760
+ Add more shadcn/ui components as needed:
761
+ </p>
762
+ <CodeBlock
763
+ id="add-component"
764
+ code={`npx shadcn@latest add select
765
+ npx shadcn@latest add dropdown-menu
766
+ npx shadcn@latest add form`}
767
+ onCopy={copyCode}
768
+ isCopied={copiedCode === "add-component"}
769
+ />
770
+ <Button
771
+ variant="outline"
772
+ size="sm"
773
+ onClick={() =>
774
+ window.open(
775
+ "https://ui.shadcn.com/docs/components",
776
+ "_blank",
777
+ )
778
+ }
779
+ className="gap-2"
780
+ >
781
+ <CodeIcon className="w-4 h-4" />
782
+ Browse Components
783
+ </Button>
784
+ </CardContent>
785
+ </Card>
786
+
787
+ <Card>
788
+ <CardHeader>
789
+ <CardTitle className="flex items-center gap-2">
790
+ <Database01Icon className="w-5 h-5 text-green-600" />
791
+ DynamicsWebApi
792
+ </CardTitle>
793
+ </CardHeader>
794
+ <CardContent className="space-y-4">
795
+ <p className="text-sm text-muted-foreground">
796
+ This boilerplate uses DynamicsWebApi for Dataverse
797
+ operations.
798
+ </p>
799
+ <ul className="space-y-1 text-sm text-muted-foreground">
800
+ <li className="flex items-center gap-2">
801
+ <div className="w-1.5 h-1.5 bg-green-500 rounded-full" />
802
+ Full TypeScript support
803
+ </li>
804
+ <li className="flex items-center gap-2">
805
+ <div className="w-1.5 h-1.5 bg-green-500 rounded-full" />
806
+ OData query builder
807
+ </li>
808
+ <li className="flex items-center gap-2">
809
+ <div className="w-1.5 h-1.5 bg-green-500 rounded-full" />
810
+ Batch operations
811
+ </li>
812
+ </ul>
813
+ <Button
814
+ variant="outline"
815
+ size="sm"
816
+ onClick={() =>
817
+ window.open(
818
+ "https://github.com/AleksandrRogov/DynamicsWebApi",
819
+ "_blank",
820
+ )
821
+ }
822
+ className="gap-2"
823
+ >
824
+ <File01Icon className="w-4 h-4" />
825
+ View Docs
826
+ </Button>
827
+ </CardContent>
828
+ </Card>
829
+ </div>
830
+
831
+ <Alert>
832
+ <BulbIcon className="h-4 w-4" />
833
+ <AlertTitle>Tips</AlertTitle>
834
+ <AlertDescription className="space-y-1">
835
+ <p>
836
+ Use TypeScript interfaces for entity types to get better
837
+ IntelliSense
838
+ </p>
839
+ <p>Check the Demo page for working CRUD examples</p>
840
+ <p>Use PageLayout component for consistent page structure</p>
841
+ </AlertDescription>
842
+ </Alert>
843
+ </TabsContent>
844
+ </Tabs>
845
+ </div>
846
+ </PageLayout>
847
+ );
848
+ };
849
+
850
+ export default Documentation;