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,132 @@
1
+ import { Button } from "@/components/ui/button";
2
+ import {
3
+ BookOpen01Icon,
4
+ Rocket01Icon,
5
+ CheckmarkCircle02Icon,
6
+ ArrowRight01Icon,
7
+ TestTube01Icon,
8
+ } from "hugeicons-react";
9
+ import { useNavigation } from "@/hooks/useNavigation";
10
+ import { Badge } from "@/components/ui/badge";
11
+ import { PAGES } from "@/config/pages.config";
12
+
13
+ const Home = () => {
14
+ const { navigateTo } = useNavigation();
15
+ const currentYear = new Date().getFullYear();
16
+
17
+ const features = [
18
+ "Type-safe Dataverse integration",
19
+ "Enterprise-grade authentication",
20
+ "Production-ready architecture",
21
+ "Modern React patterns",
22
+ ];
23
+
24
+ return (
25
+ <div className="flex flex-col min-h-screen bg-background">
26
+ {/* Main Content */}
27
+ <main className="flex-1 flex items-center justify-center px-6 py-12">
28
+ <div className="w-full max-w-3xl mx-auto">
29
+ {/* Hero Section */}
30
+ <div className="text-center space-y-8">
31
+ {/* Brand Badge */}
32
+ <Badge
33
+ variant="outline"
34
+ className="px-4 py-2 text-sm font-medium gap-2 border-primary/20"
35
+ >
36
+ <Rocket01Icon className="w-4 h-4 text-primary" />
37
+ D365 Development Boilerplate
38
+ </Badge>
39
+
40
+ {/* Main Title */}
41
+ <div className="space-y-4">
42
+ <h1 className="text-4xl sm:text-5xl md:text-6xl font-bold tracking-tight text-foreground">
43
+ novalogica
44
+ </h1>
45
+ <p className="text-lg text-muted-foreground max-w-xl mx-auto">
46
+ A production-ready React boilerplate for building enterprise
47
+ Dynamics 365 applications with modern tooling.
48
+ </p>
49
+ </div>
50
+
51
+ {/* CTA Buttons */}
52
+ <div className="flex flex-col sm:flex-row gap-3 justify-center pt-4">
53
+ <Button
54
+ variant="gradient"
55
+ size="lg"
56
+ leftIcon={TestTube01Icon}
57
+ onClick={() => navigateTo(PAGES.demo)}
58
+ >
59
+ View Demo
60
+ </Button>
61
+ <Button
62
+ variant="soft"
63
+ size="lg"
64
+ leftIcon={BookOpen01Icon}
65
+ onClick={() => navigateTo(PAGES.documentation)}
66
+ >
67
+ Documentation
68
+ </Button>
69
+ </div>
70
+
71
+ {/* Features List */}
72
+ <div className="pt-8">
73
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 max-w-lg mx-auto text-left">
74
+ {features.map((feature) => (
75
+ <div
76
+ key={feature}
77
+ className="flex items-center gap-2 text-sm text-muted-foreground"
78
+ >
79
+ <CheckmarkCircle02Icon className="w-4 h-4 text-primary shrink-0" />
80
+ <span>{feature}</span>
81
+ </div>
82
+ ))}
83
+ </div>
84
+ </div>
85
+
86
+ {/* Tech Stack */}
87
+ <div className="pt-8 border-t border-border/50">
88
+ <p className="text-xs text-muted-foreground/60 uppercase tracking-wider mb-4">
89
+ Built with
90
+ </p>
91
+ <div className="flex flex-wrap justify-center gap-2">
92
+ {[
93
+ "Vite 7",
94
+ "React 19",
95
+ "TypeScript",
96
+ "Tailwind CSS 4",
97
+ "shadcn/ui",
98
+ "dynamics-web-api",
99
+ ].map((tech) => (
100
+ <span
101
+ key={tech}
102
+ className="px-3 py-1 text-xs text-muted-foreground bg-muted/50 rounded-full"
103
+ >
104
+ {tech}
105
+ </span>
106
+ ))}
107
+ </div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </main>
112
+
113
+ {/* Footer */}
114
+ <footer className="py-6 px-6 border-t border-border/50">
115
+ <div className="max-w-3xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4 text-sm text-muted-foreground">
116
+ <p>© {currentYear} novalogica. All rights reserved.</p>
117
+ <a
118
+ href="https://github.com/novalogica"
119
+ target="_blank"
120
+ rel="noopener noreferrer"
121
+ className="flex items-center gap-1 hover:text-foreground transition-colors"
122
+ >
123
+ View on GitHub
124
+ <ArrowRight01Icon className="w-3 h-3" />
125
+ </a>
126
+ </div>
127
+ </footer>
128
+ </div>
129
+ );
130
+ };
131
+
132
+ export default Home;
@@ -0,0 +1,4 @@
1
+ import Home from "./Home";
2
+ import Documentation from "./Documentation";
3
+ import Demo from "./Demo";
4
+ export { Home, Documentation, Demo };
@@ -0,0 +1,81 @@
1
+ import { DynamicsWebApi } from "dynamics-web-api";
2
+ import { type ReactNode, useEffect, useState } from "react";
3
+
4
+ import { DataverseContext } from "@/contexts/dataverse-context";
5
+
6
+ interface DataverseProviderProps {
7
+ children: ReactNode;
8
+ }
9
+
10
+ declare global {
11
+ interface Window {
12
+ Xrm?: {
13
+ WebApi?: unknown;
14
+ };
15
+ }
16
+ }
17
+
18
+ const isInD365 = (): boolean => {
19
+ try {
20
+ return typeof window.parent?.Xrm?.WebApi !== "undefined";
21
+ } catch {
22
+ return false;
23
+ }
24
+ };
25
+
26
+ const onTokenRefresh = async (): Promise<string | null> => {
27
+ const token = import.meta.env.VITE_D365_TOKEN;
28
+
29
+ if (!token) {
30
+ console.error("No authentication token available");
31
+ return null;
32
+ }
33
+
34
+ return token;
35
+ };
36
+
37
+ export const DataverseProvider = ({ children }: DataverseProviderProps) => {
38
+ const [api, setApi] = useState<DynamicsWebApi | null>(null);
39
+ const [isReady, setIsReady] = useState(false);
40
+ const [error, setError] = useState<string | null>(null);
41
+
42
+ useEffect(() => {
43
+ const initialize = () => {
44
+ try {
45
+ const baseConfig = {
46
+ dataApi: { version: import.meta.env.VITE_D365_API_VERSION || "9.2" },
47
+ };
48
+
49
+ const config = isInD365()
50
+ ? baseConfig
51
+ : {
52
+ ...baseConfig,
53
+ serverUrl: import.meta.env.VITE_D365_API_URL,
54
+ onTokenRefresh,
55
+ };
56
+
57
+ const apiInstance = new DynamicsWebApi(config);
58
+
59
+ setApi(apiInstance);
60
+ setIsReady(true);
61
+ setError(null);
62
+ } catch (err) {
63
+ const errorMessage =
64
+ err instanceof Error
65
+ ? err.message
66
+ : "Failed to initialize Dataverse connection";
67
+ setError(errorMessage);
68
+ setApi(null);
69
+ setIsReady(false);
70
+ }
71
+ };
72
+
73
+ initialize();
74
+ }, []);
75
+
76
+ return (
77
+ <DataverseContext.Provider value={{ api, isReady, error }}>
78
+ {children}
79
+ </DataverseContext.Provider>
80
+ );
81
+ };
@@ -0,0 +1,33 @@
1
+ import { type ReactNode, useCallback, useState } from "react";
2
+
3
+ import { DEFAULT_PAGE } from "@/config/pages.config";
4
+ import type { PageConfig } from "@/config/pages.config";
5
+ import { NavigationContext } from "@/contexts/navigation-context";
6
+
7
+ export function NavigationProvider({ children }: { children: ReactNode }) {
8
+ const [currentPage, setCurrentPage] = useState<PageConfig>(DEFAULT_PAGE);
9
+ const [history, setHistory] = useState<PageConfig[]>([DEFAULT_PAGE]);
10
+
11
+ const navigate = useCallback((page: PageConfig) => {
12
+ setCurrentPage(page);
13
+ setHistory((prev) => [...prev, page]);
14
+ }, []);
15
+
16
+ const goBack = useCallback(() => {
17
+ if (history.length > 1) {
18
+ const newHistory = history.slice(0, -1);
19
+ setHistory(newHistory);
20
+ setCurrentPage(newHistory[newHistory.length - 1]);
21
+ }
22
+ }, [history]);
23
+
24
+ const canGoBack = history.length > 1;
25
+
26
+ return (
27
+ <NavigationContext.Provider
28
+ value={{ currentPage, navigate, canGoBack, goBack }}
29
+ >
30
+ {children}
31
+ </NavigationContext.Provider>
32
+ );
33
+ }
@@ -0,0 +1,92 @@
1
+ import { createContext, useContext, useEffect, useState } from "react";
2
+
3
+ type Theme = "dark" | "light";
4
+
5
+ interface ThemeProviderProps {
6
+ children: React.ReactNode;
7
+ defaultTheme?: Theme;
8
+ storageKey?: string;
9
+ disableTransitionOnChange?: boolean;
10
+ }
11
+
12
+ interface ThemeProviderState {
13
+ theme: Theme;
14
+ setTheme: (theme: Theme) => void;
15
+ }
16
+
17
+ const initialState: ThemeProviderState = {
18
+ theme: "light",
19
+ setTheme: () => null,
20
+ };
21
+
22
+ const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
23
+
24
+ export function ThemeProvider({
25
+ children,
26
+ defaultTheme = "light",
27
+ storageKey = "novalogica-ui-theme",
28
+ disableTransitionOnChange = false,
29
+ ...props
30
+ }: ThemeProviderProps) {
31
+ const [theme, setTheme] = useState<Theme>(
32
+ () => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
33
+ );
34
+
35
+ useEffect(() => {
36
+ const root = window.document.documentElement;
37
+
38
+ root.classList.remove("light", "dark");
39
+
40
+ if (disableTransitionOnChange) {
41
+ const css = document.createElement("style");
42
+ css.appendChild(
43
+ document.createTextNode(
44
+ `* {
45
+ -webkit-transition: none !important;
46
+ -moz-transition: none !important;
47
+ -o-transition: none !important;
48
+ -ms-transition: none !important;
49
+ transition: none !important;
50
+ }`,
51
+ ),
52
+ );
53
+ document.head.appendChild(css);
54
+
55
+ root.classList.add(theme);
56
+
57
+ // Force restyle
58
+ (() => window.getComputedStyle(document.body))();
59
+
60
+ // Wait for next tick before removing
61
+ setTimeout(() => {
62
+ document.head.removeChild(css);
63
+ }, 1);
64
+ } else {
65
+ root.classList.add(theme);
66
+ }
67
+ }, [theme, disableTransitionOnChange]);
68
+
69
+ const value = {
70
+ theme,
71
+ setTheme: (theme: Theme) => {
72
+ localStorage.setItem(storageKey, theme);
73
+ setTheme(theme);
74
+ },
75
+ };
76
+
77
+ return (
78
+ <ThemeProviderContext.Provider {...props} value={value}>
79
+ {children}
80
+ </ThemeProviderContext.Provider>
81
+ );
82
+ }
83
+
84
+ // eslint-disable-next-line react-refresh/only-export-components
85
+ export const useTheme = () => {
86
+ const context = useContext(ThemeProviderContext);
87
+
88
+ if (context === undefined)
89
+ throw new Error("useTheme must be used within a ThemeProvider");
90
+
91
+ return context;
92
+ };
@@ -0,0 +1,24 @@
1
+ <svg width="122" height="122" viewBox="0 0 122 122" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M49.2285 11.7134C55.6976 5.24425 66.1862 5.24427 72.6553 11.7134L110.287 49.3447C116.756 55.8138 116.756 66.3023 110.287 72.7714L72.7714 110.287C66.3023 116.756 55.8138 116.756 49.3446 110.287L11.7133 72.6553C5.24421 66.1862 5.24423 55.6977 11.7134 49.2286L49.2285 11.7134ZM63.5492 20.8195C62.1092 19.3795 59.7746 19.3795 58.3346 20.8195L20.8194 58.3346C19.3795 59.7746 19.3795 62.1093 20.8194 63.5492L58.4507 101.181C59.8907 102.62 62.2254 102.62 63.6653 101.181L101.18 63.6654C102.62 62.2254 102.62 59.8907 101.18 58.4508L63.5492 20.8195Z" fill="url(#paint0_linear)"/>
3
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M36.2867 102.279C27.1379 102.279 19.7214 94.8621 19.7214 85.7133L19.7214 36.2928C19.7214 27.1441 27.1379 19.7276 36.2867 19.7276L85.7072 19.7276C94.8559 19.7276 102.272 27.1441 102.272 36.2928L102.272 85.7133C102.272 94.8621 94.8559 102.279 85.7072 102.279L36.2867 102.279ZM32.5994 85.7133C32.5994 87.7498 34.2502 89.4006 36.2867 89.4006L85.7072 89.4006C87.7436 89.4006 89.3945 87.7498 89.3945 85.7133L89.3945 36.2928C89.3945 34.2564 87.7436 32.6055 85.7072 32.6055L36.2867 32.6055C34.2502 32.6055 32.5994 34.2564 32.5994 36.2928L32.5994 85.7133Z" fill="url(#paint1_linear)" fill-opacity="0.9"/>
4
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M80.6696 19.7277L72.6553 11.7134C66.1862 5.24431 55.6977 5.2443 49.2286 11.7134L11.7134 49.2286C5.2443 55.6977 5.24428 66.1862 11.7134 72.6554L19.7215 80.6635L19.7215 60.728L19.7473 60.7023C19.8031 59.84 20.1605 58.9937 20.8195 58.3347L58.3347 20.8195C59.0085 20.1457 59.8783 19.7872 60.7606 19.744L60.7769 19.7277L80.6696 19.7277Z" fill="url(#paint2_linear)"/>
5
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M102.272 41.3305L110.287 49.3446C116.756 55.8138 116.756 66.3023 110.287 72.7714L72.7714 110.287C66.3023 116.756 55.8138 116.756 49.3446 110.287L41.3367 102.279L60.9419 102.279L60.9614 102.259C61.937 102.285 62.9208 101.925 63.6653 101.18L101.18 63.6653C101.883 62.9626 102.243 62.0467 102.26 61.1257L102.272 61.1132L102.272 41.3305Z" fill="url(#paint3_linear)"/>
6
+ <defs>
7
+ <linearGradient id="paint0_linear" x1="120.564" y1="83.8165" x2="9.83416" y2="56.3893" gradientUnits="userSpaceOnUse">
8
+ <stop stop-color="#429691"/>
9
+ <stop offset="1" stop-color="#95E8C2"/>
10
+ </linearGradient>
11
+ <linearGradient id="paint1_linear" x1="36.3003" y1="5.7623" x2="92.101" y2="98.5145" gradientUnits="userSpaceOnUse">
12
+ <stop stop-color="#429691"/>
13
+ <stop offset="1" stop-color="#95E8C2"/>
14
+ </linearGradient>
15
+ <linearGradient id="paint2_linear" x1="28.5819" y1="32.0553" x2="56.5372" y2="59.2992" gradientUnits="userSpaceOnUse">
16
+ <stop stop-color="#89DDBC"/>
17
+ <stop offset="1" stop-color="#6BBEA9"/>
18
+ </linearGradient>
19
+ <linearGradient id="paint3_linear" x1="41.3367" y1="78.2345" x2="115.138" y2="78.2345" gradientUnits="userSpaceOnUse">
20
+ <stop stop-color="#75C8B0"/>
21
+ <stop offset="1" stop-color="#51A49A"/>
22
+ </linearGradient>
23
+ </defs>
24
+ </svg>
@@ -0,0 +1,32 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "types": ["vite/client"],
9
+ "skipLibCheck": true,
10
+
11
+ /* Bundler mode */
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "moduleDetection": "force",
16
+ "noEmit": true,
17
+ "jsx": "react-jsx",
18
+
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "erasableSyntaxOnly": true,
24
+ "noFallthroughCasesInSwitch": true,
25
+ "noUncheckedSideEffectImports": true,
26
+ "baseUrl": ".",
27
+ "paths": {
28
+ "@/*": ["./src/*"]
29
+ }
30
+ },
31
+ "include": ["src"]
32
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ {
5
+ "path": "./tsconfig.app.json"
6
+ },
7
+ {
8
+ "path": "./tsconfig.node.json"
9
+ }
10
+ ],
11
+ "compilerOptions": {
12
+ "baseUrl": ".",
13
+ "paths": {
14
+ "@/*": ["./src/*"]
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["vite.config.ts"]
26
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,26 @@
1
+ import tailwindcss from "@tailwindcss/vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import path from "path";
4
+ import { defineConfig } from "vite";
5
+ import { viteSingleFile } from "vite-plugin-singlefile";
6
+
7
+ export default defineConfig({
8
+ plugins: [react(), viteSingleFile(), tailwindcss()],
9
+ resolve: {
10
+ alias: {
11
+ "@": path.resolve(__dirname, "./src"),
12
+ },
13
+ },
14
+ build: {
15
+ target: "es2015",
16
+ cssCodeSplit: false,
17
+ assetsInlineLimit: 100000000,
18
+ minify: "terser",
19
+ terserOptions: {
20
+ compress: {
21
+ drop_console: true,
22
+ drop_debugger: true,
23
+ },
24
+ },
25
+ },
26
+ });