igniteui-cli 15.2.2 → 15.3.1-beta.1

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 (134) hide show
  1. package/lib/commands/build.js +7 -12
  2. package/package.json +7 -7
  3. package/templates/blazor/igb/projects/ai-config/files/skills/AGENTS.md +0 -5
  4. package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/SKILL.md +2 -0
  5. package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/references/charts.md +7 -35
  6. package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/references/data-display.md +0 -54
  7. package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/references/feedback.md +0 -38
  8. package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/references/form-controls.md +0 -68
  9. package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/references/layout-manager.md +1 -124
  10. package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/references/layout.md +0 -62
  11. package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-grids/references/grid-migration.md +322 -0
  12. package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-theming/SKILL.md +1 -1
  13. package/templates/react/igr-ts/projects/_base/files/package.json +1 -0
  14. package/templates/react/igr-ts/projects/_base/files/src/app/app.tsx +4 -2
  15. package/templates/react/igr-ts/projects/_base/files/src/setupTests.ts +12 -0
  16. package/templates/react/igr-ts/projects/_base/files/styles.css +6 -0
  17. package/templates/react/igr-ts/projects/_base_with_home/files/index.html +2 -1
  18. package/templates/react/igr-ts/projects/_base_with_home/files/src/app/home/home.tsx +60 -10
  19. package/templates/react/igr-ts/projects/_base_with_home/files/src/app/home/style.module.css +79 -20
  20. package/templates/react/igr-ts/projects/ai-config/files/skills/grid-lite-to-igr-grid-migration/SKILL.md +274 -0
  21. package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-components/SKILL.md +0 -8
  22. package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-components/reference/CHARTS-GRIDS.md +6 -36
  23. package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-components/reference/COMPONENT-CATALOGUE.md +8 -142
  24. package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-components/reference/EVENT-HANDLING.md +2 -0
  25. package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-customize-theme/SKILL.md +7 -14
  26. package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-customize-theme/reference/CSS-THEMING.md +2 -0
  27. package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-customize-theme/reference/MCP-SERVER.md +0 -8
  28. package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-generate-from-image-design/SKILL.md +2 -2
  29. package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-generate-from-image-design/reference/component-mapping.md +60 -74
  30. package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-generate-from-image-design/reference/gotchas.md +2 -2
  31. package/templates/react/igr-ts/projects/empty/index.js +2 -2
  32. package/templates/react/igr-ts/projects/side-nav/files/src/app/app-routes.tsx +5 -0
  33. package/templates/react/igr-ts/projects/side-nav/files/src/app/app.css +82 -0
  34. package/templates/react/igr-ts/projects/side-nav/files/src/app/app.tsx +104 -0
  35. package/templates/react/igr-ts/projects/side-nav/files/src/app/home/home.tsx +69 -0
  36. package/templates/react/igr-ts/projects/side-nav/files/src/app/home/style.module.css +105 -0
  37. package/templates/react/igr-ts/projects/{top-nav → side-nav}/index.d.ts +2 -2
  38. package/templates/react/igr-ts/projects/{top-nav → side-nav}/index.js +7 -7
  39. package/templates/react/igr-ts/projects/side-nav-auth/files/index.html +19 -0
  40. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/app-routes.tsx +24 -0
  41. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/app.css +84 -0
  42. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/app.tsx +124 -0
  43. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/AuthContext.tsx +73 -0
  44. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/AuthGuard.tsx +14 -0
  45. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/Login.module.css +93 -0
  46. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/Login.tsx +69 -0
  47. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/LoginBar.module.css +42 -0
  48. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/LoginBar.tsx +44 -0
  49. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/LoginDialog.module.css +14 -0
  50. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/LoginDialog.tsx +49 -0
  51. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/Register.module.css +74 -0
  52. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/Register.tsx +67 -0
  53. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/models/external-login.ts +10 -0
  54. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/models/login.ts +4 -0
  55. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/models/register-info.ts +6 -0
  56. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/models/user.ts +19 -0
  57. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/Profile.module.css +87 -0
  58. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/Profile.tsx +42 -0
  59. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/RedirectFacebook.tsx +44 -0
  60. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/RedirectGoogle.tsx +40 -0
  61. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/RedirectMicrosoft.tsx +40 -0
  62. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/authentication.ts +37 -0
  63. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/external-auth-config.ts +44 -0
  64. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/externalAuth.ts +272 -0
  65. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/fakeBackend.ts +88 -0
  66. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/jwtUtil.ts +10 -0
  67. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/pkce.ts +29 -0
  68. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/userStore.ts +39 -0
  69. package/templates/react/igr-ts/projects/side-nav-auth/index.d.ts +15 -0
  70. package/templates/react/igr-ts/projects/side-nav-auth/index.js +46 -0
  71. package/templates/react/igr-ts/projects/side-nav-mini/files/src/app/app-routes.tsx +5 -0
  72. package/templates/react/igr-ts/projects/side-nav-mini/files/src/app/app.css +109 -0
  73. package/templates/react/igr-ts/projects/side-nav-mini/files/src/app/app.test.tsx +20 -0
  74. package/templates/react/igr-ts/projects/side-nav-mini/files/src/app/app.tsx +81 -0
  75. package/templates/react/igr-ts/projects/side-nav-mini/files/src/app/home/home.tsx +69 -0
  76. package/templates/react/igr-ts/projects/side-nav-mini/files/src/app/home/style.module.css +105 -0
  77. package/templates/react/igr-ts/projects/side-nav-mini/index.d.ts +15 -0
  78. package/templates/react/igr-ts/projects/side-nav-mini/index.js +46 -0
  79. package/templates/react/igr-ts/projects/side-nav-mini-auth/files/src/app/app.css +106 -0
  80. package/templates/react/igr-ts/projects/side-nav-mini-auth/files/src/app/app.tsx +101 -0
  81. package/templates/react/igr-ts/projects/side-nav-mini-auth/index.d.ts +15 -0
  82. package/templates/react/igr-ts/projects/side-nav-mini-auth/index.js +50 -0
  83. package/templates/webcomponents/igc-ts/projects/_base/files/src/app/app.ts +6 -1
  84. package/templates/webcomponents/igc-ts/projects/_base/files/styles.css +1 -0
  85. package/templates/webcomponents/igc-ts/projects/_base_with_home/files/index.html +2 -0
  86. package/templates/webcomponents/igc-ts/projects/_base_with_home/files/src/app/home/home.ts +103 -9
  87. package/templates/webcomponents/igc-ts/projects/_base_with_home/files/src/assets/wc.png +0 -0
  88. package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-choose-components/SKILL.md +122 -160
  89. package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-customize-component-theme/SKILL.md +83 -311
  90. package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-customize-component-theme/references/mcp-setup.md +69 -0
  91. package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-generate-from-image-design/SKILL.md +4 -1
  92. package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-generate-from-image-design/references/component-mapping.md +60 -61
  93. package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-generate-from-image-design/references/gotchas.md +15 -11
  94. package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-migrate-grid-lite-to-premium/SKILL.md +446 -0
  95. package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-optimize-bundle-size/SKILL.md +23 -274
  96. package/templates/webcomponents/igc-ts/projects/empty/index.js +1 -1
  97. package/templates/webcomponents/igc-ts/projects/side-nav/files/index.html +21 -0
  98. package/templates/webcomponents/igc-ts/projects/side-nav/files/src/app/app-routing.ts +9 -0
  99. package/templates/webcomponents/igc-ts/projects/side-nav/files/src/app/app.ts +192 -22
  100. package/templates/webcomponents/igc-ts/projects/side-nav/files/src/app/home/home.ts +175 -0
  101. package/templates/webcomponents/igc-ts/projects/side-nav/index.js +1 -1
  102. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/index.html +25 -0
  103. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/app-routing.ts +37 -0
  104. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/app.ts +251 -0
  105. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/login-bar/login-bar.ts +124 -0
  106. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/login-dialog/login-dialog.ts +253 -0
  107. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/models/external-login.ts +10 -0
  108. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/models/login.ts +4 -0
  109. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/models/register-info.ts +6 -0
  110. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/models/user.ts +19 -0
  111. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/authentication.ts +37 -0
  112. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/external-auth-config.ts +44 -0
  113. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/externalAuth.ts +272 -0
  114. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/fakeBackend.ts +88 -0
  115. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/jwtUtil.ts +10 -0
  116. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/pkce.ts +29 -0
  117. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/userStore.ts +39 -0
  118. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/profile/profile.ts +142 -0
  119. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/redirect/redirect-facebook.ts +57 -0
  120. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/redirect/redirect-google.ts +53 -0
  121. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/redirect/redirect-microsoft.ts +53 -0
  122. package/templates/webcomponents/igc-ts/projects/side-nav-auth/index.d.ts +15 -0
  123. package/templates/webcomponents/igc-ts/projects/side-nav-auth/index.js +46 -0
  124. package/templates/webcomponents/igc-ts/projects/side-nav-mini/files/src/app/app-routing.ts +13 -0
  125. package/templates/webcomponents/igc-ts/projects/side-nav-mini/files/src/app/app.ts +238 -0
  126. package/templates/webcomponents/igc-ts/projects/side-nav-mini/index.d.ts +14 -0
  127. package/templates/webcomponents/igc-ts/projects/side-nav-mini/index.js +45 -0
  128. package/templates/webcomponents/igc-ts/projects/side-nav-mini-auth/files/src/app/app.ts +258 -0
  129. package/templates/webcomponents/igc-ts/projects/side-nav-mini-auth/index.d.ts +15 -0
  130. package/templates/webcomponents/igc-ts/projects/side-nav-mini-auth/index.js +50 -0
  131. package/templates/react/igr-ts/projects/top-nav/files/src/app/app.css +0 -62
  132. package/templates/react/igr-ts/projects/top-nav/files/src/app/app.tsx +0 -18
  133. package/templates/react/igr-ts/projects/top-nav/files/src/components/navigation-header/index.tsx +0 -19
  134. /package/templates/react/igr-ts/projects/{top-nav → side-nav}/files/src/app/app.test.tsx +0 -0
