minutework 0.1.32 → 0.1.33
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/assets/claude-local/skills/README.md +2 -0
- package/assets/claude-local/skills/app-pack-authoring/SKILL.md +14 -1
- package/assets/claude-local/skills/contract-first-public-intake/SKILL.md +11 -3
- package/assets/claude-local/skills/generated-workspace-architecture/SKILL.md +10 -3
- package/assets/claude-local/skills/published-web-and-mw-core-site/SKILL.md +9 -6
- package/assets/claude-local/skills/standalone-mobile-client/SKILL.md +5 -4
- package/assets/templates/next-tenant-app/README.md +26 -138
- package/assets/templates/next-tenant-app/package.json +1 -0
- package/assets/templates/next-tenant-app/src/app/app/demo/page.tsx +15 -0
- package/assets/templates/next-tenant-app/src/app/app/layout.tsx +1 -4
- package/assets/templates/next-tenant-app/src/app/app/page.tsx +2 -17
- package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.tsx +9 -67
- package/assets/templates/next-tenant-app/src/app/blog/page.tsx +10 -46
- package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.tsx +9 -65
- package/assets/templates/next-tenant-app/src/app/docs/page.tsx +10 -46
- package/assets/templates/next-tenant-app/src/app/layout.tsx +8 -10
- package/assets/templates/next-tenant-app/src/app/login/page.tsx +3 -23
- package/assets/templates/next-tenant-app/src/app/page.tsx +11 -44
- package/assets/templates/next-tenant-app/src/app/pricing/page.tsx +10 -44
- package/assets/templates/next-tenant-app/src/app/providers.tsx +2 -1
- package/assets/templates/next-tenant-app/src/app/robots.ts +7 -18
- package/assets/templates/next-tenant-app/src/app/sitemap.ts +4 -39
- package/assets/templates/next-tenant-app/src/features/auth/components/login-screen.tsx +97 -98
- package/assets/templates/next-tenant-app/src/features/dashboard/components/tenant-dashboard.tsx +43 -78
- package/assets/templates/next-tenant-app/src/features/demo/components/manifest-demo.tsx +89 -0
- package/assets/templates/next-tenant-app/src/features/public-shell/components/static-public-page.tsx +58 -0
- package/assets/templates/next-tenant-app/src/features/shell/components/private-app-shell.tsx +48 -552
- package/assets/templates/next-tenant-app/src/lib/app-routes.ts +2 -2
- package/assets/templates/next-tenant-app/src/lib/public-site.ts +5 -30
- package/assets/templates/next-tenant-app/src/mw/client.ts +18 -0
- package/assets/templates/next-tenant-app/src/mw/mock.test.ts +21 -0
- package/assets/templates/next-tenant-app/src/mw/mock.ts +35 -0
- package/assets/templates/next-tenant-app/src/mw/provider.tsx +17 -0
- package/assets/templates/next-tenant-app/template.json +3 -3
- package/assets/templates/next-tenant-app/template.schema.json +1 -0
- package/assets/templates/next-tenant-app/tools/template/validate-route-contract.mjs +4 -5
- package/package.json +2 -2
- package/vendor/workspace-mcp/types.d.ts +4 -0
- package/assets/templates/next-tenant-app/src/app/(cms)/[...path]/page.tsx +0 -89
- package/assets/templates/next-tenant-app/src/app/api/auth/context/route.test.ts +0 -90
- package/assets/templates/next-tenant-app/src/app/api/auth/context/route.ts +0 -78
- package/assets/templates/next-tenant-app/src/app/api/auth/login/route.ts +0 -31
- package/assets/templates/next-tenant-app/src/app/api/auth/logout/route.ts +0 -16
- package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.test.ts +0 -79
- package/assets/templates/next-tenant-app/src/app/api/auth/password-change/route.ts +0 -40
- package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.test.ts +0 -42
- package/assets/templates/next-tenant-app/src/app/api/auth/password-status/route.ts +0 -29
- package/assets/templates/next-tenant-app/src/app/api/auth/session/route.ts +0 -26
- package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.test.ts +0 -40
- package/assets/templates/next-tenant-app/src/app/api/gateway/commands/[runId]/route.ts +0 -47
- package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.test.ts +0 -43
- package/assets/templates/next-tenant-app/src/app/api/gateway/commands/route.ts +0 -45
- package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.test.ts +0 -83
- package/assets/templates/next-tenant-app/src/app/app/examples/runtime-commands/page.tsx +0 -30
- package/assets/templates/next-tenant-app/src/app/app/page.test.ts +0 -62
- package/assets/templates/next-tenant-app/src/app/app/private-content-source.test.ts +0 -88
- package/assets/templates/next-tenant-app/src/app/blog/[slug]/page.test.ts +0 -70
- package/assets/templates/next-tenant-app/src/app/blog/page.test.ts +0 -46
- package/assets/templates/next-tenant-app/src/app/docs/[...slug]/page.test.ts +0 -70
- package/assets/templates/next-tenant-app/src/app/docs/page.test.ts +0 -46
- package/assets/templates/next-tenant-app/src/app/login/page.test.ts +0 -55
- package/assets/templates/next-tenant-app/src/app/page.test.ts +0 -90
- package/assets/templates/next-tenant-app/src/app/pricing/page.test.ts +0 -59
- package/assets/templates/next-tenant-app/src/app/robots.test.ts +0 -40
- package/assets/templates/next-tenant-app/src/app/sitemap.test.ts +0 -63
- package/assets/templates/next-tenant-app/src/features/examples/runtime-command-demo/components/runtime-command-demo.tsx +0 -342
- package/assets/templates/next-tenant-app/src/features/public-shell/components/content-article.tsx +0 -66
- package/assets/templates/next-tenant-app/src/features/public-shell/components/content-collection.tsx +0 -108
- package/assets/templates/next-tenant-app/src/features/public-shell/components/marketing-page-canvas.tsx +0 -111
- package/assets/templates/next-tenant-app/src/features/public-shell/components/public-site-shell.tsx +0 -111
- package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.test.ts +0 -38
- package/assets/templates/next-tenant-app/src/features/public-shell/components/section-renderer.tsx +0 -145
- package/assets/templates/next-tenant-app/src/lib/content/__fixtures__/public-site-snapshot.ts +0 -189
- package/assets/templates/next-tenant-app/src/lib/content/adapter.server.test.ts +0 -444
- package/assets/templates/next-tenant-app/src/lib/content/adapter.server.ts +0 -383
- package/assets/templates/next-tenant-app/src/lib/content/contracts.test.ts +0 -138
- package/assets/templates/next-tenant-app/src/lib/content/contracts.ts +0 -399
- package/assets/templates/next-tenant-app/src/lib/content/custom-adapter.ts +0 -5
- package/assets/templates/next-tenant-app/src/lib/content/empty-state.ts +0 -96
- package/assets/templates/next-tenant-app/src/lib/content/release-manifest.test.ts +0 -93
- package/assets/templates/next-tenant-app/src/lib/content/release-manifest.ts +0 -123
- package/assets/templates/next-tenant-app/src/lib/platform/auth.server.test.ts +0 -75
- package/assets/templates/next-tenant-app/src/lib/platform/auth.server.ts +0 -25
- package/assets/templates/next-tenant-app/src/lib/platform/client.server.test.ts +0 -170
- package/assets/templates/next-tenant-app/src/lib/platform/client.server.ts +0 -661
- package/assets/templates/next-tenant-app/src/lib/platform/contracts.ts +0 -131
- package/assets/templates/next-tenant-app/src/lib/platform/endpoints.server.ts +0 -34
- package/assets/templates/next-tenant-app/src/lib/platform/env.server.test.ts +0 -211
- package/assets/templates/next-tenant-app/src/lib/platform/env.server.ts +0 -151
- package/assets/templates/next-tenant-app/src/lib/platform/route-response.ts +0 -33
- package/assets/templates/next-tenant-app/src/lib/platform/session.server.ts +0 -108
|
@@ -1,51 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { StaticPublicPage } from "@/features/public-shell/components/static-public-page";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { appRoutes } from "@/lib/app-routes";
|
|
7
|
-
import {
|
|
8
|
-
buildDisabledPublicMetadata,
|
|
9
|
-
buildPublicMetadata,
|
|
10
|
-
isPublicContentDisabled,
|
|
11
|
-
} from "@/lib/public-site";
|
|
12
|
-
|
|
13
|
-
export async function generateMetadata() {
|
|
14
|
-
if (isPublicContentDisabled()) {
|
|
15
|
-
return buildDisabledPublicMetadata();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const siteConfig = await getSiteConfig();
|
|
19
|
-
const collection = siteConfig.collections.docs;
|
|
20
|
-
|
|
21
|
-
return buildPublicMetadata({
|
|
22
|
-
title: collection.title,
|
|
23
|
-
description: collection.description,
|
|
24
|
-
path: appRoutes.docsIndex,
|
|
25
|
-
siteName: siteConfig.siteName,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export default async function DocsIndexPage() {
|
|
30
|
-
if (isPublicContentDisabled()) {
|
|
31
|
-
notFound();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const [siteConfig, entries] = await Promise.all([
|
|
35
|
-
getSiteConfig(),
|
|
36
|
-
listEntries("docs"),
|
|
37
|
-
]);
|
|
38
|
-
const collection = siteConfig.collections.docs;
|
|
3
|
+
export const metadata = {
|
|
4
|
+
title: "Docs",
|
|
5
|
+
};
|
|
39
6
|
|
|
7
|
+
export default function DocsIndexPage() {
|
|
40
8
|
return (
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
kind="docs"
|
|
47
|
-
title={collection.title}
|
|
48
|
-
/>
|
|
49
|
-
</PublicSiteShell>
|
|
9
|
+
<StaticPublicPage
|
|
10
|
+
eyebrow="Docs"
|
|
11
|
+
title="Customer documentation"
|
|
12
|
+
body="Guides, onboarding notes, and product documentation."
|
|
13
|
+
/>
|
|
50
14
|
);
|
|
51
15
|
}
|
|
@@ -5,7 +5,6 @@ import { cookies } from "next/headers";
|
|
|
5
5
|
import type { ReactNode } from "react";
|
|
6
6
|
|
|
7
7
|
import { AppProviders } from "./providers";
|
|
8
|
-
import { env } from "@/lib/platform/env.server";
|
|
9
8
|
import { resolvePublicMetadataBase } from "@/lib/public-site";
|
|
10
9
|
import {
|
|
11
10
|
isThemeMode,
|
|
@@ -25,22 +24,21 @@ const geistMono = Geist_Mono({
|
|
|
25
24
|
|
|
26
25
|
export function generateMetadata(): Metadata {
|
|
27
26
|
const metadataBase = resolvePublicMetadataBase();
|
|
27
|
+
const appName = process.env.MW_TEMPLATE_APP_NAME || "Tenant App";
|
|
28
28
|
|
|
29
29
|
return {
|
|
30
30
|
...(metadataBase ? { metadataBase } : {}),
|
|
31
31
|
title: {
|
|
32
|
-
default:
|
|
33
|
-
template: `%s | ${
|
|
32
|
+
default: appName,
|
|
33
|
+
template: `%s | ${appName}`,
|
|
34
34
|
},
|
|
35
|
-
description:
|
|
36
|
-
"SEO-first public and authenticated Next.js starter with a server-owned platform session BFF.",
|
|
35
|
+
description: "Customer-facing tenant app.",
|
|
37
36
|
openGraph: {
|
|
38
|
-
title:
|
|
39
|
-
description:
|
|
40
|
-
|
|
41
|
-
siteName: env.MW_TEMPLATE_APP_NAME,
|
|
37
|
+
title: appName,
|
|
38
|
+
description: "Customer-facing tenant app.",
|
|
39
|
+
siteName: appName,
|
|
42
40
|
type: "website",
|
|
43
|
-
...(env.MW_PUBLIC_BASE_URL ? { url: env.MW_PUBLIC_BASE_URL } : {}),
|
|
41
|
+
...(process.env.MW_PUBLIC_BASE_URL ? { url: process.env.MW_PUBLIC_BASE_URL } : {}),
|
|
44
42
|
},
|
|
45
43
|
};
|
|
46
44
|
}
|
|
@@ -1,33 +1,13 @@
|
|
|
1
|
-
import { redirect } from "next/navigation";
|
|
2
|
-
|
|
3
1
|
import { LoginScreen } from "@/features/auth/components/login-screen";
|
|
4
|
-
import { appRoutes } from "@/lib/app-routes";
|
|
5
|
-
import { resolveCurrentSession } from "@/lib/platform/client.server";
|
|
6
|
-
import { platformAuthEndpoints } from "@/lib/platform/endpoints.server";
|
|
7
|
-
import { env } from "@/lib/platform/env.server";
|
|
8
|
-
import { readPlatformAuthState } from "@/lib/platform/session.server";
|
|
9
2
|
|
|
10
3
|
export const metadata = {
|
|
11
|
-
title: "
|
|
12
|
-
description:
|
|
13
|
-
"Authenticate into the combined MinuteWork starter to continue into the private workspace surface under /app.",
|
|
4
|
+
title: "Log In",
|
|
14
5
|
robots: {
|
|
15
6
|
index: false,
|
|
16
7
|
follow: false,
|
|
17
8
|
},
|
|
18
9
|
};
|
|
19
10
|
|
|
20
|
-
export default
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (session.authenticated) {
|
|
24
|
-
redirect(appRoutes.appHome);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return (
|
|
28
|
-
<LoginScreen
|
|
29
|
-
appName={env.MW_TEMPLATE_APP_NAME}
|
|
30
|
-
operatorConsoleHref={platformAuthEndpoints.operatorConsole}
|
|
31
|
-
/>
|
|
32
|
-
);
|
|
11
|
+
export default function LoginPage() {
|
|
12
|
+
return <LoginScreen appName={process.env.MW_TEMPLATE_APP_NAME || "Tenant App"} />;
|
|
33
13
|
}
|
|
@@ -1,49 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { StaticPublicPage } from "@/features/public-shell/components/static-public-page";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import { appRoutes } from "@/lib/app-routes";
|
|
8
|
-
import {
|
|
9
|
-
buildDisabledPublicMetadata,
|
|
10
|
-
buildPublicMetadata,
|
|
11
|
-
isPublicContentDisabled,
|
|
12
|
-
} from "@/lib/public-site";
|
|
13
|
-
|
|
14
|
-
export async function generateMetadata() {
|
|
15
|
-
if (isPublicContentDisabled()) {
|
|
16
|
-
return buildDisabledPublicMetadata();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const [siteConfig, page] = await Promise.all([
|
|
20
|
-
getSiteConfig(),
|
|
21
|
-
getMarketingPage("home"),
|
|
22
|
-
]);
|
|
23
|
-
const resolvedPage = page ?? buildEmptyMarketingPage("home", siteConfig);
|
|
24
|
-
|
|
25
|
-
return buildPublicMetadata({
|
|
26
|
-
title: resolvedPage.seo.title,
|
|
27
|
-
description: resolvedPage.seo.description,
|
|
28
|
-
path: resolvedPage.path,
|
|
29
|
-
siteName: siteConfig.siteName,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export default async function HomePage() {
|
|
34
|
-
if (isPublicContentDisabled()) {
|
|
35
|
-
redirect(appRoutes.appHome);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const [siteConfig, page] = await Promise.all([
|
|
39
|
-
getSiteConfig(),
|
|
40
|
-
getMarketingPage("home"),
|
|
41
|
-
]);
|
|
42
|
-
const resolvedPage = page ?? buildEmptyMarketingPage("home", siteConfig);
|
|
3
|
+
export const metadata = {
|
|
4
|
+
title: "Tenant App",
|
|
5
|
+
description: "Customer-facing tenant app.",
|
|
6
|
+
};
|
|
43
7
|
|
|
8
|
+
export default function HomePage() {
|
|
44
9
|
return (
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
10
|
+
<StaticPublicPage
|
|
11
|
+
eyebrow="Tenant App"
|
|
12
|
+
title="A customer app connected to MinuteWork"
|
|
13
|
+
body="Public pages stay generic, while customer auth and manifest APIs come from the MinuteWork web SDK."
|
|
14
|
+
/>
|
|
48
15
|
);
|
|
49
16
|
}
|
|
@@ -1,49 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { StaticPublicPage } from "@/features/public-shell/components/static-public-page";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { buildEmptyMarketingPage } from "@/lib/content/empty-state";
|
|
7
|
-
import { appRoutes } from "@/lib/app-routes";
|
|
8
|
-
import {
|
|
9
|
-
buildDisabledPublicMetadata,
|
|
10
|
-
buildPublicMetadata,
|
|
11
|
-
isPublicContentDisabled,
|
|
12
|
-
} from "@/lib/public-site";
|
|
13
|
-
|
|
14
|
-
export async function generateMetadata() {
|
|
15
|
-
if (isPublicContentDisabled()) {
|
|
16
|
-
return buildDisabledPublicMetadata();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const [siteConfig, page] = await Promise.all([
|
|
20
|
-
getSiteConfig(),
|
|
21
|
-
getMarketingPage("pricing"),
|
|
22
|
-
]);
|
|
23
|
-
const resolvedPage = page ?? buildEmptyMarketingPage("pricing", siteConfig);
|
|
24
|
-
|
|
25
|
-
return buildPublicMetadata({
|
|
26
|
-
title: resolvedPage.seo.title,
|
|
27
|
-
description: resolvedPage.seo.description,
|
|
28
|
-
path: resolvedPage.path,
|
|
29
|
-
siteName: siteConfig.siteName,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export default async function PricingPage() {
|
|
34
|
-
if (isPublicContentDisabled()) {
|
|
35
|
-
notFound();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const [siteConfig, page] = await Promise.all([
|
|
39
|
-
getSiteConfig(),
|
|
40
|
-
getMarketingPage("pricing"),
|
|
41
|
-
]);
|
|
42
|
-
const resolvedPage = page ?? buildEmptyMarketingPage("pricing", siteConfig);
|
|
3
|
+
export const metadata = {
|
|
4
|
+
title: "Pricing",
|
|
5
|
+
};
|
|
43
6
|
|
|
7
|
+
export default function PricingPage() {
|
|
44
8
|
return (
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
9
|
+
<StaticPublicPage
|
|
10
|
+
eyebrow="Pricing"
|
|
11
|
+
title="Plans for customer access"
|
|
12
|
+
body="Clear plan options for customer workspaces."
|
|
13
|
+
/>
|
|
48
14
|
);
|
|
49
15
|
}
|
|
@@ -4,6 +4,7 @@ import type { ReactNode } from "react";
|
|
|
4
4
|
|
|
5
5
|
import { ThemeProvider } from "@/lib/theme";
|
|
6
6
|
import type { ThemeMode } from "@/lib/theme-config";
|
|
7
|
+
import { MinuteWorkSdkProvider } from "@/mw/provider";
|
|
7
8
|
|
|
8
9
|
export function AppProviders({
|
|
9
10
|
children,
|
|
@@ -19,7 +20,7 @@ export function AppProviders({
|
|
|
19
20
|
enableSystem
|
|
20
21
|
disableTransitionOnChange
|
|
21
22
|
>
|
|
22
|
-
{children}
|
|
23
|
+
<MinuteWorkSdkProvider>{children}</MinuteWorkSdkProvider>
|
|
23
24
|
</ThemeProvider>
|
|
24
25
|
);
|
|
25
26
|
}
|
|
@@ -1,24 +1,9 @@
|
|
|
1
1
|
import type { MetadataRoute } from "next";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
isPublicContentDisabled,
|
|
5
|
-
resolvePublicMetadataBase,
|
|
6
|
-
} from "@/lib/public-site";
|
|
3
|
+
import { resolvePublicMetadataBase } from "@/lib/public-site";
|
|
7
4
|
|
|
8
5
|
export default function robots(): MetadataRoute.Robots {
|
|
9
6
|
const metadataBase = resolvePublicMetadataBase();
|
|
10
|
-
|
|
11
|
-
if (!metadataBase || isPublicContentDisabled()) {
|
|
12
|
-
return {
|
|
13
|
-
rules: [
|
|
14
|
-
{
|
|
15
|
-
userAgent: "*",
|
|
16
|
-
disallow: "/",
|
|
17
|
-
},
|
|
18
|
-
],
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
7
|
return {
|
|
23
8
|
rules: [
|
|
24
9
|
{
|
|
@@ -26,7 +11,11 @@ export default function robots(): MetadataRoute.Robots {
|
|
|
26
11
|
allow: "/",
|
|
27
12
|
},
|
|
28
13
|
],
|
|
29
|
-
|
|
30
|
-
|
|
14
|
+
...(metadataBase
|
|
15
|
+
? {
|
|
16
|
+
sitemap: new URL("/sitemap.xml", metadataBase).toString(),
|
|
17
|
+
host: metadataBase.host,
|
|
18
|
+
}
|
|
19
|
+
: {}),
|
|
31
20
|
};
|
|
32
21
|
}
|
|
@@ -1,30 +1,14 @@
|
|
|
1
1
|
import type { MetadataRoute } from "next";
|
|
2
2
|
|
|
3
|
-
import { listEntries } from "@/lib/content/adapter.server";
|
|
4
3
|
import { appRoutes } from "@/lib/app-routes";
|
|
5
|
-
import {
|
|
4
|
+
import { resolvePublicSiteUrl } from "@/lib/public-site";
|
|
6
5
|
|
|
7
6
|
function buildSitemapUrl(pathname: string) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (!url) {
|
|
11
|
-
throw new Error("MW_PUBLIC_BASE_URL is required to build a public sitemap.");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return url.toString();
|
|
7
|
+
return resolvePublicSiteUrl(pathname)?.toString() ?? pathname;
|
|
15
8
|
}
|
|
16
9
|
|
|
17
|
-
export default
|
|
18
|
-
|
|
19
|
-
return [];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const [docsEntries, blogEntries] = await Promise.all([
|
|
23
|
-
listEntries("docs"),
|
|
24
|
-
listEntries("blog"),
|
|
25
|
-
]);
|
|
26
|
-
|
|
27
|
-
const entries: MetadataRoute.Sitemap = [
|
|
10
|
+
export default function sitemap(): MetadataRoute.Sitemap {
|
|
11
|
+
return [
|
|
28
12
|
{
|
|
29
13
|
url: buildSitemapUrl(appRoutes.publicHome),
|
|
30
14
|
changeFrequency: "weekly",
|
|
@@ -46,23 +30,4 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
|
46
30
|
priority: 0.8,
|
|
47
31
|
},
|
|
48
32
|
];
|
|
49
|
-
|
|
50
|
-
entries.push(
|
|
51
|
-
...docsEntries.map((entry) => ({
|
|
52
|
-
url: buildSitemapUrl(appRoutes.docsPage(entry.slug)),
|
|
53
|
-
changeFrequency: "weekly" as const,
|
|
54
|
-
priority: 0.7,
|
|
55
|
-
})),
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
entries.push(
|
|
59
|
-
...blogEntries.map((entry) => ({
|
|
60
|
-
url: buildSitemapUrl(appRoutes.blogPost(entry.slug)),
|
|
61
|
-
changeFrequency: "monthly" as const,
|
|
62
|
-
priority: 0.7,
|
|
63
|
-
...(entry.publishedAt ? { lastModified: new Date(entry.publishedAt) } : {}),
|
|
64
|
-
})),
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
return entries;
|
|
68
33
|
}
|
|
@@ -1,108 +1,81 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { startTransition, useEffect, useState } from "react";
|
|
4
3
|
import type { FormEvent } from "react";
|
|
4
|
+
import { startTransition, useState } from "react";
|
|
5
|
+
import { ArrowRight, MailCheck } from "lucide-react";
|
|
6
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
5
7
|
|
|
6
|
-
import { ArrowRight, ShieldCheck, TerminalSquare } from "lucide-react";
|
|
7
|
-
import { useRouter } from "next/navigation";
|
|
8
|
-
|
|
9
|
-
import { Button } from "@/design-system/primitives/button";
|
|
10
|
-
import { Input } from "@/design-system/primitives/input";
|
|
11
8
|
import { PanelFrame } from "@/design-system/patterns/panel-frame";
|
|
12
9
|
import { ThemeModeToggle } from "@/design-system/patterns/theme-mode-toggle";
|
|
10
|
+
import { Button } from "@/design-system/primitives/button";
|
|
11
|
+
import { Input } from "@/design-system/primitives/input";
|
|
13
12
|
import { appRoutes } from "@/lib/app-routes";
|
|
13
|
+
import { useMinuteWorkAuth } from "@minutework/web-auth/react";
|
|
14
|
+
|
|
15
|
+
type AuthMode = "login" | "signup";
|
|
14
16
|
|
|
15
|
-
export function LoginScreen({
|
|
16
|
-
appName,
|
|
17
|
-
operatorConsoleHref,
|
|
18
|
-
}: {
|
|
19
|
-
appName: string;
|
|
20
|
-
operatorConsoleHref: string;
|
|
21
|
-
}) {
|
|
17
|
+
export function LoginScreen({ appName }: { appName: string }) {
|
|
22
18
|
const router = useRouter();
|
|
23
|
-
const
|
|
24
|
-
const
|
|
19
|
+
const searchParams = useSearchParams();
|
|
20
|
+
const { login, signup } = useMinuteWorkAuth();
|
|
21
|
+
const [mode, setMode] = useState<AuthMode>("login");
|
|
22
|
+
const [email, setEmail] = useState("");
|
|
23
|
+
const [displayName, setDisplayName] = useState("");
|
|
24
|
+
const [password, setPassword] = useState("");
|
|
25
|
+
const [notice, setNotice] = useState("");
|
|
25
26
|
const [error, setError] = useState("");
|
|
26
27
|
const [submitting, setSubmitting] = useState(false);
|
|
27
28
|
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
void fetch("/api/auth/session", {
|
|
30
|
-
method: "GET",
|
|
31
|
-
cache: "no-store",
|
|
32
|
-
}).catch(() => null);
|
|
33
|
-
}, []);
|
|
34
|
-
|
|
35
29
|
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
|
36
30
|
event.preventDefault();
|
|
37
31
|
setSubmitting(true);
|
|
38
32
|
setError("");
|
|
33
|
+
setNotice("");
|
|
39
34
|
|
|
40
35
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
detail?: string;
|
|
50
|
-
};
|
|
51
|
-
setError(payload.detail || "Login failed.");
|
|
36
|
+
if (mode === "signup") {
|
|
37
|
+
await signup({
|
|
38
|
+
email,
|
|
39
|
+
password,
|
|
40
|
+
displayName,
|
|
41
|
+
claimRef: searchParams.get("claim_ref") || undefined,
|
|
42
|
+
});
|
|
43
|
+
setNotice("Check your email to finish verification.");
|
|
52
44
|
setSubmitting(false);
|
|
53
45
|
return;
|
|
54
46
|
}
|
|
55
47
|
|
|
48
|
+
await login({ email, password });
|
|
56
49
|
startTransition(() => {
|
|
57
50
|
router.replace(appRoutes.appHome);
|
|
58
51
|
router.refresh();
|
|
59
52
|
});
|
|
60
|
-
} catch {
|
|
61
|
-
|
|
53
|
+
} catch (caught) {
|
|
54
|
+
const message =
|
|
55
|
+
caught instanceof Error ? caught.message : "Unable to complete auth.";
|
|
56
|
+
setError(message);
|
|
62
57
|
setSubmitting(false);
|
|
63
58
|
}
|
|
64
59
|
}
|
|
65
60
|
|
|
66
61
|
return (
|
|
67
62
|
<main className="min-h-screen bg-background text-foreground">
|
|
68
|
-
<div className="mx-auto flex min-h-screen max-w-6xl flex-col gap-
|
|
69
|
-
<
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
<section className="max-w-2xl space-y-6">
|
|
63
|
+
<div className="mx-auto flex min-h-screen max-w-6xl flex-col gap-8 px-6 py-8 lg:flex-row lg:items-center lg:justify-between">
|
|
64
|
+
<section className="max-w-2xl space-y-5">
|
|
65
|
+
<div className="flex justify-end lg:hidden">
|
|
66
|
+
<ThemeModeToggle className="max-w-xs" />
|
|
67
|
+
</div>
|
|
74
68
|
<div className="space-y-3">
|
|
75
69
|
<p className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
|
|
76
|
-
|
|
70
|
+
{appName}
|
|
77
71
|
</p>
|
|
78
72
|
<h1 className="max-w-xl text-4xl font-semibold tracking-tight text-balance sm:text-5xl">
|
|
79
|
-
|
|
73
|
+
Customer workspace
|
|
80
74
|
</h1>
|
|
81
75
|
<p className="max-w-xl text-base leading-7 text-muted-foreground sm:text-lg">
|
|
82
|
-
|
|
83
|
-
root while preserving a server-owned platform session BFF under
|
|
84
|
-
`/app`.
|
|
76
|
+
Use your customer account to continue.
|
|
85
77
|
</p>
|
|
86
78
|
</div>
|
|
87
|
-
|
|
88
|
-
<div className="grid gap-3 sm:grid-cols-2">
|
|
89
|
-
<PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-2">
|
|
90
|
-
<TerminalSquare className="size-5 text-primary" />
|
|
91
|
-
<p className="text-sm font-medium">Generic tenant shell</p>
|
|
92
|
-
<p className="text-sm leading-6 text-muted-foreground">
|
|
93
|
-
Start with public storytelling at the root and authenticated
|
|
94
|
-
workspace routing under `/app` instead of a private-only home page.
|
|
95
|
-
</p>
|
|
96
|
-
</PanelFrame>
|
|
97
|
-
<PanelFrame tone="raised" radius="xl" padding="lg" className="space-y-2">
|
|
98
|
-
<ShieldCheck className="size-5 text-primary" />
|
|
99
|
-
<p className="text-sm font-medium">Server-owned session</p>
|
|
100
|
-
<p className="text-sm leading-6 text-muted-foreground">
|
|
101
|
-
Session cookies stay on the server boundary while public pages
|
|
102
|
-
remain free of tenant-context reads and private BFF assumptions.
|
|
103
|
-
</p>
|
|
104
|
-
</PanelFrame>
|
|
105
|
-
</div>
|
|
106
79
|
</section>
|
|
107
80
|
|
|
108
81
|
<div className="flex w-full max-w-md flex-col gap-4 self-center lg:self-auto">
|
|
@@ -111,37 +84,69 @@ export function LoginScreen({
|
|
|
111
84
|
</div>
|
|
112
85
|
|
|
113
86
|
<PanelFrame tone="floating" radius="xl" padding="lg" className="space-y-6 shadow-2xl">
|
|
114
|
-
<div className="
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
87
|
+
<div className="grid grid-cols-2 gap-2 rounded-lg border border-border p-1">
|
|
88
|
+
<Button
|
|
89
|
+
type="button"
|
|
90
|
+
variant={mode === "login" ? "default" : "ghost"}
|
|
91
|
+
onClick={() => {
|
|
92
|
+
setMode("login");
|
|
93
|
+
setError("");
|
|
94
|
+
setNotice("");
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
Log in
|
|
98
|
+
</Button>
|
|
99
|
+
<Button
|
|
100
|
+
type="button"
|
|
101
|
+
variant={mode === "signup" ? "default" : "ghost"}
|
|
102
|
+
onClick={() => {
|
|
103
|
+
setMode("signup");
|
|
104
|
+
setError("");
|
|
105
|
+
setNotice("");
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
Sign up
|
|
109
|
+
</Button>
|
|
125
110
|
</div>
|
|
126
111
|
|
|
127
112
|
<form className="space-y-5" onSubmit={handleSubmit}>
|
|
113
|
+
{mode === "signup" ? (
|
|
114
|
+
<div className="space-y-2">
|
|
115
|
+
<label
|
|
116
|
+
className="text-xs font-semibold uppercase tracking-widest text-muted-foreground"
|
|
117
|
+
htmlFor="display-name"
|
|
118
|
+
>
|
|
119
|
+
Name
|
|
120
|
+
</label>
|
|
121
|
+
<Input
|
|
122
|
+
id="display-name"
|
|
123
|
+
autoComplete="name"
|
|
124
|
+
className="h-12 text-base"
|
|
125
|
+
value={displayName}
|
|
126
|
+
onChange={(event) => setDisplayName(event.target.value)}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
) : null}
|
|
130
|
+
|
|
128
131
|
<div className="space-y-2">
|
|
129
132
|
<label
|
|
130
133
|
className="text-xs font-semibold uppercase tracking-widest text-muted-foreground"
|
|
131
|
-
htmlFor="
|
|
134
|
+
htmlFor="email"
|
|
132
135
|
>
|
|
133
|
-
|
|
136
|
+
Email
|
|
134
137
|
</label>
|
|
135
138
|
<Input
|
|
136
|
-
id="
|
|
137
|
-
name="
|
|
138
|
-
|
|
139
|
+
id="email"
|
|
140
|
+
name="email"
|
|
141
|
+
type="email"
|
|
142
|
+
autoComplete="email"
|
|
139
143
|
className="h-12 text-base"
|
|
140
|
-
value={
|
|
144
|
+
value={email}
|
|
141
145
|
onChange={(event) => {
|
|
142
|
-
|
|
146
|
+
setEmail(event.target.value);
|
|
143
147
|
setError("");
|
|
144
148
|
}}
|
|
149
|
+
required
|
|
145
150
|
/>
|
|
146
151
|
</div>
|
|
147
152
|
|
|
@@ -156,40 +161,34 @@ export function LoginScreen({
|
|
|
156
161
|
id="password"
|
|
157
162
|
name="password"
|
|
158
163
|
type="password"
|
|
159
|
-
autoComplete="current-password"
|
|
164
|
+
autoComplete={mode === "signup" ? "new-password" : "current-password"}
|
|
160
165
|
className="h-12 text-base"
|
|
161
166
|
value={password}
|
|
162
167
|
onChange={(event) => {
|
|
163
168
|
setPassword(event.target.value);
|
|
164
169
|
setError("");
|
|
165
170
|
}}
|
|
171
|
+
required
|
|
166
172
|
/>
|
|
167
173
|
</div>
|
|
168
174
|
|
|
169
|
-
{
|
|
170
|
-
<
|
|
175
|
+
{notice ? (
|
|
176
|
+
<div className="flex items-start gap-2 rounded-md border border-border bg-muted/40 p-3 text-sm text-foreground">
|
|
177
|
+
<MailCheck className="mt-0.5 size-4 text-primary" />
|
|
178
|
+
<p>{notice}</p>
|
|
179
|
+
</div>
|
|
171
180
|
) : null}
|
|
181
|
+
{error ? <p className="text-sm leading-6 text-destructive">{error}</p> : null}
|
|
172
182
|
|
|
173
183
|
<Button
|
|
174
184
|
type="submit"
|
|
175
185
|
className="h-12 w-full text-sm font-semibold"
|
|
176
186
|
disabled={submitting}
|
|
177
187
|
>
|
|
178
|
-
{submitting ? "
|
|
188
|
+
{submitting ? "Working" : mode === "signup" ? "Create account" : "Log in"}
|
|
179
189
|
<ArrowRight className="size-4" />
|
|
180
190
|
</Button>
|
|
181
191
|
</form>
|
|
182
|
-
|
|
183
|
-
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-border pt-4">
|
|
184
|
-
<p className="text-sm text-muted-foreground">
|
|
185
|
-
Need the operator console instead?
|
|
186
|
-
</p>
|
|
187
|
-
<Button asChild type="button" variant="outline">
|
|
188
|
-
<a href={operatorConsoleHref} target="_blank" rel="noreferrer">
|
|
189
|
-
Open /ops
|
|
190
|
-
</a>
|
|
191
|
-
</Button>
|
|
192
|
-
</div>
|
|
193
192
|
</PanelFrame>
|
|
194
193
|
</div>
|
|
195
194
|
</div>
|