camelmind 0.1.0
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/index.js +142 -0
- package/package.json +51 -0
- package/template/_env.example +16 -0
- package/template/_gitignore +32 -0
- package/template/_package.json +39 -0
- package/template/app/[...slug]/page.tsx +150 -0
- package/template/app/api/auth/callback/route.ts +47 -0
- package/template/app/api/auth/login/route.ts +39 -0
- package/template/app/api/auth/logout/route.ts +8 -0
- package/template/app/api/auth/signin/route.ts +64 -0
- package/template/app/api/download/route.ts +67 -0
- package/template/app/api/raw/route.ts +43 -0
- package/template/app/api/search/route.ts +138 -0
- package/template/app/favicon.ico +0 -0
- package/template/app/globals.css +524 -0
- package/template/app/home/page.tsx +136 -0
- package/template/app/icon.svg +29 -0
- package/template/app/layout.tsx +44 -0
- package/template/app/login/LoginForm.tsx +105 -0
- package/template/app/login/page.tsx +9 -0
- package/template/app/page.tsx +5 -0
- package/template/camelmind.config.ts +33 -0
- package/template/components/Breadcrumbs/Breadcrumbs.tsx +41 -0
- package/template/components/Code/CodeBlock.tsx +55 -0
- package/template/components/DocActions/DocActions.tsx +62 -0
- package/template/components/Nav/MobileDrawer.tsx +221 -0
- package/template/components/Nav/ThemeToggle.tsx +62 -0
- package/template/components/Nav/TopNav.tsx +173 -0
- package/template/components/Nav/VersionSelector.tsx +107 -0
- package/template/components/PageNav/PageNav.tsx +68 -0
- package/template/components/Search/SearchModal.tsx +278 -0
- package/template/components/SectionCards/SectionCards.tsx +33 -0
- package/template/components/Sidebar/Sidebar.tsx +196 -0
- package/template/components/Toc/Toc.tsx +57 -0
- package/template/components/ZoomImages/ZoomImages.tsx +24 -0
- package/template/components/mdx/Callout.tsx +43 -0
- package/template/components/mdx/Details.tsx +65 -0
- package/template/components/mdx/Icon.tsx +25 -0
- package/template/components/mdx/Steps.tsx +33 -0
- package/template/components/mdx/Tabs.tsx +69 -0
- package/template/components/mdx/index.tsx +34 -0
- package/template/content/getting-started/installation.mdx +51 -0
- package/template/content/getting-started/overview.mdx +39 -0
- package/template/content/guides/writing-docs.mdx +72 -0
- package/template/eslint.config.mjs +18 -0
- package/template/lib/auth-providers/dev-mock.ts +45 -0
- package/template/lib/auth-providers/oidc.ts +95 -0
- package/template/lib/auth-roles.ts +28 -0
- package/template/lib/auth.ts +76 -0
- package/template/lib/config-types.ts +30 -0
- package/template/lib/config.ts +29 -0
- package/template/lib/mdx.ts +53 -0
- package/template/lib/nav-types.ts +31 -0
- package/template/lib/nav.ts +125 -0
- package/template/lib/versions.ts +61 -0
- package/template/nav/nav.yml +20 -0
- package/template/next.config.ts +35 -0
- package/template/postcss.config.mjs +7 -0
- package/template/proxy.ts +50 -0
- package/template/public/2f-logo.png +0 -0
- package/template/public/favicon.png +0 -0
- package/template/public/file.svg +1 -0
- package/template/public/globe.svg +1 -0
- package/template/public/images/gwa-app-central-main.png +0 -0
- package/template/public/next.svg +1 -0
- package/template/public/window.svg +1 -0
- package/template/scripts/build-offline.sh +189 -0
- package/template/scripts/build-pdf.sh +41 -0
- package/template/scripts/build-search-index.ts +90 -0
- package/template/scripts/generate-master-pdf.ts +260 -0
- package/template/scripts/generate-pdfs.ts +185 -0
- package/template/tsconfig.json +34 -0
- package/template/versions.yml +5 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import siteConfig from "@/camelmind.config"
|
|
2
|
+
import type { AuthConfig, CamelMindConfig } from "./config-types"
|
|
3
|
+
|
|
4
|
+
export type { AuthConfig, AuthProvider, CamelMindConfig, OidcConfig } from "./config-types"
|
|
5
|
+
|
|
6
|
+
let _config: CamelMindConfig | null = null
|
|
7
|
+
|
|
8
|
+
export function getConfig(): CamelMindConfig {
|
|
9
|
+
if (!_config) _config = siteConfig
|
|
10
|
+
return _config
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isAuthEnabled(): boolean {
|
|
14
|
+
if (process.env.OFFLINE_MODE === "true") return false
|
|
15
|
+
return getConfig().auth.enabled
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isPrivateSite(): boolean {
|
|
19
|
+
return isAuthEnabled() && getConfig().auth.requireLogin
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getAuthConfig(): AuthConfig {
|
|
23
|
+
return getConfig().auth
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isPublicPath(pathname: string): boolean {
|
|
27
|
+
const { publicPaths } = getAuthConfig()
|
|
28
|
+
return publicPaths.some((p) => pathname === p || pathname.startsWith(p + "/"))
|
|
29
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import matter from "gray-matter"
|
|
4
|
+
|
|
5
|
+
export type FrontMatter = {
|
|
6
|
+
title: string
|
|
7
|
+
description?: string
|
|
8
|
+
roles?: string[]
|
|
9
|
+
tags?: string[]
|
|
10
|
+
download_pdf?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type TocEntry = {
|
|
14
|
+
id: string
|
|
15
|
+
text: string
|
|
16
|
+
level: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type DocContent = {
|
|
20
|
+
frontmatter: FrontMatter
|
|
21
|
+
source: string
|
|
22
|
+
toc: TocEntry[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function extractToc(source: string): TocEntry[] {
|
|
26
|
+
const headingRegex = /^(#{2,3})\s+(.+)$/gm
|
|
27
|
+
const entries: TocEntry[] = []
|
|
28
|
+
let match
|
|
29
|
+
|
|
30
|
+
while ((match = headingRegex.exec(source)) !== null) {
|
|
31
|
+
const level = match[1].length
|
|
32
|
+
const text = match[2].trim()
|
|
33
|
+
const id = text
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/[^\w\s-]/g, "")
|
|
36
|
+
.replace(/\s+/g, "-")
|
|
37
|
+
entries.push({ id, text, level })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return entries
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function loadMdxFile(filePath: string): DocContent {
|
|
44
|
+
const fullPath = path.join(process.cwd(), filePath)
|
|
45
|
+
const raw = fs.readFileSync(fullPath, "utf-8")
|
|
46
|
+
const { data, content } = matter(raw)
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
frontmatter: data as FrontMatter,
|
|
50
|
+
source: content,
|
|
51
|
+
toc: extractToc(content),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type NavChild = {
|
|
2
|
+
label: string
|
|
3
|
+
slug: string
|
|
4
|
+
file: string
|
|
5
|
+
roles: string[]
|
|
6
|
+
children?: NavChild[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type NavEntry = {
|
|
10
|
+
label: string
|
|
11
|
+
slug: string
|
|
12
|
+
file: string
|
|
13
|
+
roles: string[]
|
|
14
|
+
section?: NavChild[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type NavGroup = {
|
|
18
|
+
label: string
|
|
19
|
+
dropdown: boolean
|
|
20
|
+
noDropdown?: boolean // show as direct link in top nav, not a dropdown button
|
|
21
|
+
slug?: string // href for the direct link when noDropdown is true
|
|
22
|
+
items: NavEntry[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type NavConfig = {
|
|
26
|
+
nav: (NavEntry | NavGroup)[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isNavGroup(item: NavEntry | NavGroup): item is NavGroup {
|
|
30
|
+
return "dropdown" in item
|
|
31
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import yaml from "js-yaml"
|
|
4
|
+
import type { NavConfig, NavEntry, NavGroup, NavChild } from "./nav-types"
|
|
5
|
+
|
|
6
|
+
export type { NavConfig, NavEntry, NavGroup, NavChild }
|
|
7
|
+
export { isNavGroup } from "./nav-types"
|
|
8
|
+
|
|
9
|
+
let _navCache: NavConfig | null = null
|
|
10
|
+
|
|
11
|
+
export function loadNav(): NavConfig {
|
|
12
|
+
if (process.env.NODE_ENV !== "development" && _navCache) return _navCache
|
|
13
|
+
const navPath = path.join(process.cwd(), "nav", "nav.yml")
|
|
14
|
+
const raw = fs.readFileSync(navPath, "utf-8")
|
|
15
|
+
_navCache = yaml.load(raw) as NavConfig
|
|
16
|
+
return _navCache
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function flattenChildren(children: NavChild[]): NavChild[] {
|
|
20
|
+
return children.flatMap((c) => [c, ...(c.children ? flattenChildren(c.children) : [])])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Extract all slugs from any NavConfig (default or versioned)
|
|
24
|
+
export function getSlugsFromConfig(nav: NavConfig): string[] {
|
|
25
|
+
const slugs: string[] = []
|
|
26
|
+
for (const item of nav.nav) {
|
|
27
|
+
if ("dropdown" in item) {
|
|
28
|
+
const group = item as NavGroup
|
|
29
|
+
// noDropdown groups have a top-level slug (the direct link target)
|
|
30
|
+
if (group.noDropdown && group.slug) slugs.push(group.slug)
|
|
31
|
+
for (const entry of (group.items ?? [])) {
|
|
32
|
+
slugs.push(entry.slug)
|
|
33
|
+
if (entry.section) {
|
|
34
|
+
for (const child of flattenChildren(entry.section)) {
|
|
35
|
+
slugs.push(child.slug)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} else if ("slug" in item) {
|
|
40
|
+
slugs.push((item as NavEntry).slug)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return slugs
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getAllSlugs(): string[] {
|
|
47
|
+
return getSlugsFromConfig(loadNav())
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Search any NavConfig for a slug — used for both default and versioned navs
|
|
51
|
+
export function getEntryBySlugFromConfig(nav: NavConfig, slug: string): (NavEntry | NavChild) | null {
|
|
52
|
+
const normalized = slug.startsWith("/") ? slug : `/${slug}`
|
|
53
|
+
|
|
54
|
+
for (const item of nav.nav) {
|
|
55
|
+
if ("dropdown" in item) {
|
|
56
|
+
const group = item as NavGroup
|
|
57
|
+
// noDropdown group: its top-level slug maps to the first item in the group
|
|
58
|
+
if (group.noDropdown && group.slug === normalized && group.items?.length) {
|
|
59
|
+
return group.items[0]
|
|
60
|
+
}
|
|
61
|
+
for (const entry of (group.items ?? [])) {
|
|
62
|
+
if (entry.slug === normalized) return entry
|
|
63
|
+
if (entry.section) {
|
|
64
|
+
const match = flattenChildren(entry.section).find((c) => c.slug === normalized)
|
|
65
|
+
if (match) return match
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} else if ("slug" in item) {
|
|
69
|
+
const entry = item as NavEntry
|
|
70
|
+
if (entry.slug === normalized) return entry
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getNavEntryBySlug(slug: string): (NavEntry | NavChild) | null {
|
|
78
|
+
return getEntryBySlugFromConfig(loadNav(), slug)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Return the NavGroup that contains the given slug in any NavConfig
|
|
82
|
+
export function getGroupForSlugFromConfig(nav: NavConfig, slug: string): NavGroup | null {
|
|
83
|
+
const normalized = slug.startsWith("/") ? slug : `/${slug}`
|
|
84
|
+
|
|
85
|
+
for (const item of nav.nav) {
|
|
86
|
+
if (!("dropdown" in item)) continue
|
|
87
|
+
const group = item as NavGroup
|
|
88
|
+
if (group.noDropdown && group.slug === normalized) return group
|
|
89
|
+
for (const entry of (group.items ?? [])) {
|
|
90
|
+
if (entry.slug === normalized) return group
|
|
91
|
+
if (entry.section) {
|
|
92
|
+
const match = flattenChildren(entry.section).find((c) => c.slug === normalized)
|
|
93
|
+
if (match) return group
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getGroupForSlug(slug: string): NavGroup | null {
|
|
102
|
+
return getGroupForSlugFromConfig(loadNav(), slug)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Return the section-root NavEntry for a slug in any NavConfig
|
|
106
|
+
export function getSectionForSlugFromConfig(nav: NavConfig, slug: string): NavEntry | null {
|
|
107
|
+
const normalized = slug.startsWith("/") ? slug : `/${slug}`
|
|
108
|
+
|
|
109
|
+
for (const item of nav.nav) {
|
|
110
|
+
if (!("dropdown" in item)) continue
|
|
111
|
+
for (const entry of ((item as NavGroup).items ?? [])) {
|
|
112
|
+
if (entry.slug === normalized) return entry
|
|
113
|
+
if (entry.section) {
|
|
114
|
+
const match = flattenChildren(entry.section).find((c) => c.slug === normalized)
|
|
115
|
+
if (match) return entry
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function getSectionForSlug(slug: string): NavEntry | null {
|
|
124
|
+
return getSectionForSlugFromConfig(loadNav(), slug)
|
|
125
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import yaml from "js-yaml"
|
|
4
|
+
import { loadNav } from "./nav"
|
|
5
|
+
import type { NavConfig } from "./nav-types"
|
|
6
|
+
|
|
7
|
+
export type Version = {
|
|
8
|
+
id: string
|
|
9
|
+
label: string
|
|
10
|
+
stable: boolean
|
|
11
|
+
badge?: string
|
|
12
|
+
nav: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type VersionsConfig = {
|
|
16
|
+
versions: Version[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let _versionsCache: VersionsConfig | null = null
|
|
20
|
+
|
|
21
|
+
export function loadVersions(): VersionsConfig {
|
|
22
|
+
if (_versionsCache) return _versionsCache
|
|
23
|
+
const raw = fs.readFileSync(path.join(process.cwd(), "versions.yml"), "utf-8")
|
|
24
|
+
_versionsCache = yaml.load(raw) as VersionsConfig
|
|
25
|
+
return _versionsCache
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getVersionFromSlug(slug: string): string | null {
|
|
29
|
+
const { versions } = loadVersions()
|
|
30
|
+
for (const v of versions) {
|
|
31
|
+
if (slug.startsWith(`/${v.id}/`) || slug === `/${v.id}`) return v.id
|
|
32
|
+
}
|
|
33
|
+
// no version prefix = default (latest)
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getNavForVersion(versionId: string | null): NavConfig {
|
|
38
|
+
if (!versionId) return loadNav()
|
|
39
|
+
const { versions } = loadVersions()
|
|
40
|
+
const v = versions.find((v) => v.id === versionId)
|
|
41
|
+
if (!v) return loadNav()
|
|
42
|
+
|
|
43
|
+
const raw = fs.readFileSync(path.join(process.cwd(), v.nav), "utf-8")
|
|
44
|
+
return yaml.load(raw) as NavConfig
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getLatestVersion(): Version {
|
|
48
|
+
const { versions } = loadVersions()
|
|
49
|
+
return versions[0]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Given a slug like /v1.9/getting-started/overview, return /getting-started/overview
|
|
53
|
+
export function stripVersionPrefix(slug: string, versionId: string): string {
|
|
54
|
+
return slug.replace(new RegExp(`^/${versionId}`), "") || "/"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Given /getting-started/overview and a target version, return /v1.9/getting-started/overview
|
|
58
|
+
export function swapVersion(slug: string, currentVersionId: string | null, targetVersionId: string): string {
|
|
59
|
+
const bare = currentVersionId ? stripVersionPrefix(slug, currentVersionId) : slug
|
|
60
|
+
return `/${targetVersionId}${bare}`
|
|
61
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
nav:
|
|
2
|
+
- label: "Getting Started"
|
|
3
|
+
dropdown: true
|
|
4
|
+
items:
|
|
5
|
+
- label: "Overview"
|
|
6
|
+
slug: /getting-started/overview
|
|
7
|
+
file: content/getting-started/overview.mdx
|
|
8
|
+
roles: []
|
|
9
|
+
- label: "Installation"
|
|
10
|
+
slug: /getting-started/installation
|
|
11
|
+
file: content/getting-started/installation.mdx
|
|
12
|
+
roles: []
|
|
13
|
+
|
|
14
|
+
- label: "Guides"
|
|
15
|
+
dropdown: true
|
|
16
|
+
items:
|
|
17
|
+
- label: "Writing Docs"
|
|
18
|
+
slug: /guides/writing-docs
|
|
19
|
+
file: content/guides/writing-docs.mdx
|
|
20
|
+
roles: []
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { NextConfig } from "next";
|
|
2
|
+
|
|
3
|
+
const isOffline = process.env.OFFLINE_MODE === "true"
|
|
4
|
+
|
|
5
|
+
const securityHeaders = [
|
|
6
|
+
{ key: "X-Frame-Options", value: "DENY" },
|
|
7
|
+
{ key: "X-Content-Type-Options", value: "nosniff" },
|
|
8
|
+
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
|
9
|
+
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
|
|
10
|
+
{
|
|
11
|
+
key: "Content-Security-Policy",
|
|
12
|
+
value: [
|
|
13
|
+
"default-src 'self'",
|
|
14
|
+
"script-src 'self' 'unsafe-inline'",
|
|
15
|
+
"style-src 'self' 'unsafe-inline'",
|
|
16
|
+
"img-src 'self' data:",
|
|
17
|
+
"font-src 'self'",
|
|
18
|
+
"connect-src 'self'",
|
|
19
|
+
"frame-ancestors 'none'",
|
|
20
|
+
].join("; "),
|
|
21
|
+
},
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const nextConfig: NextConfig = {
|
|
25
|
+
// Static export for offline builds — no server required
|
|
26
|
+
...(isOffline ? { output: "export", trailingSlash: true } : {}),
|
|
27
|
+
// Security headers are only meaningful when running as a server (not static export)
|
|
28
|
+
...(!isOffline ? {
|
|
29
|
+
async headers() {
|
|
30
|
+
return [{ source: "/(.*)", headers: securityHeaders }]
|
|
31
|
+
},
|
|
32
|
+
} : {}),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default nextConfig;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server"
|
|
2
|
+
import { jwtVerify } from "jose"
|
|
3
|
+
import { COOKIE_NAME, getSecret } from "@/lib/auth"
|
|
4
|
+
import { isAuthEnabled, isPrivateSite, isPublicPath } from "@/lib/config"
|
|
5
|
+
|
|
6
|
+
export async function proxy(req: NextRequest) {
|
|
7
|
+
const { pathname } = req.nextUrl
|
|
8
|
+
|
|
9
|
+
// Auth disabled — all traffic passes through
|
|
10
|
+
if (!isAuthEnabled()) {
|
|
11
|
+
return NextResponse.next()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Always allow public paths and Next.js internals
|
|
15
|
+
if (
|
|
16
|
+
isPublicPath(pathname) ||
|
|
17
|
+
pathname.startsWith("/_next") ||
|
|
18
|
+
pathname.startsWith("/favicon")
|
|
19
|
+
) {
|
|
20
|
+
return NextResponse.next()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// RBAC-only mode: per-page enforcement happens in page.tsx
|
|
24
|
+
if (!isPrivateSite()) {
|
|
25
|
+
return NextResponse.next()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Private site mode: require valid session for all non-public paths
|
|
29
|
+
const token = req.cookies.get(COOKIE_NAME)?.value
|
|
30
|
+
if (!token) {
|
|
31
|
+
const loginUrl = new URL("/login", req.url)
|
|
32
|
+
loginUrl.searchParams.set("returnTo", pathname)
|
|
33
|
+
return NextResponse.redirect(loginUrl)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
await jwtVerify(token, getSecret())
|
|
38
|
+
return NextResponse.next()
|
|
39
|
+
} catch {
|
|
40
|
+
const loginUrl = new URL("/login", req.url)
|
|
41
|
+
loginUrl.searchParams.set("returnTo", pathname)
|
|
42
|
+
const res = NextResponse.redirect(loginUrl)
|
|
43
|
+
res.cookies.set(COOKIE_NAME, "", { maxAge: 0, path: "/" })
|
|
44
|
+
return res
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const config = {
|
|
49
|
+
matcher: ["/((?!_next/static|_next/image|favicon.ico|public/).*)"],
|
|
50
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Build a self-contained offline ZIP for one version of the help center.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# ./scripts/build-offline.sh v2.0
|
|
6
|
+
# ./scripts/build-offline.sh v1.9
|
|
7
|
+
#
|
|
8
|
+
# Output: offline-builds/gw-helpcenter-<version>-offline.zip
|
|
9
|
+
# Users unzip and run: npx serve out/ (or open index.html directly)
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
VERSION=${1:-v2.0}
|
|
14
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
15
|
+
OUT_DIR="$ROOT/out"
|
|
16
|
+
BUILDS_DIR="$ROOT/offline-builds"
|
|
17
|
+
ZIP_NAME="camelmind-${VERSION}-offline.zip"
|
|
18
|
+
|
|
19
|
+
# API routes that require a live server — stash them outside app/ during static export
|
|
20
|
+
SERVER_ROUTES=(
|
|
21
|
+
"app/api/raw/route.ts"
|
|
22
|
+
"app/api/download/route.ts"
|
|
23
|
+
"app/api/search/route.ts"
|
|
24
|
+
"app/api/auth"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
stash_routes() {
|
|
28
|
+
mkdir -p "$ROOT/.offline-stash"
|
|
29
|
+
for r in "${SERVER_ROUTES[@]}"; do
|
|
30
|
+
if [ -e "$ROOT/$r" ]; then
|
|
31
|
+
# Flatten path for stash filename
|
|
32
|
+
stash_name="${r//\//__}"
|
|
33
|
+
mv "$ROOT/$r" "$ROOT/.offline-stash/$stash_name"
|
|
34
|
+
fi
|
|
35
|
+
done
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
restore_routes() {
|
|
39
|
+
for r in "${SERVER_ROUTES[@]}"; do
|
|
40
|
+
stash_name="${r//\//__}"
|
|
41
|
+
if [ -e "$ROOT/.offline-stash/$stash_name" ]; then
|
|
42
|
+
# Recreate parent dir if needed (e.g. app/api/raw/)
|
|
43
|
+
mkdir -p "$ROOT/$(dirname "$r")"
|
|
44
|
+
mv "$ROOT/.offline-stash/$stash_name" "$ROOT/$r"
|
|
45
|
+
fi
|
|
46
|
+
done
|
|
47
|
+
rmdir "$ROOT/.offline-stash" 2>/dev/null || true
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Always restore on exit (even on error)
|
|
51
|
+
trap restore_routes EXIT
|
|
52
|
+
|
|
53
|
+
echo "▶ Building offline package for version: $VERSION"
|
|
54
|
+
|
|
55
|
+
# 1. Pre-build the search index (no API server in static export)
|
|
56
|
+
echo " → Generating search index..."
|
|
57
|
+
cd "$ROOT"
|
|
58
|
+
npx tsx scripts/build-search-index.ts
|
|
59
|
+
|
|
60
|
+
# 2. Stash server-only routes so next build doesn't complain
|
|
61
|
+
echo " → Stashing server-only API routes..."
|
|
62
|
+
stash_routes
|
|
63
|
+
|
|
64
|
+
# 3. Static export
|
|
65
|
+
echo " → Running next build (OFFLINE_MODE=true, TARGET_VERSION=$VERSION)..."
|
|
66
|
+
OFFLINE_MODE=true TARGET_VERSION="$VERSION" npx next build
|
|
67
|
+
|
|
68
|
+
# 3. Write launcher scripts and README into the export
|
|
69
|
+
|
|
70
|
+
cat > "$OUT_DIR/launch.sh" <<'LAUNCHER'
|
|
71
|
+
#!/usr/bin/env bash
|
|
72
|
+
# Game Warden Help Center — offline launcher (Mac / Linux)
|
|
73
|
+
PORT=8765
|
|
74
|
+
URL="http://localhost:$PORT/home/"
|
|
75
|
+
|
|
76
|
+
# Try Python 3 first (available on most systems without internet)
|
|
77
|
+
if command -v python3 &>/dev/null; then
|
|
78
|
+
echo "Starting local server on $URL"
|
|
79
|
+
python3 -m http.server $PORT &
|
|
80
|
+
SERVER_PID=$!
|
|
81
|
+
sleep 1
|
|
82
|
+
# Open browser
|
|
83
|
+
if command -v open &>/dev/null; then open "$URL"
|
|
84
|
+
elif command -v xdg-open &>/dev/null; then xdg-open "$URL"
|
|
85
|
+
fi
|
|
86
|
+
echo "Press Ctrl+C to stop the server."
|
|
87
|
+
wait $SERVER_PID
|
|
88
|
+
# Fall back to Python 2
|
|
89
|
+
elif command -v python &>/dev/null; then
|
|
90
|
+
echo "Starting local server on $URL"
|
|
91
|
+
python -m SimpleHTTPServer $PORT &
|
|
92
|
+
SERVER_PID=$!
|
|
93
|
+
sleep 1
|
|
94
|
+
if command -v open &>/dev/null; then open "$URL"
|
|
95
|
+
elif command -v xdg-open &>/dev/null; then xdg-open "$URL"
|
|
96
|
+
fi
|
|
97
|
+
echo "Press Ctrl+C to stop the server."
|
|
98
|
+
wait $SERVER_PID
|
|
99
|
+
# Fall back to Node / npx
|
|
100
|
+
elif command -v npx &>/dev/null; then
|
|
101
|
+
echo "Starting local server on $URL"
|
|
102
|
+
if command -v open &>/dev/null; then sleep 2 && open "$URL" &
|
|
103
|
+
elif command -v xdg-open &>/dev/null; then sleep 2 && xdg-open "$URL" &
|
|
104
|
+
fi
|
|
105
|
+
npx serve . -p $PORT
|
|
106
|
+
else
|
|
107
|
+
echo "ERROR: Python 3, Python 2, or Node.js is required to run this package."
|
|
108
|
+
echo "Install Python from https://www.python.org or Node.js from https://nodejs.org"
|
|
109
|
+
exit 1
|
|
110
|
+
fi
|
|
111
|
+
LAUNCHER
|
|
112
|
+
chmod +x "$OUT_DIR/launch.sh"
|
|
113
|
+
|
|
114
|
+
cat > "$OUT_DIR/launch.bat" <<'LAUNCHER'
|
|
115
|
+
@echo off
|
|
116
|
+
REM Game Warden Help Center — offline launcher (Windows)
|
|
117
|
+
set PORT=8765
|
|
118
|
+
set URL=http://localhost:%PORT%/home/
|
|
119
|
+
|
|
120
|
+
REM Try Python 3
|
|
121
|
+
python --version >nul 2>&1
|
|
122
|
+
if %errorlevel% == 0 (
|
|
123
|
+
echo Starting local server on %URL%
|
|
124
|
+
start "" "%URL%"
|
|
125
|
+
python -m http.server %PORT%
|
|
126
|
+
goto :end
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
REM Try Python via py launcher
|
|
130
|
+
py --version >nul 2>&1
|
|
131
|
+
if %errorlevel% == 0 (
|
|
132
|
+
echo Starting local server on %URL%
|
|
133
|
+
start "" "%URL%"
|
|
134
|
+
py -m http.server %PORT%
|
|
135
|
+
goto :end
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
REM Try npx (Node.js)
|
|
139
|
+
npx --version >nul 2>&1
|
|
140
|
+
if %errorlevel% == 0 (
|
|
141
|
+
echo Starting local server on %URL%
|
|
142
|
+
start "" "%URL%"
|
|
143
|
+
npx serve . -p %PORT%
|
|
144
|
+
goto :end
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
echo ERROR: Python or Node.js is required to run this package.
|
|
148
|
+
echo Install Python from https://www.python.org or Node.js from https://nodejs.org
|
|
149
|
+
pause
|
|
150
|
+
:end
|
|
151
|
+
LAUNCHER
|
|
152
|
+
|
|
153
|
+
cat > "$OUT_DIR/README.txt" <<EOF
|
|
154
|
+
Game Warden Help Center — Offline Package ($VERSION)
|
|
155
|
+
====================================================
|
|
156
|
+
|
|
157
|
+
QUICK START
|
|
158
|
+
-----------
|
|
159
|
+
Mac / Linux: Double-click launch.sh (or run: ./launch.sh in Terminal)
|
|
160
|
+
Windows: Double-click launch.bat
|
|
161
|
+
|
|
162
|
+
This opens a local web server at http://localhost:8765 and launches your
|
|
163
|
+
browser automatically. Press Ctrl+C (or close Terminal) to stop.
|
|
164
|
+
|
|
165
|
+
REQUIREMENTS
|
|
166
|
+
------------
|
|
167
|
+
Python 3 is recommended (pre-installed on most Macs and Linux systems).
|
|
168
|
+
Windows users may need to install Python from https://www.python.org
|
|
169
|
+
Node.js (https://nodejs.org) works as a fallback on all platforms.
|
|
170
|
+
|
|
171
|
+
SEARCH
|
|
172
|
+
------
|
|
173
|
+
Full-text search works offline — no internet connection required.
|
|
174
|
+
|
|
175
|
+
NOTES
|
|
176
|
+
-----
|
|
177
|
+
- Do not open index.html directly in your browser; navigation will break.
|
|
178
|
+
Always use the launcher or start a local server first.
|
|
179
|
+
- This package was generated for authenticated Game Warden users.
|
|
180
|
+
Do not redistribute without authorization.
|
|
181
|
+
EOF
|
|
182
|
+
|
|
183
|
+
# 4. Zip the output
|
|
184
|
+
echo " → Packaging..."
|
|
185
|
+
mkdir -p "$BUILDS_DIR"
|
|
186
|
+
cd "$OUT_DIR"
|
|
187
|
+
zip -r "$BUILDS_DIR/$ZIP_NAME" . --quiet
|
|
188
|
+
|
|
189
|
+
echo "✓ Done: offline-builds/$ZIP_NAME ($(du -sh "$BUILDS_DIR/$ZIP_NAME" | cut -f1))"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Build the master PDF for one version of the Help Center.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# ./scripts/build-pdf.sh v2.0
|
|
6
|
+
#
|
|
7
|
+
# Requires: the Next.js dev server running on port 3000 (npm run dev)
|
|
8
|
+
# Output: offline-builds/Game-Warden-Help-Center-<version>.pdf
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
VERSION=${1:-v2.0}
|
|
13
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
14
|
+
PAGES_DIR="$ROOT/.pdf-pages"
|
|
15
|
+
PORT=3000
|
|
16
|
+
|
|
17
|
+
cleanup() {
|
|
18
|
+
rm -rf "$PAGES_DIR"
|
|
19
|
+
}
|
|
20
|
+
trap cleanup EXIT
|
|
21
|
+
|
|
22
|
+
echo "▶ Building master PDF for version: $VERSION"
|
|
23
|
+
|
|
24
|
+
# Verify dev server is running
|
|
25
|
+
if ! curl -s -o /dev/null "http://localhost:$PORT/home"; then
|
|
26
|
+
echo "✗ Dev server not running on port $PORT"
|
|
27
|
+
echo " Start it with: npm run dev"
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Generate per-page PDFs against the live dev server
|
|
32
|
+
echo " → Rendering pages via Playwright (port $PORT)..."
|
|
33
|
+
cd "$ROOT"
|
|
34
|
+
npx tsx scripts/generate-pdfs.ts --port="$PORT" --out="$PAGES_DIR"
|
|
35
|
+
|
|
36
|
+
# Assemble master PDF
|
|
37
|
+
echo " → Assembling master PDF..."
|
|
38
|
+
npx tsx scripts/generate-master-pdf.ts --version="$VERSION" --pages="$PAGES_DIR" --out="$ROOT/offline-builds"
|
|
39
|
+
|
|
40
|
+
SIZE=$(du -sh "$ROOT/offline-builds/Game-Warden-Help-Center-${VERSION}.pdf" | cut -f1)
|
|
41
|
+
echo "✓ Done: offline-builds/Game-Warden-Help-Center-${VERSION}.pdf ($SIZE)"
|