@@ -0,0 +1,84 @@
1
+ .app {
2
+ display: flex;
3
+ flex-flow: column nowrap;
4
+ height: 100%;
5
+ overflow: hidden;
6
+ }
7
+
8
+ .app__navbar {
9
+ display: flex;
10
+ align-items: center;
11
+ flex: 0 0 auto;
12
+ height: 56px;
13
+ padding: 0 16px;
14
+ background: #239ef0;
15
+ box-shadow: 0 2px 4px rgba(0, 0, 0, .24);
16
+ box-sizing: border-box;
17
+ position: relative;
18
+ z-index: 10;
19
+ }
20
+
21
+ .app__navbar-spacer {
22
+ flex: 1 1 auto;
23
+ }
24
+
25
+ .app__title {
26
+ margin: 0 0 0 16px;
27
+ font-size: 1.25rem;
28
+ font-weight: 600;
29
+ line-height: 1;
30
+ color: #000;
31
+ }
32
+
33
+ .app__menu-button {
34
+ display: inline-flex;
35
+ align-items: center;
36
+ justify-content: center;
37
+ width: 40px;
38
+ height: 40px;
39
+ padding: 0;
40
+ color: #000;
41
+ border: 0;
42
+ background: transparent;
43
+ cursor: pointer;
44
+ }
45
+
46
+ .app__menu-button igc-icon {
47
+ font-size: 24px;
48
+ }
49
+
50
+ .app__body {
51
+ display: flex;
52
+ flex: 1 1 auto;
53
+ min-height: 0;
54
+ }
55
+
56
+ .app__drawer {
57
+ flex: 0 0 auto;
58
+ height: 100%;
59
+ --menu-full-width: 280px;
60
+ }
61
+
62
+ igc-nav-drawer-item::part(base) {
63
+ min-height: 48px;
64
+ color: #2d2d2d;
65
+ }
66
+
67
+ igc-nav-drawer-item[active]::part(base) {
68
+ background: #e0f2ff;
69
+ color: #0075d2;
70
+ }
71
+
72
+ igc-nav-drawer-item[active] igc-icon {
73
+ color: #0075d2;
74
+ }
75
+
76
+ .app__content {
77
+ flex: 1 1 auto;
78
+ display: flex;
79
+ flex-flow: row nowrap;
80
+ justify-content: center;
81
+ align-items: stretch;
82
+ min-width: 0;
83
+ overflow: auto;
84
+ }
@@ -0,0 +1,124 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { Outlet, useLocation, useNavigate } from "react-router-dom";
3
+ import {
4
+ IgrIcon,
5
+ IgrNavDrawer,
6
+ IgrNavDrawerItem,
7
+ registerIcon,
8
+ } from "igniteui-react";
9
+ import { configureTheme } from "igniteui-webcomponents";
10
+ import { AuthProvider, useAuth } from "./authentication/AuthContext";
11
+ import { LoginBar } from "./authentication/components/LoginBar";
12
+ import { routes } from "./app-routes";
13
+ import "igniteui-webcomponents/themes/light/material.css";
14
+ import "./app.css";
15
+
16
+ configureTheme('material', 'light');
17
+
18
+ const materialIcons = [
19
+ ['home', 'action/svg/production/ic_home_24px.svg'],
20
+ ['menu', 'navigation/svg/production/ic_menu_24px.svg'],
21
+ ['apps', 'navigation/svg/production/ic_apps_24px.svg'],
22
+ ['code', 'action/svg/production/ic_code_24px.svg'],
23
+ ['build', 'action/svg/production/ic_build_24px.svg'],
24
+ ['palette', 'image/svg/production/ic_palette_24px.svg'],
25
+ ['account_circle', 'action/svg/production/ic_account_circle_24px.svg'],
26
+ ['lock', 'action/svg/production/ic_lock_24px.svg'],
27
+ ['assignment_ind', 'action/svg/production/ic_assignment_ind_24px.svg'],
28
+ ] as const;
29
+
30
+ materialIcons.forEach(([name, path]) =>
31
+ registerIcon(name, `https://unpkg.com/material-design-icons@3.0.1/${path}`, "material")
32
+ );
33
+
34
+ function AppContent() {
35
+ const name = "$(name)";
36
+ const location = useLocation();
37
+ const navigate = useNavigate();
38
+ const { currentUser } = useAuth();
39
+ const [drawerOpen, setDrawerOpen] = useState(true);
40
+ const [drawerPosition, setDrawerPosition] = useState<"relative" | "start">("relative");
41
+
42
+ const visibleRoutes = useMemo(() => {
43
+ return routes.filter((route) => {
44
+ if (!route.path || !route.text) return false;
45
+ if ((route as any).requiresAuth && !currentUser) return false;
46
+ return true;
47
+ });
48
+ }, [currentUser]);
49
+
50
+ useEffect(() => {
51
+ const mediaQuery = window.matchMedia("(min-width: 1025px)");
52
+ const updateDrawerState = () => {
53
+ setDrawerOpen(mediaQuery.matches);
54
+ setDrawerPosition(mediaQuery.matches ? "relative" : "start");
55
+ };
56
+
57
+ updateDrawerState();
58
+ mediaQuery.addEventListener("change", updateDrawerState);
59
+
60
+ return () => mediaQuery.removeEventListener("change", updateDrawerState);
61
+ }, []);
62
+
63
+ const handleRouteClick = (path: string) => {
64
+ navigate(path);
65
+
66
+ if (window.matchMedia("(max-width: 1024px)").matches) {
67
+ setDrawerOpen(false);
68
+ }
69
+ };
70
+
71
+ return (
72
+ <div className="app">
73
+ <header className="app__navbar">
74
+ <button
75
+ className="app__menu-button"
76
+ type="button"
77
+ aria-label="Toggle navigation"
78
+ onClick={() => setDrawerOpen((open) => !open)}
79
+ >
80
+ <IgrIcon name="menu" collection="material" />
81
+ </button>
82
+ <h1 className="app__title">{name}</h1>
83
+ <div className="app__navbar-spacer" />
84
+ <LoginBar />
85
+ </header>
86
+ <div className="app__body">
87
+ <IgrNavDrawer
88
+ className="app__drawer"
89
+ open={drawerOpen}
90
+ position={drawerPosition}
91
+ >
92
+ {visibleRoutes.map((route) => (
93
+ <IgrNavDrawerItem
94
+ key={route.path}
95
+ active={location.pathname === route.path}
96
+ onClick={() => handleRouteClick(route.path)}
97
+ >
98
+ <IgrIcon
99
+ slot="icon"
100
+ name={route.icon || "home"}
101
+ collection="material"
102
+ style={{
103
+ color: location.pathname === route.path ? "#0075D2" : "#2d2d2d",
104
+ }}
105
+ />
106
+ <span slot="content">{route.text}</span>
107
+ </IgrNavDrawerItem>
108
+ ))}
109
+ </IgrNavDrawer>
110
+ <main className="app__content">
111
+ <Outlet />
112
+ </main>
113
+ </div>
114
+ </div>
115
+ );
116
+ }
117
+
118
+ export default function App() {
119
+ return (
120
+ <AuthProvider>
121
+ <AppContent />
122
+ </AuthProvider>
123
+ );
124
+ }
@@ -0,0 +1,73 @@
1
+ import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
2
+ import type { User } from './models/user';
3
+ import type { Login } from './models/login';
4
+ import type { RegisterInfo } from './models/register-info';
5
+ import type { ExternalLogin } from './models/external-login';
6
+ import { Authentication } from './services/authentication';
7
+ import { UserStore } from './services/userStore';
8
+ import { ExternalAuth } from './services/externalAuth';
9
+
10
+ interface AuthContextType {
11
+ currentUser: User | null;
12
+ initials: string | null;
13
+ login: (data: Login) => Promise<string | null>;
14
+ register: (data: RegisterInfo) => Promise<string | null>;
15
+ loginWith: (data: ExternalLogin) => Promise<string | null>;
16
+ logout: () => void;
17
+ }
18
+
19
+ const AuthContext = createContext<AuthContextType | null>(null);
20
+
21
+ export function AuthProvider({ children }: { children: ReactNode }) {
22
+ const [currentUser, setCurrentUser] = useState<User | null>(() => UserStore.getUser());
23
+
24
+ const initials = currentUser ? UserStore.getInitials(currentUser) : null;
25
+
26
+ const login = useCallback(async (data: Login): Promise<string | null> => {
27
+ const result = await Authentication.login(data);
28
+ if (result.user) {
29
+ UserStore.setUser(result.user);
30
+ setCurrentUser(result.user);
31
+ return null;
32
+ }
33
+ return result.error ?? 'Login failed';
34
+ }, []);
35
+
36
+ const register = useCallback(async (data: RegisterInfo): Promise<string | null> => {
37
+ const result = await Authentication.register(data);
38
+ if (result.user) {
39
+ UserStore.setUser(result.user);
40
+ setCurrentUser(result.user);
41
+ return null;
42
+ }
43
+ return result.error ?? 'Registration failed';
44
+ }, []);
45
+
46
+ const loginWith = useCallback(async (data: ExternalLogin): Promise<string | null> => {
47
+ const result = await Authentication.loginWith(data);
48
+ if (result.user) {
49
+ UserStore.setUser(result.user);
50
+ setCurrentUser(result.user);
51
+ return null;
52
+ }
53
+ return result.error ?? 'Social login failed';
54
+ }, []);
55
+
56
+ const logout = useCallback(() => {
57
+ ExternalAuth.logout();
58
+ UserStore.clearUser();
59
+ setCurrentUser(null);
60
+ }, []);
61
+
62
+ return (
63
+ <AuthContext.Provider value={{ currentUser, initials, login, register, loginWith, logout }}>
64
+ {children}
65
+ </AuthContext.Provider>
66
+ );
67
+ }
68
+
69
+ export function useAuth() {
70
+ const ctx = useContext(AuthContext);
71
+ if (!ctx) throw new Error('useAuth must be used within AuthProvider');
72
+ return ctx;
73
+ }
@@ -0,0 +1,14 @@
1
+ import { Navigate, useLocation } from 'react-router-dom';
2
+ import { useAuth } from './AuthContext';
3
+ import type { ReactNode } from 'react';
4
+
5
+ export function AuthGuard({ children }: { children: ReactNode }) {
6
+ const { currentUser } = useAuth();
7
+ const location = useLocation();
8
+
9
+ if (!currentUser) {
10
+ return <Navigate to="/" state={{ returnUrl: location.pathname }} replace />;
11
+ }
12
+
13
+ return <>{children}</>;
14
+ }
@@ -0,0 +1,93 @@
1
+ .form {
2
+ display: flex;
3
+ flex-flow: column;
4
+ gap: 16px;
5
+ padding: 8px 0 0;
6
+ }
7
+
8
+ .form igc-input {
9
+ width: 100%;
10
+ --ig-input-group-focused-secondary-color: #239ef0;
11
+ --ig-input-group-focused-border-color: #239ef0;
12
+ --ig-input-group-filled-text-color: #2d2d2d;
13
+ }
14
+
15
+ .form igc-input igc-icon {
16
+ color: #0075d2;
17
+ --ig-icon-size: 1.50rem;
18
+ }
19
+
20
+ .error {
21
+ margin: 0;
22
+ font-size: .875rem;
23
+ color: #d32f2f;
24
+ }
25
+
26
+ .actions {
27
+ display: flex;
28
+ flex-flow: column;
29
+ gap: 8px;
30
+ padding-top: 4px;
31
+ }
32
+
33
+ .submitBtn {
34
+ display: block;
35
+ }
36
+
37
+ .submitBtn::part(base) {
38
+ width: 100%;
39
+ min-height: 40px;
40
+ font-weight: 600;
41
+ text-transform: uppercase;
42
+ }
43
+
44
+ .submitBtn:not([disabled])::part(base) {
45
+ background: #239ef0;
46
+ color: #fff;
47
+ }
48
+
49
+ .submitBtn:not([disabled])::part(base):hover {
50
+ background: #1a8fd8;
51
+ }
52
+
53
+ .submitBtn[disabled]::part(base) {
54
+ background: #e0e0e0;
55
+ color: #767676;
56
+ }
57
+
58
+ .linkBtn {
59
+ align-self: center;
60
+ color: #0075d2;
61
+ font-size: .875rem;
62
+ cursor: pointer;
63
+ text-decoration: underline;
64
+ text-transform: none;
65
+ }
66
+
67
+ .linkBtn:hover,
68
+ .linkBtn:focus-visible {
69
+ color: #005da8;
70
+ }
71
+
72
+ .socialLogin {
73
+ display: grid;
74
+ gap: 8px;
75
+ padding-top: 16px;
76
+ border-top: 1px solid #d7d7d7;
77
+ }
78
+
79
+ .socialBtn {
80
+ display: block;
81
+ }
82
+
83
+ .socialBtn::part(base) {
84
+ width: 100%;
85
+ min-height: 40px;
86
+ color: #fff;
87
+ font-weight: 600;
88
+ text-transform: uppercase;
89
+ }
90
+
91
+ .google::part(base) { background: rgb(255, 19, 74); }
92
+ .facebook::part(base) { background: rgb(19, 119, 213); }
93
+ .microsoft::part(base){ background: rgb(27, 158, 245); }
@@ -0,0 +1,69 @@
1
+ import { useState, type FormEvent } from 'react';
2
+ import { IgrButton, IgrIcon, IgrInput } from 'igniteui-react';
3
+ import { useAuth } from '../AuthContext';
4
+ import { ExternalAuth } from '../services/externalAuth';
5
+ import type { Login as LoginData } from '../models/login';
6
+ import styles from './Login.module.css';
7
+
8
+ interface LoginProps {
9
+ onRegister: () => void;
10
+ onSuccess: () => void;
11
+ }
12
+
13
+ export function Login({ onRegister, onSuccess }: LoginProps) {
14
+ const { login } = useAuth();
15
+ const [email, setEmail] = useState('');
16
+ const [password, setPassword] = useState('');
17
+ const [error, setError] = useState('');
18
+
19
+ const canSubmit = email.trim() !== '' && password !== '';
20
+
21
+ const handleSubmit = async (e: FormEvent) => {
22
+ e.preventDefault();
23
+ setError('');
24
+ const data: LoginData = { email, password };
25
+ const err = await login(data);
26
+ if (err) {
27
+ setError(err);
28
+ } else {
29
+ setPassword('');
30
+ onSuccess();
31
+ }
32
+ };
33
+
34
+ return (
35
+ <form className={styles.form} onSubmit={handleSubmit} noValidate>
36
+ <IgrInput outlined type="email" name="email" label="Email" autoComplete="email" required={true}
37
+ value={email} onInput={(e: any) => setEmail(e.detail ?? '')}>
38
+ <IgrIcon slot="suffix" name="account_circle" collection="material" />
39
+ </IgrInput>
40
+ <IgrInput outlined type="password" name="password" label="Password" autoComplete="current-password" required={true}
41
+ value={password} onInput={(e: any) => setPassword(e.detail ?? '')}>
42
+ <IgrIcon slot="suffix" name="lock" collection="material" />
43
+ </IgrInput>
44
+ {error && <p className={styles.error}>{error}</p>}
45
+ <div className={styles.actions}>
46
+ <IgrButton variant="contained" type="submit" className={styles.submitBtn} disabled={!canSubmit}>
47
+ <span>Log In</span>
48
+ </IgrButton>
49
+ <a className={styles.linkBtn} onClick={onRegister} role="button" tabIndex={0}>Create new account</a>
50
+ </div>
51
+ {ExternalAuth.hasProvider() && (
52
+ <div className={styles.socialLogin}>
53
+ {ExternalAuth.hasProvider('google') && (
54
+ <IgrButton variant="contained" type="button" className={`${styles.socialBtn} ${styles.google}`}
55
+ onClick={() => ExternalAuth.login('google')}><span>Sign in with Google</span></IgrButton>
56
+ )}
57
+ {ExternalAuth.hasProvider('facebook') && (
58
+ <IgrButton variant="contained" type="button" className={`${styles.socialBtn} ${styles.facebook}`}
59
+ onClick={() => ExternalAuth.login('facebook')}><span>Sign in with Facebook</span></IgrButton>
60
+ )}
61
+ {ExternalAuth.hasProvider('microsoft') && (
62
+ <IgrButton variant="contained" type="button" className={`${styles.socialBtn} ${styles.microsoft}`}
63
+ onClick={() => ExternalAuth.login('microsoft')}><span>Sign in with Microsoft</span></IgrButton>
64
+ )}
65
+ </div>
66
+ )}
67
+ </form>
68
+ );
69
+ }
@@ -0,0 +1,42 @@
1
+ .loginBtn::part(base) {
2
+ color: #0075d2;
3
+ background: #fff;
4
+ border-color: rgba(0, 117, 210, 0.35);
5
+ font-weight: 600;
6
+ white-space: nowrap;
7
+ transition: background .15s;
8
+ }
9
+
10
+ .loginBtn::part(base):hover {
11
+ background: #e8f3fc;
12
+ }
13
+
14
+ .profileAvatar {
15
+ cursor: pointer;
16
+ color: #0075d2;
17
+ --ig-avatar-background: #fff;
18
+ --ig-avatar-color: #0075d2;
19
+ }
20
+
21
+ :global(igc-dropdown-item:hover),
22
+ :global(igc-dropdown-item[active]:hover) {
23
+ background: #e8f3fc;
24
+ color: #0075d2;
25
+ }
26
+
27
+ :global(igc-dropdown-item[active]) {
28
+ background: #e8f3fc;
29
+ color: #0075d2;
30
+ }
31
+
32
+ :global(igc-dropdown-item[selected]),
33
+ :global(igc-dropdown-item[selected]:hover),
34
+ :global(igc-dropdown-item[selected][active]) {
35
+ background: #e8f3fc;
36
+ color: #0075d2;
37
+ }
38
+
39
+ .profileAvatar:focus-visible {
40
+ outline: 2px solid #fff;
41
+ outline-offset: 2px;
42
+ }
@@ -0,0 +1,44 @@
1
+ import { useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { IgrAvatar, IgrButton, IgrDropdown, IgrDropdownItem } from 'igniteui-react';
4
+ import { useAuth } from '../AuthContext';
5
+ import { LoginDialog } from './LoginDialog';
6
+ import styles from './LoginBar.module.css';
7
+
8
+ export function LoginBar() {
9
+ const { currentUser, initials, logout } = useAuth();
10
+ const navigate = useNavigate();
11
+ const [dialogOpen, setDialogOpen] = useState(false);
12
+
13
+ if (!currentUser) {
14
+ return (
15
+ <>
16
+ <IgrButton variant="outlined" className={styles.loginBtn} onClick={() => setDialogOpen(true)}>
17
+ <span>Log In</span>
18
+ </IgrButton>
19
+ <LoginDialog open={dialogOpen} onClose={() => setDialogOpen(false)} />
20
+ </>
21
+ );
22
+ }
23
+
24
+ return (
25
+ <IgrDropdown placement="bottom-end">
26
+ <IgrAvatar
27
+ slot="target"
28
+ className={styles.profileAvatar}
29
+ shape="circle"
30
+ size="small"
31
+ initials={initials}
32
+ src={currentUser.picture ?? ''}
33
+ tabIndex={0}
34
+ aria-label="Open profile menu"
35
+ />
36
+ <IgrDropdownItem onClick={() => navigate('/auth/profile')}>
37
+ <span>Profile</span>
38
+ </IgrDropdownItem>
39
+ <IgrDropdownItem onClick={() => { logout(); navigate('/'); }}>
40
+ <span>Log Out</span>
41
+ </IgrDropdownItem>
42
+ </IgrDropdown>
43
+ );
44
+ }
@@ -0,0 +1,14 @@
1
+ .dialog::part(base) {
2
+ max-width: 24rem;
3
+ width: calc(100vw - 48px);
4
+ }
5
+
6
+ .dialog::part(title) {
7
+ font-size: 1.125rem;
8
+ font-weight: 600;
9
+ color: #2d2d2d;
10
+ }
11
+
12
+ .body {
13
+ padding: 4px 0;
14
+ }
@@ -0,0 +1,49 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { IgrDialog } from 'igniteui-react';
4
+ import { Login } from './Login';
5
+ import { Register } from './Register';
6
+ import styles from './LoginDialog.module.css';
7
+
8
+ interface LoginDialogProps {
9
+ open: boolean;
10
+ onClose: () => void;
11
+ }
12
+
13
+ export function LoginDialog({ open, onClose }: LoginDialogProps) {
14
+ const [showLogin, setShowLogin] = useState(true);
15
+ const dialogRef = useRef<IgrDialog>(null);
16
+ const navigate = useNavigate();
17
+
18
+ useEffect(() => {
19
+ if (open) {
20
+ setShowLogin(true);
21
+ dialogRef.current?.show();
22
+ } else {
23
+ dialogRef.current?.hide();
24
+ }
25
+ }, [open]);
26
+
27
+ const handleSuccess = () => {
28
+ dialogRef.current?.hide();
29
+ navigate('/auth/profile');
30
+ };
31
+
32
+ return (
33
+ <IgrDialog
34
+ ref={dialogRef}
35
+ className={styles.dialog}
36
+ title={showLogin ? 'Login' : 'Register'}
37
+ closeOnOutsideClick={true}
38
+ onClosed={onClose}
39
+ >
40
+ <div className={styles.body}>
41
+ {showLogin
42
+ ? <Login onRegister={() => setShowLogin(false)} onSuccess={handleSuccess} />
43
+ : <Register onLogin={() => setShowLogin(true)} onSuccess={handleSuccess} />
44
+ }
45
+ </div>
46
+ <span slot="footer" />
47
+ </IgrDialog>
48
+ );
49
+ }
@@ -0,0 +1,74 @@
1
+ .form {
2
+ display: flex;
3
+ flex-flow: column;
4
+ gap: 16px;
5
+ padding: 8px 0 0;
6
+ }
7
+
8
+ .form igc-input {
9
+ width: 100%;
10
+ --ig-input-group-focused-secondary-color: #239ef0;
11
+ --ig-input-group-focused-border-color: #239ef0;
12
+ --ig-input-group-filled-text-color: #2d2d2d;
13
+ }
14
+
15
+ .form igc-input igc-icon {
16
+ color: #0075d2;
17
+ --ig-icon-size: 1.50rem;
18
+ }
19
+
20
+ .error {
21
+ margin: 0;
22
+ font-size: .875rem;
23
+ color: #d32f2f;
24
+ }
25
+
26
+ .actions {
27
+ display: flex;
28
+ flex-flow: column;
29
+ gap: 8px;
30
+ padding-top: 4px;
31
+ }
32
+
33
+ .submitBtn {
34
+ display: block;
35
+ }
36
+
37
+ .submitBtn::part(base) {
38
+ width: 100%;
39
+ min-height: 40px;
40
+ font-weight: 600;
41
+ text-transform: uppercase;
42
+ }
43
+
44
+ .submitBtn:not([disabled])::part(base) {
45
+ background: #239ef0;
46
+ color: #fff;
47
+ }
48
+
49
+ .submitBtn:not([disabled])::part(base):hover {
50
+ background: #1a8fd8;
51
+ }
52
+
53
+ .submitBtn[disabled]::part(base) {
54
+ background: #e0e0e0;
55
+ color: #767676;
56
+ }
57
+
58
+ .linkBtn {
59
+ align-self: center;
60
+ color: #0075d2;
61
+ font-size: .875rem;
62
+ cursor: pointer;
63
+ text-decoration: underline;
64
+ text-transform: none;
65
+ }
66
+
67
+ .linkBtn:hover,
68
+ .linkBtn:focus-visible {
69
+ color: #005da8;
70
+ }
71
+
72
+ .linkBtn::part(base):hover {
73
+ color: #005da8;
74
+ }