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.
- package/.env.example +22 -0
- package/README.md +75 -0
- package/bin/cli.js +84 -0
- package/components.json +22 -0
- package/eslint.config.js +23 -0
- package/getToken.js +197 -0
- package/index.html +30 -0
- package/package.json +69 -0
- package/src/App.tsx +28 -0
- package/src/assets/images/novalogica-logo.svg +24 -0
- package/src/components/nl-header.tsx +165 -0
- package/src/components/page-layout.tsx +78 -0
- package/src/components/page-render.tsx +16 -0
- package/src/components/ui/alert.tsx +66 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/button.tsx +165 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +156 -0
- package/src/components/ui/input.tsx +23 -0
- package/src/components/ui/label.tsx +23 -0
- package/src/components/ui/separator.tsx +26 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/tabs.tsx +91 -0
- package/src/components/ui/theme-toggle.tsx +28 -0
- package/src/config/pages.config.ts +34 -0
- package/src/contexts/dataverse-context.tsx +12 -0
- package/src/contexts/navigation-context.tsx +14 -0
- package/src/hooks/useAccounts.ts +194 -0
- package/src/hooks/useDataverse.ts +11 -0
- package/src/hooks/useNavigation.ts +41 -0
- package/src/index.css +147 -0
- package/src/lib/nav-items.ts +25 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +12 -0
- package/src/pages/Demo.tsx +465 -0
- package/src/pages/Documentation.tsx +850 -0
- package/src/pages/Home.tsx +132 -0
- package/src/pages/index.ts +4 -0
- package/src/providers/dataverse-provider.tsx +81 -0
- package/src/providers/navigation-provider.tsx +33 -0
- package/src/providers/theme-provider.tsx +92 -0
- package/src/public/novalogica-logo.svg +24 -0
- package/tsconfig.app.json +32 -0
- package/tsconfig.json +17 -0
- package/tsconfig.node.json +26 -0
- 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;
|