@lukoweb/apitogo 0.1.0 → 0.1.2
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/dist/cli/cli.js +37 -147
- package/dist/declarations/lib/ui/Command.d.ts +1 -1
- package/dist/declarations/lib/util/MdxComponents.d.ts +1 -1
- package/package.json +1 -1
- package/src/app/main.tsx +150 -150
- package/src/config/loader.ts +245 -245
- package/src/index.ts +33 -33
- package/src/lib/authentication/components/CallbackHandler.tsx +45 -45
- package/src/lib/authentication/components/SignIn.tsx +46 -46
- package/src/lib/authentication/components/SignUp.tsx +48 -48
- package/src/lib/authentication/providers/clerk.tsx +224 -224
- package/src/lib/authentication/ui/AuthCard.tsx +7 -7
- package/src/lib/authentication/ui/EmailLinkCallbackUi.tsx +129 -125
- package/src/lib/authentication/ui/EmailLinkSentUi.tsx +105 -101
- package/src/lib/authentication/ui/EmailLinkSignInUi.tsx +100 -96
- package/src/lib/authentication/ui/EmailVerificationUi.tsx +128 -124
- package/src/lib/authentication/ui/ZudokuAuthUi.tsx +620 -616
- package/src/lib/components/AiAssistantMenuItems.tsx +102 -102
- package/src/lib/components/Autocomplete.tsx +123 -123
- package/src/lib/components/DeveloperHint.tsx +39 -35
- package/src/lib/components/Header.tsx +260 -260
- package/src/lib/components/Layout.tsx +53 -53
- package/src/lib/components/Main.tsx +51 -51
- package/src/lib/components/Mermaid.tsx +74 -70
- package/src/lib/components/MobileTopNavigation.tsx +276 -276
- package/src/lib/components/MultiSelect.tsx +86 -82
- package/src/lib/components/TopNavigation.tsx +103 -103
- package/src/lib/components/index.ts +26 -26
- package/src/lib/components/navigation/NavigationCategory.tsx +157 -157
- package/src/lib/components/navigation/NavigationFilterInput.tsx +35 -35
- package/src/lib/components/navigation/NavigationItem.tsx +181 -177
- package/src/lib/core/RouteGuard.tsx +193 -193
- package/src/lib/errors/ErrorMessage.tsx +50 -46
- package/src/lib/plugins/api-catalog/Catalog.tsx +80 -80
- package/src/lib/plugins/api-keys/CreateApiKey.tsx +128 -124
- package/src/lib/plugins/api-keys/CreateApiKeyDialog.tsx +49 -49
- package/src/lib/plugins/api-keys/SettingsApiKeys.tsx +72 -72
- package/src/lib/plugins/api-keys/settings/ApiKeyItem.tsx +342 -342
- package/src/lib/plugins/api-keys/settings/RevealApiKey.tsx +127 -127
- package/src/lib/plugins/markdown/MdxPage.tsx +285 -285
- package/src/lib/plugins/openapi/ApiHeader.tsx +96 -96
- package/src/lib/plugins/openapi/CollapsibleCode.tsx +88 -88
- package/src/lib/plugins/openapi/DownloadSchemaButton.tsx +94 -94
- package/src/lib/plugins/openapi/GeneratedExampleSidecarBox.tsx +52 -52
- package/src/lib/plugins/openapi/OperationListItem.tsx +183 -183
- package/src/lib/plugins/openapi/RequestBodySidecarBox.tsx +63 -63
- package/src/lib/plugins/openapi/ResponsesSidecarBox.tsx +120 -117
- package/src/lib/plugins/openapi/SchemaInfo.tsx +344 -344
- package/src/lib/plugins/openapi/SchemaList.tsx +113 -113
- package/src/lib/plugins/openapi/Sidecar.tsx +312 -309
- package/src/lib/plugins/openapi/SidecarExamples.tsx +177 -174
- package/src/lib/plugins/openapi/components/EnumValues.tsx +58 -58
- package/src/lib/plugins/openapi/components/ResponseContent.tsx +117 -114
- package/src/lib/plugins/openapi/playground/BodyPanel.tsx +274 -271
- package/src/lib/plugins/openapi/playground/CollapsibleHeader.tsx +51 -51
- package/src/lib/plugins/openapi/playground/ExamplesDropdown.tsx +60 -60
- package/src/lib/plugins/openapi/playground/Headers.tsx +188 -181
- package/src/lib/plugins/openapi/playground/IdentityDialog.tsx +75 -75
- package/src/lib/plugins/openapi/playground/IdentitySelector.tsx +38 -38
- package/src/lib/plugins/openapi/playground/ParamsGrid.tsx +45 -45
- package/src/lib/plugins/openapi/playground/Playground.tsx +602 -599
- package/src/lib/plugins/openapi/playground/PlaygroundDialog.tsx +56 -56
- package/src/lib/plugins/openapi/playground/QueryParams.tsx +151 -148
- package/src/lib/plugins/openapi/playground/RequestLoginDialog.tsx +70 -70
- package/src/lib/plugins/openapi/playground/request-panel/MultipartField.tsx +91 -91
- package/src/lib/plugins/openapi/playground/result-panel/AudioPlayer.tsx +50 -50
- package/src/lib/plugins/openapi/playground/result-panel/ResponseTab.tsx +355 -355
- package/src/lib/plugins/openapi/playground/result-panel/ResultPanel.tsx +106 -102
- package/src/lib/plugins/openapi/schema/SchemaPropertyItem.tsx +189 -184
- package/src/lib/plugins/openapi/schema/SchemaView.tsx +299 -299
- package/src/lib/plugins/search-pagefind/IndexingDialog.tsx +162 -162
- package/src/lib/plugins/search-pagefind/PagefindSearch.tsx +202 -202
- package/src/lib/plugins/search-pagefind/ResultList.tsx +122 -118
- package/src/lib/ui/ActionButton.tsx +21 -21
- package/src/lib/ui/Command.tsx +191 -191
- package/src/lib/util/MdxComponents.tsx +150 -150
- package/src/vite/config.ts +234 -231
- package/src/vite/dev-server.ts +291 -291
- package/src/vite/plugin-api-keys.ts +50 -50
- package/src/vite/plugin-api.ts +320 -318
- package/src/vite/plugin-auth.ts +38 -38
- package/src/vite/plugin-component.ts +45 -39
- package/src/vite/plugin-config.ts +75 -75
- package/src/vite/plugin-custom-pages.ts +36 -36
- package/src/vite/plugin-docs.ts +202 -202
- package/src/vite/plugin-markdown-export.ts +214 -214
- package/src/vite/plugin-mdx.ts +149 -149
- package/src/vite/plugin-navigation.ts +106 -106
- package/src/vite/plugin-search.ts +47 -47
- package/src/vite/plugin.ts +42 -42
- package/src/vite/prerender/prerender.ts +233 -233
|
@@ -1,193 +1,193 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} from "
|
|
19
|
-
import { REASON_CODES } from "../../config/validators/reason-codes.js";
|
|
20
|
-
import { useAuth } from "../authentication/hook.js";
|
|
21
|
-
import { RenderContext } from "../components/context/RenderContext.js";
|
|
22
|
-
import { useZudoku } from "../components/context/ZudokuContext.js";
|
|
23
|
-
import { Layout } from "../components/Layout.js";
|
|
24
|
-
import { ZudokuError } from "../util/invariant.js";
|
|
25
|
-
|
|
26
|
-
export const SEARCH_PROTECTED_SECTION = "protected";
|
|
27
|
-
|
|
28
|
-
type LoginDialogProps = {
|
|
29
|
-
open: boolean;
|
|
30
|
-
onCancel: () => void;
|
|
31
|
-
onLogin: () => void;
|
|
32
|
-
onRegister: () => void;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const LoginDialog = ({
|
|
36
|
-
open,
|
|
37
|
-
onCancel,
|
|
38
|
-
onLogin,
|
|
39
|
-
onRegister,
|
|
40
|
-
}: LoginDialogProps) => (
|
|
41
|
-
<Dialog open={open} onOpenChange={(nextOpen) => !nextOpen && onCancel()}>
|
|
42
|
-
<DialogContent>
|
|
43
|
-
<DialogHeader>
|
|
44
|
-
<DialogTitle>Login to continue</DialogTitle>
|
|
45
|
-
</DialogHeader>
|
|
46
|
-
<DialogDescription>Please login to access this page.</DialogDescription>
|
|
47
|
-
<DialogFooter>
|
|
48
|
-
<Button variant="outline" onClick={onCancel}>
|
|
49
|
-
Cancel
|
|
50
|
-
</Button>
|
|
51
|
-
<div className="w-full" />
|
|
52
|
-
<Button variant="secondary" onClick={onRegister}>
|
|
53
|
-
Register
|
|
54
|
-
</Button>
|
|
55
|
-
<Button onClick={onLogin}>Login</Button>
|
|
56
|
-
</DialogFooter>
|
|
57
|
-
</DialogContent>
|
|
58
|
-
</Dialog>
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
const BypassRoute = ({ isProtectedRoute }: { isProtectedRoute: boolean }) => (
|
|
62
|
-
<>
|
|
63
|
-
{isProtectedRoute && (
|
|
64
|
-
<Helmet>
|
|
65
|
-
<meta
|
|
66
|
-
name="pagefind"
|
|
67
|
-
data-pagefind-filter={`section:${SEARCH_PROTECTED_SECTION}`}
|
|
68
|
-
content="true"
|
|
69
|
-
/>
|
|
70
|
-
</Helmet>
|
|
71
|
-
)}
|
|
72
|
-
<Outlet />
|
|
73
|
-
</>
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
const ForbiddenPage = () => {
|
|
77
|
-
const renderContext = use(RenderContext);
|
|
78
|
-
renderContext.status = 403;
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<Layout>
|
|
82
|
-
<div className="flex flex-col items-center justify-center min-h-[50vh] gap-4">
|
|
83
|
-
<h1 className="text-2xl font-bold">Access Denied</h1>
|
|
84
|
-
<p className="text-muted-foreground">
|
|
85
|
-
You don't have permission to access this page.
|
|
86
|
-
</p>
|
|
87
|
-
</div>
|
|
88
|
-
</Layout>
|
|
89
|
-
);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
export const RouteGuard = () => {
|
|
93
|
-
const auth = useAuth();
|
|
94
|
-
const zudoku = useZudoku();
|
|
95
|
-
const navigate = useNavigate();
|
|
96
|
-
const location = useLocation();
|
|
97
|
-
const renderContext = use(RenderContext);
|
|
98
|
-
const shouldBypass = renderContext.bypassProtection;
|
|
99
|
-
const authCheckContext = useMemo(
|
|
100
|
-
() => ({ auth, context: zudoku, reasonCode: REASON_CODES }),
|
|
101
|
-
[auth, zudoku],
|
|
102
|
-
);
|
|
103
|
-
const { protectedRoutes } = zudoku;
|
|
104
|
-
|
|
105
|
-
const getAuthCheck = useCallback(
|
|
106
|
-
(pathname: string) => {
|
|
107
|
-
if (!protectedRoutes) return;
|
|
108
|
-
for (const [pattern, check] of Object.entries(protectedRoutes)) {
|
|
109
|
-
if (matchPath({ path: pattern, end: true }, pathname)) {
|
|
110
|
-
return check;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
[protectedRoutes],
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
const currentAuthCheck = getAuthCheck(location.pathname);
|
|
118
|
-
const isProtectedRoute = currentAuthCheck !== undefined;
|
|
119
|
-
const rawResult = currentAuthCheck?.(authCheckContext) ?? true;
|
|
120
|
-
// Normalize: false is equivalent to UNAUTHORIZED
|
|
121
|
-
const authResult =
|
|
122
|
-
rawResult === false ? REASON_CODES.UNAUTHORIZED : rawResult;
|
|
123
|
-
const isForbidden = authResult === REASON_CODES.FORBIDDEN;
|
|
124
|
-
const needsToSignIn = authResult === REASON_CODES.UNAUTHORIZED;
|
|
125
|
-
|
|
126
|
-
const blocker = useBlocker(({ nextLocation }) => {
|
|
127
|
-
if (shouldBypass) return false;
|
|
128
|
-
const check = getAuthCheck(nextLocation.pathname);
|
|
129
|
-
if (!check) return false;
|
|
130
|
-
const result = check(authCheckContext);
|
|
131
|
-
// Only block for unauthorized (needs login), not for reason codes like "forbidden"
|
|
132
|
-
return result === false || result === REASON_CODES.UNAUTHORIZED;
|
|
133
|
-
});
|
|
134
|
-
const isBlocked = blocker.state === "blocked";
|
|
135
|
-
|
|
136
|
-
// Proceed after successful login
|
|
137
|
-
useEffect(() => {
|
|
138
|
-
if (!auth.isAuthenticated || !isBlocked) return;
|
|
139
|
-
const check = getAuthCheck(blocker.location.pathname);
|
|
140
|
-
if (!check) {
|
|
141
|
-
blocker.proceed?.();
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
const result = check(authCheckContext);
|
|
145
|
-
// Proceed whenever the result is no longer UNAUTHORIZED (e.g. true or FORBIDDEN)
|
|
146
|
-
if (result !== false && result !== REASON_CODES.UNAUTHORIZED) {
|
|
147
|
-
blocker.proceed?.();
|
|
148
|
-
}
|
|
149
|
-
}, [
|
|
150
|
-
auth.isAuthenticated,
|
|
151
|
-
isBlocked,
|
|
152
|
-
blocker,
|
|
153
|
-
authCheckContext,
|
|
154
|
-
getAuthCheck,
|
|
155
|
-
]);
|
|
156
|
-
|
|
157
|
-
if (isForbidden) {
|
|
158
|
-
return <ForbiddenPage />;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (shouldBypass) {
|
|
162
|
-
return <BypassRoute isProtectedRoute={isProtectedRoute} />;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (isProtectedRoute && !auth.isAuthEnabled) {
|
|
166
|
-
throw new ZudokuError("Authentication is not enabled", {
|
|
167
|
-
title: "Authentication is not enabled",
|
|
168
|
-
developerHint:
|
|
169
|
-
"To use protectedRoutes you need authentication to be enabled",
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (needsToSignIn && auth.isPending && typeof window !== "undefined") {
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const showDialog = needsToSignIn || isBlocked;
|
|
178
|
-
const redirectTo = isBlocked
|
|
179
|
-
? blocker.location.pathname + blocker.location.search
|
|
180
|
-
: location.pathname + location.search;
|
|
181
|
-
|
|
182
|
-
return (
|
|
183
|
-
<>
|
|
184
|
-
{!needsToSignIn && <Outlet />}
|
|
185
|
-
<LoginDialog
|
|
186
|
-
open={showDialog}
|
|
187
|
-
onCancel={needsToSignIn ? () => navigate(-1) : () => blocker.reset?.()}
|
|
188
|
-
onLogin={() => void auth.login({ redirectTo })}
|
|
189
|
-
onRegister={() => void auth.signup({ redirectTo })}
|
|
190
|
-
/>
|
|
191
|
-
</>
|
|
192
|
-
);
|
|
193
|
-
};
|
|
1
|
+
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogContent,
|
|
5
|
+
DialogDescription,
|
|
6
|
+
DialogFooter,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
} from "@lukoweb/apitogo/ui/Dialog.js";
|
|
10
|
+
import { Helmet } from "@zudoku/react-helmet-async";
|
|
11
|
+
import { use, useCallback, useEffect, useMemo } from "react";
|
|
12
|
+
import {
|
|
13
|
+
matchPath,
|
|
14
|
+
Outlet,
|
|
15
|
+
useBlocker,
|
|
16
|
+
useLocation,
|
|
17
|
+
useNavigate,
|
|
18
|
+
} from "react-router";
|
|
19
|
+
import { REASON_CODES } from "../../config/validators/reason-codes.js";
|
|
20
|
+
import { useAuth } from "../authentication/hook.js";
|
|
21
|
+
import { RenderContext } from "../components/context/RenderContext.js";
|
|
22
|
+
import { useZudoku } from "../components/context/ZudokuContext.js";
|
|
23
|
+
import { Layout } from "../components/Layout.js";
|
|
24
|
+
import { ZudokuError } from "../util/invariant.js";
|
|
25
|
+
|
|
26
|
+
export const SEARCH_PROTECTED_SECTION = "protected";
|
|
27
|
+
|
|
28
|
+
type LoginDialogProps = {
|
|
29
|
+
open: boolean;
|
|
30
|
+
onCancel: () => void;
|
|
31
|
+
onLogin: () => void;
|
|
32
|
+
onRegister: () => void;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const LoginDialog = ({
|
|
36
|
+
open,
|
|
37
|
+
onCancel,
|
|
38
|
+
onLogin,
|
|
39
|
+
onRegister,
|
|
40
|
+
}: LoginDialogProps) => (
|
|
41
|
+
<Dialog open={open} onOpenChange={(nextOpen) => !nextOpen && onCancel()}>
|
|
42
|
+
<DialogContent>
|
|
43
|
+
<DialogHeader>
|
|
44
|
+
<DialogTitle>Login to continue</DialogTitle>
|
|
45
|
+
</DialogHeader>
|
|
46
|
+
<DialogDescription>Please login to access this page.</DialogDescription>
|
|
47
|
+
<DialogFooter>
|
|
48
|
+
<Button variant="outline" onClick={onCancel}>
|
|
49
|
+
Cancel
|
|
50
|
+
</Button>
|
|
51
|
+
<div className="w-full" />
|
|
52
|
+
<Button variant="secondary" onClick={onRegister}>
|
|
53
|
+
Register
|
|
54
|
+
</Button>
|
|
55
|
+
<Button onClick={onLogin}>Login</Button>
|
|
56
|
+
</DialogFooter>
|
|
57
|
+
</DialogContent>
|
|
58
|
+
</Dialog>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const BypassRoute = ({ isProtectedRoute }: { isProtectedRoute: boolean }) => (
|
|
62
|
+
<>
|
|
63
|
+
{isProtectedRoute && (
|
|
64
|
+
<Helmet>
|
|
65
|
+
<meta
|
|
66
|
+
name="pagefind"
|
|
67
|
+
data-pagefind-filter={`section:${SEARCH_PROTECTED_SECTION}`}
|
|
68
|
+
content="true"
|
|
69
|
+
/>
|
|
70
|
+
</Helmet>
|
|
71
|
+
)}
|
|
72
|
+
<Outlet />
|
|
73
|
+
</>
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const ForbiddenPage = () => {
|
|
77
|
+
const renderContext = use(RenderContext);
|
|
78
|
+
renderContext.status = 403;
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Layout>
|
|
82
|
+
<div className="flex flex-col items-center justify-center min-h-[50vh] gap-4">
|
|
83
|
+
<h1 className="text-2xl font-bold">Access Denied</h1>
|
|
84
|
+
<p className="text-muted-foreground">
|
|
85
|
+
You don't have permission to access this page.
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
</Layout>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const RouteGuard = () => {
|
|
93
|
+
const auth = useAuth();
|
|
94
|
+
const zudoku = useZudoku();
|
|
95
|
+
const navigate = useNavigate();
|
|
96
|
+
const location = useLocation();
|
|
97
|
+
const renderContext = use(RenderContext);
|
|
98
|
+
const shouldBypass = renderContext.bypassProtection;
|
|
99
|
+
const authCheckContext = useMemo(
|
|
100
|
+
() => ({ auth, context: zudoku, reasonCode: REASON_CODES }),
|
|
101
|
+
[auth, zudoku],
|
|
102
|
+
);
|
|
103
|
+
const { protectedRoutes } = zudoku;
|
|
104
|
+
|
|
105
|
+
const getAuthCheck = useCallback(
|
|
106
|
+
(pathname: string) => {
|
|
107
|
+
if (!protectedRoutes) return;
|
|
108
|
+
for (const [pattern, check] of Object.entries(protectedRoutes)) {
|
|
109
|
+
if (matchPath({ path: pattern, end: true }, pathname)) {
|
|
110
|
+
return check;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
[protectedRoutes],
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const currentAuthCheck = getAuthCheck(location.pathname);
|
|
118
|
+
const isProtectedRoute = currentAuthCheck !== undefined;
|
|
119
|
+
const rawResult = currentAuthCheck?.(authCheckContext) ?? true;
|
|
120
|
+
// Normalize: false is equivalent to UNAUTHORIZED
|
|
121
|
+
const authResult =
|
|
122
|
+
rawResult === false ? REASON_CODES.UNAUTHORIZED : rawResult;
|
|
123
|
+
const isForbidden = authResult === REASON_CODES.FORBIDDEN;
|
|
124
|
+
const needsToSignIn = authResult === REASON_CODES.UNAUTHORIZED;
|
|
125
|
+
|
|
126
|
+
const blocker = useBlocker(({ nextLocation }) => {
|
|
127
|
+
if (shouldBypass) return false;
|
|
128
|
+
const check = getAuthCheck(nextLocation.pathname);
|
|
129
|
+
if (!check) return false;
|
|
130
|
+
const result = check(authCheckContext);
|
|
131
|
+
// Only block for unauthorized (needs login), not for reason codes like "forbidden"
|
|
132
|
+
return result === false || result === REASON_CODES.UNAUTHORIZED;
|
|
133
|
+
});
|
|
134
|
+
const isBlocked = blocker.state === "blocked";
|
|
135
|
+
|
|
136
|
+
// Proceed after successful login
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (!auth.isAuthenticated || !isBlocked) return;
|
|
139
|
+
const check = getAuthCheck(blocker.location.pathname);
|
|
140
|
+
if (!check) {
|
|
141
|
+
blocker.proceed?.();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const result = check(authCheckContext);
|
|
145
|
+
// Proceed whenever the result is no longer UNAUTHORIZED (e.g. true or FORBIDDEN)
|
|
146
|
+
if (result !== false && result !== REASON_CODES.UNAUTHORIZED) {
|
|
147
|
+
blocker.proceed?.();
|
|
148
|
+
}
|
|
149
|
+
}, [
|
|
150
|
+
auth.isAuthenticated,
|
|
151
|
+
isBlocked,
|
|
152
|
+
blocker,
|
|
153
|
+
authCheckContext,
|
|
154
|
+
getAuthCheck,
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
if (isForbidden) {
|
|
158
|
+
return <ForbiddenPage />;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (shouldBypass) {
|
|
162
|
+
return <BypassRoute isProtectedRoute={isProtectedRoute} />;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (isProtectedRoute && !auth.isAuthEnabled) {
|
|
166
|
+
throw new ZudokuError("Authentication is not enabled", {
|
|
167
|
+
title: "Authentication is not enabled",
|
|
168
|
+
developerHint:
|
|
169
|
+
"To use protectedRoutes you need authentication to be enabled",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (needsToSignIn && auth.isPending && typeof window !== "undefined") {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const showDialog = needsToSignIn || isBlocked;
|
|
178
|
+
const redirectTo = isBlocked
|
|
179
|
+
? blocker.location.pathname + blocker.location.search
|
|
180
|
+
: location.pathname + location.search;
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<>
|
|
184
|
+
{!needsToSignIn && <Outlet />}
|
|
185
|
+
<LoginDialog
|
|
186
|
+
open={showDialog}
|
|
187
|
+
onCancel={needsToSignIn ? () => navigate(-1) : () => blocker.reset?.()}
|
|
188
|
+
onLogin={() => void auth.login({ redirectTo })}
|
|
189
|
+
onRegister={() => void auth.signup({ redirectTo })}
|
|
190
|
+
/>
|
|
191
|
+
</>
|
|
192
|
+
);
|
|
193
|
+
};
|
|
@@ -1,46 +1,50 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
1
|
+
import {
|
|
2
|
+
Alert,
|
|
3
|
+
AlertDescription,
|
|
4
|
+
AlertTitle,
|
|
5
|
+
} from "@lukoweb/apitogo/ui/Alert.js";
|
|
6
|
+
import { Suspense, lazy } from "react";
|
|
7
|
+
import { DeveloperHint } from "../components/DeveloperHint.js";
|
|
8
|
+
import { ZudokuError } from "../util/invariant.js";
|
|
9
|
+
|
|
10
|
+
const SyntaxHighlight = lazy(() =>
|
|
11
|
+
import("@lukoweb/apitogo/ui/SyntaxHighlight.js").then((m) => ({
|
|
12
|
+
default: m.SyntaxHighlight,
|
|
13
|
+
})),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
export function ErrorMessage({ error }: { error: unknown }) {
|
|
17
|
+
const message =
|
|
18
|
+
error instanceof Error ? error.message : "Something went wrong";
|
|
19
|
+
|
|
20
|
+
const showDeveloperHints = process.env.NODE_ENV === "development";
|
|
21
|
+
|
|
22
|
+
const hint = error instanceof ZudokuError ? error.developerHint : undefined;
|
|
23
|
+
const title =
|
|
24
|
+
error instanceof ZudokuError ? error.title : "Something went wrong";
|
|
25
|
+
const stack = error instanceof Error ? error.stack : undefined;
|
|
26
|
+
const cause = error instanceof Error ? error.cause : undefined;
|
|
27
|
+
|
|
28
|
+
const stringError = cause instanceof Error ? String(cause.stack) : stack;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
<Alert variant="destructive">
|
|
33
|
+
<AlertTitle>{title}</AlertTitle>
|
|
34
|
+
<AlertDescription>{message}</AlertDescription>
|
|
35
|
+
</Alert>
|
|
36
|
+
{showDeveloperHints && hint && (
|
|
37
|
+
<DeveloperHint className="mb-4">{hint}</DeveloperHint>
|
|
38
|
+
)}
|
|
39
|
+
{showDeveloperHints && stringError && (
|
|
40
|
+
<Suspense>
|
|
41
|
+
<SyntaxHighlight
|
|
42
|
+
className="max-h-[400px] [&>pre]:p-4"
|
|
43
|
+
language="js"
|
|
44
|
+
code={stringError}
|
|
45
|
+
/>
|
|
46
|
+
</Suspense>
|
|
47
|
+
)}
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { useAuthState } from "../../authentication/state.js";
|
|
6
|
-
import { Heading } from "../../components/Heading.js";
|
|
7
|
-
import { Markdown } from "../../components/Markdown.js";
|
|
8
|
-
import { joinUrl } from "../../util/joinUrl.js";
|
|
9
|
-
import { type ApiCatalogPluginOptions, getKey } from "./index.js";
|
|
10
|
-
|
|
11
|
-
export const Catalog = ({
|
|
12
|
-
items,
|
|
13
|
-
filterCatalogItems = (items) => items,
|
|
14
|
-
label = "API Library",
|
|
15
|
-
categoryLabel,
|
|
16
|
-
}: Omit<ApiCatalogPluginOptions, "path"> & {
|
|
17
|
-
categoryLabel?: string;
|
|
18
|
-
}) => {
|
|
19
|
-
const auth = useAuthState();
|
|
20
|
-
const match = useMatch({ path: "/catalog/:category" });
|
|
21
|
-
const activeCategory = match?.params.category;
|
|
22
|
-
|
|
23
|
-
const catalogItems = useSuspenseQuery({
|
|
24
|
-
queryFn: () => filterCatalogItems(items, { auth }),
|
|
25
|
-
queryKey: ["catalogItems", auth],
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
// Only index the overview page, ignore the rest
|
|
29
|
-
const dataSet = activeCategory ? { "data-pagefind-ignore": "all" } : {};
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<section className="pt-(--padding-content-top)" {...dataSet}>
|
|
33
|
-
<Helmet>
|
|
34
|
-
<title>
|
|
35
|
-
{categoryLabel ? `${categoryLabel} - ` : ""}
|
|
36
|
-
{label}
|
|
37
|
-
</title>
|
|
38
|
-
</Helmet>
|
|
39
|
-
<div className="grid gap-4">
|
|
40
|
-
<Heading level={2}>
|
|
41
|
-
{label}
|
|
42
|
-
{categoryLabel && ` - ${categoryLabel}`}
|
|
43
|
-
</Heading>
|
|
44
|
-
|
|
45
|
-
<div className="grid grid-cols-2 gap-4">
|
|
46
|
-
{catalogItems.data
|
|
47
|
-
.filter(
|
|
48
|
-
(api) =>
|
|
49
|
-
!activeCategory ||
|
|
50
|
-
api.categories.find((c) =>
|
|
51
|
-
c.tags.find((t) => getKey(c.label, t) === activeCategory),
|
|
52
|
-
),
|
|
53
|
-
)
|
|
54
|
-
.map((api) => (
|
|
55
|
-
<Link
|
|
56
|
-
to={joinUrl(api.path)}
|
|
57
|
-
className="no-underline hover:!text-foreground"
|
|
58
|
-
key={api.path}
|
|
59
|
-
>
|
|
60
|
-
<div className="border h-full rounded-lg p-4 flex flex-col gap-2 cursor-pointer hover:bg-border/20 font-normal">
|
|
61
|
-
<span className="font-semibold">{api.label}</span>
|
|
62
|
-
<Markdown
|
|
63
|
-
className="text-sm whitespace-pre-wrap mb-6 line-clamp-2"
|
|
64
|
-
content={api.description}
|
|
65
|
-
components={{
|
|
66
|
-
// Because we're wrapping the description in a Link already,
|
|
67
|
-
// we need to strip out other links to not get a hydration error, like:
|
|
68
|
-
// > In HTML, <a> cannot be a descendant of <a>.
|
|
69
|
-
// > This will cause a hydration error.
|
|
70
|
-
a: (props) => <span {...props} />,
|
|
71
|
-
}}
|
|
72
|
-
/>
|
|
73
|
-
</div>
|
|
74
|
-
</Link>
|
|
75
|
-
))}
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
</section>
|
|
79
|
-
);
|
|
80
|
-
};
|
|
1
|
+
import { Link } from "@lukoweb/apitogo/components";
|
|
2
|
+
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
3
|
+
import { Helmet } from "@zudoku/react-helmet-async";
|
|
4
|
+
import { useMatch } from "react-router";
|
|
5
|
+
import { useAuthState } from "../../authentication/state.js";
|
|
6
|
+
import { Heading } from "../../components/Heading.js";
|
|
7
|
+
import { Markdown } from "../../components/Markdown.js";
|
|
8
|
+
import { joinUrl } from "../../util/joinUrl.js";
|
|
9
|
+
import { type ApiCatalogPluginOptions, getKey } from "./index.js";
|
|
10
|
+
|
|
11
|
+
export const Catalog = ({
|
|
12
|
+
items,
|
|
13
|
+
filterCatalogItems = (items) => items,
|
|
14
|
+
label = "API Library",
|
|
15
|
+
categoryLabel,
|
|
16
|
+
}: Omit<ApiCatalogPluginOptions, "path"> & {
|
|
17
|
+
categoryLabel?: string;
|
|
18
|
+
}) => {
|
|
19
|
+
const auth = useAuthState();
|
|
20
|
+
const match = useMatch({ path: "/catalog/:category" });
|
|
21
|
+
const activeCategory = match?.params.category;
|
|
22
|
+
|
|
23
|
+
const catalogItems = useSuspenseQuery({
|
|
24
|
+
queryFn: () => filterCatalogItems(items, { auth }),
|
|
25
|
+
queryKey: ["catalogItems", auth],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Only index the overview page, ignore the rest
|
|
29
|
+
const dataSet = activeCategory ? { "data-pagefind-ignore": "all" } : {};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<section className="pt-(--padding-content-top)" {...dataSet}>
|
|
33
|
+
<Helmet>
|
|
34
|
+
<title>
|
|
35
|
+
{categoryLabel ? `${categoryLabel} - ` : ""}
|
|
36
|
+
{label}
|
|
37
|
+
</title>
|
|
38
|
+
</Helmet>
|
|
39
|
+
<div className="grid gap-4">
|
|
40
|
+
<Heading level={2}>
|
|
41
|
+
{label}
|
|
42
|
+
{categoryLabel && ` - ${categoryLabel}`}
|
|
43
|
+
</Heading>
|
|
44
|
+
|
|
45
|
+
<div className="grid grid-cols-2 gap-4">
|
|
46
|
+
{catalogItems.data
|
|
47
|
+
.filter(
|
|
48
|
+
(api) =>
|
|
49
|
+
!activeCategory ||
|
|
50
|
+
api.categories.find((c) =>
|
|
51
|
+
c.tags.find((t) => getKey(c.label, t) === activeCategory),
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
.map((api) => (
|
|
55
|
+
<Link
|
|
56
|
+
to={joinUrl(api.path)}
|
|
57
|
+
className="no-underline hover:!text-foreground"
|
|
58
|
+
key={api.path}
|
|
59
|
+
>
|
|
60
|
+
<div className="border h-full rounded-lg p-4 flex flex-col gap-2 cursor-pointer hover:bg-border/20 font-normal">
|
|
61
|
+
<span className="font-semibold">{api.label}</span>
|
|
62
|
+
<Markdown
|
|
63
|
+
className="text-sm whitespace-pre-wrap mb-6 line-clamp-2"
|
|
64
|
+
content={api.description}
|
|
65
|
+
components={{
|
|
66
|
+
// Because we're wrapping the description in a Link already,
|
|
67
|
+
// we need to strip out other links to not get a hydration error, like:
|
|
68
|
+
// > In HTML, <a> cannot be a descendant of <a>.
|
|
69
|
+
// > This will cause a hydration error.
|
|
70
|
+
a: (props) => <span {...props} />,
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
</Link>
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</section>
|
|
79
|
+
);
|
|
80
|
+
};
|