@lukoweb/apitogo-module-user-panel 0.1.37
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/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lukoweb/apitogo-module-user-panel",
|
|
3
|
+
"version": "0.1.37",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": "./src/index.tsx"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"typecheck": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"lucide-react": "0.577.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/react": "19.2.14",
|
|
16
|
+
"@types/react-dom": "19.2.3",
|
|
17
|
+
"@lukoweb/apitogo": "0.1.37",
|
|
18
|
+
"react": "19.2.4",
|
|
19
|
+
"react-dom": "19.2.4",
|
|
20
|
+
"react-router": "7.13.1",
|
|
21
|
+
"typescript": "5.9.3"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@lukoweb/apitogo": "*",
|
|
25
|
+
"react": ">=19.2.0",
|
|
26
|
+
"react-dom": ">=19.2.0"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"src"
|
|
33
|
+
]
|
|
34
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { userPanelModule } from "./userPanelModule.js";
|
|
2
|
+
export type { UserPanelModuleOptions } from "./userPanelModule.js";
|
|
3
|
+
export { DefaultDashboardPage } from "./pages/DefaultDashboardPage.js";
|
|
4
|
+
export { DefaultProfilePage } from "./pages/DefaultProfilePage.js";
|
|
5
|
+
export { DefaultPlansPage } from "./pages/DefaultPlansPage.js";
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ResolvedDashboardContent } from "@lukoweb/apitogo";
|
|
2
|
+
import { Head, Link } from "@lukoweb/apitogo/components";
|
|
3
|
+
import { useAuth } from "@lukoweb/apitogo/hooks";
|
|
4
|
+
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
5
|
+
import {
|
|
6
|
+
ArrowRightIcon,
|
|
7
|
+
BookOpenIcon,
|
|
8
|
+
CodeIcon,
|
|
9
|
+
LayoutDashboardIcon,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
|
|
12
|
+
export type DefaultDashboardPageProps = {
|
|
13
|
+
content: ResolvedDashboardContent;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const DefaultDashboardPage = ({
|
|
17
|
+
content,
|
|
18
|
+
}: DefaultDashboardPageProps) => {
|
|
19
|
+
const auth = useAuth();
|
|
20
|
+
const title = content.title ?? "Dashboard";
|
|
21
|
+
const subtitle = content.subtitle;
|
|
22
|
+
const description =
|
|
23
|
+
content.description ??
|
|
24
|
+
"Overview of your account activity, documentation shortcuts, and next steps.";
|
|
25
|
+
const quickLinks = content.quickLinks ?? [];
|
|
26
|
+
|
|
27
|
+
const greeting = auth.isAuthenticated
|
|
28
|
+
? auth.profile?.name || auth.profile?.email
|
|
29
|
+
: undefined;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="mx-auto max-w-screen-xl px-4 pb-16 pt-(--padding-content-top) lg:px-8">
|
|
33
|
+
<Head>
|
|
34
|
+
<title>{title}</title>
|
|
35
|
+
</Head>
|
|
36
|
+
|
|
37
|
+
<div className="mb-8 flex items-start gap-4">
|
|
38
|
+
<div className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
|
|
39
|
+
<LayoutDashboardIcon size={24} />
|
|
40
|
+
</div>
|
|
41
|
+
<div className="space-y-1">
|
|
42
|
+
{subtitle ? (
|
|
43
|
+
<p className="text-sm font-medium tracking-wide text-muted-foreground uppercase">
|
|
44
|
+
{subtitle}
|
|
45
|
+
</p>
|
|
46
|
+
) : null}
|
|
47
|
+
<h1 className="text-3xl font-semibold tracking-tight">
|
|
48
|
+
{greeting ? `Welcome back, ${greeting}` : title}
|
|
49
|
+
</h1>
|
|
50
|
+
<p className="max-w-2xl text-muted-foreground">{description}</p>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{quickLinks.length > 0 ? (
|
|
55
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
56
|
+
{quickLinks.map((link) => (
|
|
57
|
+
<article
|
|
58
|
+
key={link.href}
|
|
59
|
+
className="rounded-xl border bg-card/60 p-6 shadow-xs"
|
|
60
|
+
>
|
|
61
|
+
<div className="mb-4 flex size-10 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
62
|
+
{link.href.includes("api") ? (
|
|
63
|
+
<CodeIcon size={20} />
|
|
64
|
+
) : (
|
|
65
|
+
<BookOpenIcon size={20} />
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
<h2 className="text-lg font-medium">{link.label}</h2>
|
|
69
|
+
<Button asChild variant="link" className="mt-2 h-auto p-0">
|
|
70
|
+
<Link to={link.href}>
|
|
71
|
+
Open
|
|
72
|
+
<ArrowRightIcon className="ms-1" size={16} />
|
|
73
|
+
</Link>
|
|
74
|
+
</Button>
|
|
75
|
+
</article>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
) : null}
|
|
79
|
+
|
|
80
|
+
{!auth.isAuthenticated && auth.isAuthEnabled ? (
|
|
81
|
+
<p className="mt-8 text-sm text-muted-foreground">
|
|
82
|
+
Sign in to see personalized account information on your dashboard.
|
|
83
|
+
</p>
|
|
84
|
+
) : null}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ResolvedPlansContent } from "@lukoweb/apitogo";
|
|
2
|
+
import { Button, Head } from "@lukoweb/apitogo/components";
|
|
3
|
+
import { Link } from "@lukoweb/apitogo/router";
|
|
4
|
+
import { CreditCardIcon } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
export type DefaultPlansPageProps = {
|
|
7
|
+
content: ResolvedPlansContent;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const DefaultPlansPage = ({ content }: DefaultPlansPageProps) => {
|
|
11
|
+
const title = content.title ?? "Your subscription";
|
|
12
|
+
const description =
|
|
13
|
+
content.description ??
|
|
14
|
+
"Review billing details, manage your subscription, and change plans.";
|
|
15
|
+
const tiers = content.tiers ?? [];
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="mx-auto max-w-screen-xl px-4 pb-16 pt-(--padding-content-top) lg:px-8">
|
|
19
|
+
<Head>
|
|
20
|
+
<title>{title}</title>
|
|
21
|
+
</Head>
|
|
22
|
+
|
|
23
|
+
<div className="mb-8 flex flex-wrap items-start justify-between gap-4">
|
|
24
|
+
<div className="flex items-start gap-4">
|
|
25
|
+
<div className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
|
|
26
|
+
<CreditCardIcon size={24} />
|
|
27
|
+
</div>
|
|
28
|
+
<div className="space-y-1">
|
|
29
|
+
<h1 className="text-3xl font-semibold tracking-tight">{title}</h1>
|
|
30
|
+
<p className="max-w-2xl text-muted-foreground">{description}</p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<Button variant="outline" asChild>
|
|
34
|
+
<Link to="/pricing">View all plans</Link>
|
|
35
|
+
</Button>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{tiers.length > 0 ? (
|
|
39
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
40
|
+
{tiers.map((tier) => (
|
|
41
|
+
<article
|
|
42
|
+
key={tier.name}
|
|
43
|
+
className="rounded-xl border bg-card/60 p-6 shadow-xs"
|
|
44
|
+
>
|
|
45
|
+
<h2 className="text-lg font-medium">{tier.name}</h2>
|
|
46
|
+
{tier.price ? (
|
|
47
|
+
<p className="mt-2 text-2xl font-semibold">{tier.price}</p>
|
|
48
|
+
) : null}
|
|
49
|
+
{tier.description ? (
|
|
50
|
+
<p className="mt-3 text-sm text-muted-foreground">
|
|
51
|
+
{tier.description}
|
|
52
|
+
</p>
|
|
53
|
+
) : null}
|
|
54
|
+
</article>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
) : (
|
|
58
|
+
<div className="rounded-xl border border-dashed bg-muted/20 p-8 text-center">
|
|
59
|
+
<p className="text-sm text-muted-foreground">
|
|
60
|
+
Compare available plans on the public pricing page, or add billing
|
|
61
|
+
notes in{" "}
|
|
62
|
+
<code className="rounded bg-muted px-1.5 py-0.5">
|
|
63
|
+
modules.userPanel.plans.content
|
|
64
|
+
</code>
|
|
65
|
+
.
|
|
66
|
+
</p>
|
|
67
|
+
<Button className="mt-4" asChild>
|
|
68
|
+
<Link to="/pricing">Go to pricing</Link>
|
|
69
|
+
</Button>
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ResolvedProfileContent } from "@lukoweb/apitogo";
|
|
2
|
+
import { Head, Link } from "@lukoweb/apitogo/components";
|
|
3
|
+
import { useAuth } from "@lukoweb/apitogo/hooks";
|
|
4
|
+
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
5
|
+
import { UserIcon } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
export type DefaultProfilePageProps = {
|
|
8
|
+
content: ResolvedProfileContent;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const DefaultProfilePage = ({ content }: DefaultProfilePageProps) => {
|
|
12
|
+
const auth = useAuth();
|
|
13
|
+
const title = content.title ?? "Profile";
|
|
14
|
+
const description =
|
|
15
|
+
content.description ??
|
|
16
|
+
"View your account details and manage profile settings.";
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="mx-auto max-w-3xl px-4 pb-16 pt-(--padding-content-top) lg:px-8">
|
|
20
|
+
<Head>
|
|
21
|
+
<title>{title}</title>
|
|
22
|
+
</Head>
|
|
23
|
+
|
|
24
|
+
<div className="mb-8 flex items-start gap-4">
|
|
25
|
+
<div className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
|
|
26
|
+
<UserIcon size={24} />
|
|
27
|
+
</div>
|
|
28
|
+
<div className="space-y-1">
|
|
29
|
+
<h1 className="text-3xl font-semibold tracking-tight">{title}</h1>
|
|
30
|
+
<p className="text-muted-foreground">{description}</p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{auth.isAuthenticated && auth.profile ? (
|
|
35
|
+
<dl className="divide-y rounded-xl border">
|
|
36
|
+
{auth.profile.name ? (
|
|
37
|
+
<div className="grid gap-1 px-4 py-4 sm:grid-cols-3">
|
|
38
|
+
<dt className="text-sm font-medium text-muted-foreground">
|
|
39
|
+
Name
|
|
40
|
+
</dt>
|
|
41
|
+
<dd className="text-sm sm:col-span-2">{auth.profile.name}</dd>
|
|
42
|
+
</div>
|
|
43
|
+
) : null}
|
|
44
|
+
{auth.profile.email ? (
|
|
45
|
+
<div className="grid gap-1 px-4 py-4 sm:grid-cols-3">
|
|
46
|
+
<dt className="text-sm font-medium text-muted-foreground">
|
|
47
|
+
Email
|
|
48
|
+
</dt>
|
|
49
|
+
<dd className="text-sm sm:col-span-2">{auth.profile.email}</dd>
|
|
50
|
+
</div>
|
|
51
|
+
) : null}
|
|
52
|
+
{typeof auth.profile.emailVerified === "boolean" ? (
|
|
53
|
+
<div className="grid gap-1 px-4 py-4 sm:grid-cols-3">
|
|
54
|
+
<dt className="text-sm font-medium text-muted-foreground">
|
|
55
|
+
Email verified
|
|
56
|
+
</dt>
|
|
57
|
+
<dd className="text-sm sm:col-span-2">
|
|
58
|
+
{auth.profile.emailVerified ? "Yes" : "No"}
|
|
59
|
+
</dd>
|
|
60
|
+
</div>
|
|
61
|
+
) : null}
|
|
62
|
+
</dl>
|
|
63
|
+
) : (
|
|
64
|
+
<div className="rounded-xl border bg-muted/30 p-6">
|
|
65
|
+
<p className="text-sm text-muted-foreground">
|
|
66
|
+
{auth.isAuthEnabled
|
|
67
|
+
? "Sign in to view your profile details."
|
|
68
|
+
: "Configure authentication in apitogo.config.tsx to enable sign-in and profile management."}
|
|
69
|
+
</p>
|
|
70
|
+
{auth.isAuthEnabled ? (
|
|
71
|
+
<Button asChild className="mt-4">
|
|
72
|
+
<Link to="/signin">Sign in</Link>
|
|
73
|
+
</Button>
|
|
74
|
+
) : null}
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from "react";
|
|
2
|
+
import { isValidElement } from "react";
|
|
3
|
+
|
|
4
|
+
export const renderModuleElement = (
|
|
5
|
+
component: ComponentType | ReactNode | undefined,
|
|
6
|
+
fallback: ReactNode,
|
|
7
|
+
) => {
|
|
8
|
+
if (!component) {
|
|
9
|
+
return fallback;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (isValidElement(component)) {
|
|
13
|
+
return component;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const Component = component as ComponentType;
|
|
17
|
+
return <Component />;
|
|
18
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ResolvedDashboardModule,
|
|
3
|
+
ResolvedPlansModule,
|
|
4
|
+
ResolvedUserManagementModule,
|
|
5
|
+
ResolvedUserPanelModule,
|
|
6
|
+
} from "@lukoweb/apitogo";
|
|
7
|
+
import type {
|
|
8
|
+
ProfileMenuPlugin,
|
|
9
|
+
ApitogoPlugin,
|
|
10
|
+
} from "@lukoweb/apitogo/plugins";
|
|
11
|
+
import { CreditCardIcon, LayoutDashboardIcon, UserIcon } from "lucide-react";
|
|
12
|
+
import type { ComponentType, ReactNode } from "react";
|
|
13
|
+
import type { RouteObject } from "react-router";
|
|
14
|
+
import { DefaultDashboardPage } from "./pages/DefaultDashboardPage.js";
|
|
15
|
+
import { DefaultPlansPage } from "./pages/DefaultPlansPage.js";
|
|
16
|
+
import { DefaultProfilePage } from "./pages/DefaultProfilePage.js";
|
|
17
|
+
import { renderModuleElement } from "./renderModuleElement.js";
|
|
18
|
+
|
|
19
|
+
type DashboardModuleOptions = ResolvedDashboardModule & {
|
|
20
|
+
component?: ComponentType | ReactNode;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type UserManagementModuleOptions = ResolvedUserManagementModule & {
|
|
24
|
+
component?: ComponentType | ReactNode;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type PlansModuleOptions = ResolvedPlansModule & {
|
|
28
|
+
component?: ComponentType | ReactNode;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type UserPanelModuleOptions = ResolvedUserPanelModule & {
|
|
32
|
+
dashboard: DashboardModuleOptions;
|
|
33
|
+
userManagement: UserManagementModuleOptions;
|
|
34
|
+
plans: PlansModuleOptions;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const protectAccountRoute = (
|
|
38
|
+
protectedRoutes: string[],
|
|
39
|
+
enabled: boolean,
|
|
40
|
+
path: string,
|
|
41
|
+
) => {
|
|
42
|
+
if (enabled) {
|
|
43
|
+
protectedRoutes.push(path);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const userPanelModule = (
|
|
48
|
+
options: UserPanelModuleOptions,
|
|
49
|
+
): (ApitogoPlugin & ProfileMenuPlugin) | undefined => {
|
|
50
|
+
if (!options.enabled) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const routes: RouteObject[] = [];
|
|
55
|
+
const protectedRoutes: string[] = [];
|
|
56
|
+
const profileMenuItems: ReturnType<ProfileMenuPlugin["getProfileMenuItems"]> =
|
|
57
|
+
[];
|
|
58
|
+
const protectAccountRoutes = options.userManagement.enabled;
|
|
59
|
+
|
|
60
|
+
if (options.dashboard.enabled) {
|
|
61
|
+
routes.push({
|
|
62
|
+
path: options.dashboard.path,
|
|
63
|
+
element: renderModuleElement(
|
|
64
|
+
options.dashboard.component,
|
|
65
|
+
<DefaultDashboardPage content={options.dashboard.content} />,
|
|
66
|
+
),
|
|
67
|
+
handle: { layout: "default" },
|
|
68
|
+
});
|
|
69
|
+
protectAccountRoute(
|
|
70
|
+
protectedRoutes,
|
|
71
|
+
protectAccountRoutes,
|
|
72
|
+
options.dashboard.path,
|
|
73
|
+
);
|
|
74
|
+
profileMenuItems.push({
|
|
75
|
+
label: "Dashboard",
|
|
76
|
+
path: options.dashboard.path,
|
|
77
|
+
category: "top",
|
|
78
|
+
icon: LayoutDashboardIcon,
|
|
79
|
+
weight: 10,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (options.userManagement.enabled) {
|
|
84
|
+
const profilePath = options.userManagement.routes.profile;
|
|
85
|
+
routes.push({
|
|
86
|
+
path: profilePath,
|
|
87
|
+
element: renderModuleElement(
|
|
88
|
+
options.userManagement.component,
|
|
89
|
+
<DefaultProfilePage content={options.userManagement.content} />,
|
|
90
|
+
),
|
|
91
|
+
handle: { layout: "default" },
|
|
92
|
+
});
|
|
93
|
+
protectAccountRoute(protectedRoutes, protectAccountRoutes, profilePath);
|
|
94
|
+
profileMenuItems.push({
|
|
95
|
+
label: "Profile",
|
|
96
|
+
path: profilePath,
|
|
97
|
+
category: "middle",
|
|
98
|
+
icon: UserIcon,
|
|
99
|
+
weight: 20,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (options.plans.enabled) {
|
|
104
|
+
routes.push({
|
|
105
|
+
path: options.plans.path,
|
|
106
|
+
element: renderModuleElement(
|
|
107
|
+
options.plans.component,
|
|
108
|
+
<DefaultPlansPage content={options.plans.content} />,
|
|
109
|
+
),
|
|
110
|
+
handle: { layout: "default" },
|
|
111
|
+
});
|
|
112
|
+
protectAccountRoute(
|
|
113
|
+
protectedRoutes,
|
|
114
|
+
protectAccountRoutes,
|
|
115
|
+
options.plans.path,
|
|
116
|
+
);
|
|
117
|
+
profileMenuItems.push({
|
|
118
|
+
label: "Plans",
|
|
119
|
+
path: options.plans.path,
|
|
120
|
+
category: "middle",
|
|
121
|
+
icon: CreditCardIcon,
|
|
122
|
+
weight: 30,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (options.apiKeys.enabled) {
|
|
127
|
+
protectAccountRoute(
|
|
128
|
+
protectedRoutes,
|
|
129
|
+
protectAccountRoutes,
|
|
130
|
+
options.apiKeys.path,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
getRoutes: () => routes,
|
|
136
|
+
getProtectedRoutes: () => protectedRoutes,
|
|
137
|
+
getProfileMenuItems: () => profileMenuItems,
|
|
138
|
+
};
|
|
139
|
+
};
|