@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
+ };