create-vexcms 0.1.0-alpha.5 → 0.1.0-alpha.7
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 +33 -0
- package/package.json +1 -1
- package/templates/base-nextjs/convex/auth/index.ts +2 -2
- package/templates/base-nextjs/convex/auth/sessions.ts +3 -3
- package/templates/base-nextjs/convex/vex/firstUser.ts +2 -2
- package/templates/base-nextjs/next.config.ts +34 -5
- package/templates/base-nextjs/package.json +6 -6
- package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +1 -1
- package/templates/base-nextjs/src/auth/serverUtils.ts +5 -5
- package/templates/base-nextjs/src/components/WelcomePage.tsx +54 -43
- package/templates/base-nextjs/src/vexcms/collections/users.ts +1 -1
- package/templates/marketing-site/convex/schema.ts +4 -4
- package/templates/marketing-site/convex/seed.ts +286 -97
- package/templates/marketing-site/convex/theme.ts +2 -2
- package/templates/marketing-site/convex/vex.schema.ts +70 -38
- package/templates/marketing-site/src/app/(frontend)/(site)/PageContent.tsx +7 -8
- package/templates/marketing-site/src/app/(frontend)/(site)/[slug]/page.tsx +4 -2
- package/templates/marketing-site/src/app/(frontend)/(site)/layout.tsx +13 -3
- package/templates/marketing-site/src/app/(frontend)/(site)/page.tsx +4 -2
- package/templates/marketing-site/src/app/globals.css +374 -0
- package/templates/marketing-site/src/app/layout.tsx +14 -0
- package/templates/marketing-site/src/components/BrandMark.tsx +27 -0
- package/templates/marketing-site/src/components/CodePane.tsx +86 -0
- package/templates/marketing-site/src/components/Container.tsx +25 -0
- package/templates/marketing-site/src/components/CopyButton.tsx +80 -0
- package/templates/marketing-site/src/components/FirstAdminBootstrap.tsx +20 -17
- package/templates/marketing-site/src/components/InstallCommand.tsx +29 -0
- package/templates/marketing-site/src/components/MediaImage.tsx +59 -0
- package/templates/marketing-site/src/components/SectionHeader.tsx +51 -0
- package/templates/marketing-site/src/components/SiteFooter.tsx +8 -6
- package/templates/marketing-site/src/components/SiteHeader.tsx +8 -6
- package/templates/marketing-site/src/components/ThemeLive.tsx +1 -2
- package/templates/marketing-site/src/components/ThemeStyle.tsx +1 -2
- package/templates/marketing-site/src/components/motion-primitives/text-effect.tsx +19 -12
- package/templates/marketing-site/src/lib/highlight.ts +71 -0
- package/templates/marketing-site/src/lib/metadata.ts +19 -21
- package/templates/marketing-site/src/lib/shikiStarkEmber.json +57 -0
- package/templates/marketing-site/src/proxy.ts +123 -0
- package/templates/marketing-site/src/vex.config.ts +1 -2
- package/templates/marketing-site/src/vex.types.ts +92 -75
- package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +1 -0
- package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +40 -24
- package/templates/marketing-site/src/vexcms/blocks/CodeShowcase/config.ts +62 -0
- package/templates/marketing-site/src/vexcms/blocks/CodeShowcase/index.tsx +106 -0
- package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +1 -0
- package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +63 -50
- package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +1 -0
- package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +43 -58
- package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +3 -0
- package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +90 -53
- package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +3 -0
- package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +205 -138
- package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +26 -11
- package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +172 -83
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +1 -0
- package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +59 -51
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +26 -5
- package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +255 -73
- package/templates/marketing-site/src/vexcms/blocks/Split/config.ts +62 -0
- package/templates/marketing-site/src/vexcms/blocks/Split/index.tsx +186 -0
- package/templates/marketing-site/src/vexcms/blocks/Stats/config.ts +35 -0
- package/templates/marketing-site/src/vexcms/blocks/Stats/index.tsx +77 -0
- package/templates/marketing-site/src/vexcms/blocks/config.ts +27 -11
- package/templates/marketing-site/src/vexcms/blocks/constants.ts +3 -0
- package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -0
- package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -0
- package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -0
- package/templates/marketing-site/src/vexcms/collections/pages.ts +1 -0
- package/templates/marketing-site/src/vexcms/collections/themeColors.ts +14 -14
- package/templates/marketing-site/src/vexcms/collections/users.ts +20 -0
package/dist/index.js
CHANGED
|
@@ -2068,6 +2068,10 @@ function stripOrgFromDbConstants(filePath, content) {
|
|
|
2068
2068
|
}
|
|
2069
2069
|
|
|
2070
2070
|
// src/installers/base.ts
|
|
2071
|
+
var MARKETING_DEPENDENCIES = {
|
|
2072
|
+
/** Render-time syntax highlighting for the CodeShowcase and Split blocks. */
|
|
2073
|
+
shiki: "^4.4.3"
|
|
2074
|
+
};
|
|
2071
2075
|
var VexFrameworkInstaller = class {
|
|
2072
2076
|
/**
|
|
2073
2077
|
* Binds this installer to the destination directory and package name used
|
|
@@ -2106,6 +2110,34 @@ var VexFrameworkInstaller = class {
|
|
|
2106
2110
|
}
|
|
2107
2111
|
await overlayTemplate({ overlayDir, targetDir: this.targetPath });
|
|
2108
2112
|
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Apply the parts of the marketing overlay that copying files cannot express.
|
|
2115
|
+
*
|
|
2116
|
+
* `overlayTemplate` only adds and overwrites — it has no way to remove a
|
|
2117
|
+
* base file, and it does not touch `package.json`. Two things therefore
|
|
2118
|
+
* have to happen here:
|
|
2119
|
+
*
|
|
2120
|
+
* 1. **Delete base's `(frontend)/page.tsx`.** It and the overlay's
|
|
2121
|
+
* `(frontend)/(site)/page.tsx` both resolve to `/`. Next picks base's,
|
|
2122
|
+
* so the marketing home page is unreachable and every scaffold shows the
|
|
2123
|
+
* bare bootstrap `WelcomePage` instead of the seeded site. The `(site)`
|
|
2124
|
+
* route group owns `/` in a marketing scaffold.
|
|
2125
|
+
* 2. **Add `shiki`.** The CodeShowcase and Split blocks highlight at render
|
|
2126
|
+
* time in a server component. It is a marketing-only dependency, so it
|
|
2127
|
+
* is added here rather than carried in base's `package.json` where a
|
|
2128
|
+
* `--bare` scaffold would install it for nothing.
|
|
2129
|
+
*/
|
|
2130
|
+
async finalizeMarketingOverlay() {
|
|
2131
|
+
const homeRoute = path2.join(this.targetPath, "src/app/(frontend)/page.tsx");
|
|
2132
|
+
await fs5.remove(homeRoute);
|
|
2133
|
+
const pkgPath = path2.join(this.targetPath, "package.json");
|
|
2134
|
+
const pkg = await fs5.readJson(pkgPath);
|
|
2135
|
+
pkg.dependencies = { ...pkg.dependencies, shiki: MARKETING_DEPENDENCIES.shiki };
|
|
2136
|
+
pkg.dependencies = Object.fromEntries(
|
|
2137
|
+
Object.entries(pkg.dependencies).sort(([a], [b]) => a.localeCompare(b))
|
|
2138
|
+
);
|
|
2139
|
+
await fs5.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
2140
|
+
}
|
|
2109
2141
|
/**
|
|
2110
2142
|
* Update the package.json name field, the vex:update script (to match the
|
|
2111
2143
|
* detected package manager), and README.md's `{{PROJECT_NAME}}` heading.
|
|
@@ -2398,6 +2430,7 @@ var VexFrameworkInstaller = class {
|
|
|
2398
2430
|
const overlaySpinner = ora("Applying marketing site template...").start();
|
|
2399
2431
|
try {
|
|
2400
2432
|
await this.applyTemplateOverlay("marketing-site");
|
|
2433
|
+
await this.finalizeMarketingOverlay();
|
|
2401
2434
|
overlaySpinner.succeed("Marketing site template applied");
|
|
2402
2435
|
} catch (error) {
|
|
2403
2436
|
overlaySpinner.fail("Failed to apply template overlay");
|
package/package.json
CHANGED
|
@@ -3,10 +3,10 @@ import type { GenericActionCtx } from "convex/server"
|
|
|
3
3
|
import { createBetterAuthAdapter } from "@vexcms/better-auth"
|
|
4
4
|
import { betterAuth } from "better-auth"
|
|
5
5
|
|
|
6
|
-
import { authOptions } from "./options"
|
|
7
|
-
|
|
8
6
|
import type { DataModel } from "../_generated/dataModel"
|
|
9
7
|
|
|
8
|
+
import { authOptions } from "./options"
|
|
9
|
+
|
|
10
10
|
export const createAuth = (ctx: GenericActionCtx<DataModel>, { optionsOnly } = { optionsOnly: false }) => {
|
|
11
11
|
return betterAuth({
|
|
12
12
|
database: createBetterAuthAdapter(ctx),
|
|
@@ -19,7 +19,7 @@ export const getSessionWithUser = query({
|
|
|
19
19
|
.withIndex("by_token", (q) => q.eq("token", args.sessionToken))
|
|
20
20
|
.first()
|
|
21
21
|
|
|
22
|
-
if (!session) return null
|
|
22
|
+
if (!session) {return null}
|
|
23
23
|
|
|
24
24
|
if (session.expiresAt < Date.now()) {
|
|
25
25
|
console.error("[getSessionWithUser] Session Expired")
|
|
@@ -27,10 +27,10 @@ export const getSessionWithUser = query({
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const userId = session.userId
|
|
30
|
-
if (!userId) return null
|
|
30
|
+
if (!userId) {return null}
|
|
31
31
|
|
|
32
32
|
const user = await ctx.db.get(userId as Id<typeof TABLE_SLUG_USERS>)
|
|
33
|
-
if (!user) return null
|
|
33
|
+
if (!user) {return null}
|
|
34
34
|
|
|
35
35
|
return {
|
|
36
36
|
session: {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ConvexError } from "convex/values"
|
|
2
2
|
|
|
3
|
-
import { mutation, query } from "../_generated/server"
|
|
4
|
-
|
|
5
3
|
import { TABLE_SLUG_USERS } from "~/db/constants"
|
|
6
4
|
import { USER_ROLES } from "~/db/constants/auth"
|
|
7
5
|
|
|
6
|
+
import { mutation, query } from "../_generated/server"
|
|
7
|
+
|
|
8
8
|
/**
|
|
9
9
|
* Check whether the admin panel has been bootstrapped (at least one admin
|
|
10
10
|
* exists). Used by `WelcomePage` to decide between "Sign Up" and "Sign In".
|
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
import type { NextConfig } from "next"
|
|
2
2
|
|
|
3
|
+
import { existsSync } from "node:fs"
|
|
4
|
+
import { dirname, join, resolve } from "node:path"
|
|
5
|
+
|
|
3
6
|
import { env } from "./src/env.mjs"
|
|
4
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Walks up from the app directory looking for `pnpm-workspace.yaml`.
|
|
10
|
+
*
|
|
11
|
+
* @param from - Directory to start from.
|
|
12
|
+
* @returns The workspace root when this app is a workspace member, otherwise
|
|
13
|
+
* `from` unchanged.
|
|
14
|
+
*/
|
|
15
|
+
function findWorkspaceRoot(from: string): string {
|
|
16
|
+
let dir = resolve(from)
|
|
17
|
+
for (;;) {
|
|
18
|
+
if (existsSync(join(dir, "pnpm-workspace.yaml"))) {return dir}
|
|
19
|
+
const parent = dirname(dir)
|
|
20
|
+
if (parent === dir) {return resolve(from)}
|
|
21
|
+
dir = parent
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
5
25
|
// Convex file storage URLs (`next/image` sources for uploaded media) are
|
|
6
26
|
// served from `<deployment>.convex.cloud` — derive the hostname from the
|
|
7
27
|
// validated NEXT_PUBLIC_CONVEX_URL so scaffolds work without a manual
|
|
@@ -20,12 +40,21 @@ const nextConfig: NextConfig = {
|
|
|
20
40
|
},
|
|
21
41
|
allowedDevOrigins: ["127.0.01", "localhost"],
|
|
22
42
|
reactCompiler: true,
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
43
|
+
// Turbopack's root sets the file-tracing root AND Tailwind's source-scan
|
|
44
|
+
// root, so it has to be the directory that actually owns `node_modules`.
|
|
45
|
+
//
|
|
46
|
+
// Standalone scaffold: that is this app. Pinning it stops Next walking up
|
|
47
|
+
// and adopting an unrelated outer lockfile.
|
|
48
|
+
//
|
|
49
|
+
// pnpm workspace member: it is the workspace root — dependencies are
|
|
50
|
+
// hoisted into the root virtual store and `next` is only reachable from
|
|
51
|
+
// there. Pinning to the app dir makes the build fail with "Could not find
|
|
52
|
+
// the Next.js package".
|
|
53
|
+
//
|
|
54
|
+
// Detecting `pnpm-workspace.yaml` picks the right answer in both cases
|
|
55
|
+
// without the template needing a monorepo-specific variant.
|
|
27
56
|
turbopack: {
|
|
28
|
-
root: import.meta.dirname,
|
|
57
|
+
root: findWorkspaceRoot(import.meta.dirname),
|
|
29
58
|
},
|
|
30
59
|
images: {
|
|
31
60
|
// Additional remote image hosts (e.g. an external CMS or CDN) can be
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"@t3-oss/env-nextjs": "0.13.11",
|
|
25
25
|
"@tanstack/react-form": "1.33.1",
|
|
26
26
|
"@tanstack/react-query": "5.101.2",
|
|
27
|
-
"@vexcms/better-auth": "~0.1.0-alpha.
|
|
28
|
-
"@vexcms/core": "~0.1.0-alpha.
|
|
29
|
-
"@vexcms/file-storage-convex": "~0.1.0-alpha.
|
|
30
|
-
"@vexcms/next": "~0.1.0-alpha.
|
|
31
|
-
"@vexcms/react": "~0.1.0-alpha.
|
|
27
|
+
"@vexcms/better-auth": "~0.1.0-alpha.7",
|
|
28
|
+
"@vexcms/core": "~0.1.0-alpha.7",
|
|
29
|
+
"@vexcms/file-storage-convex": "~0.1.0-alpha.7",
|
|
30
|
+
"@vexcms/next": "~0.1.0-alpha.7",
|
|
31
|
+
"@vexcms/react": "~0.1.0-alpha.7",
|
|
32
32
|
"better-auth": "1.6.23",
|
|
33
33
|
"class-variance-authority": "0.7.1",
|
|
34
34
|
"clsx": "2.1.1",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@types/node": "20.19.43",
|
|
53
53
|
"@types/react": "19.2.17",
|
|
54
54
|
"@types/react-dom": "19.2.3",
|
|
55
|
-
"@vexcms/cli": "~0.1.0-alpha.
|
|
55
|
+
"@vexcms/cli": "~0.1.0-alpha.7",
|
|
56
56
|
"babel-plugin-react-compiler": "1.0.0",
|
|
57
57
|
"eslint": "9.39.5",
|
|
58
58
|
"eslint-config-next": "15.5.9",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { api } from "@convex/_generated/api";
|
|
2
|
-
import { NextAdminPage } from "@vexcms/next/server";
|
|
3
2
|
import { canAccessAdminPanel } from "@vexcms/core";
|
|
3
|
+
import { NextAdminPage } from "@vexcms/next/server";
|
|
4
4
|
import { redirect } from "next/navigation";
|
|
5
5
|
|
|
6
6
|
import { fetchAuthQuery, getToken } from "~/auth/server";
|
|
@@ -6,7 +6,7 @@ export async function getSessionToken() {
|
|
|
6
6
|
const cookieStore = await cookies()
|
|
7
7
|
const sessionTokenCookie = cookieStore.get("better-auth.session_token")?.value
|
|
8
8
|
|
|
9
|
-
if (!sessionTokenCookie) return null
|
|
9
|
+
if (!sessionTokenCookie) {return null}
|
|
10
10
|
|
|
11
11
|
// better-auth stores tokens as "<raw_token>.<hmac_signature>" (2 parts, not a JWT)
|
|
12
12
|
// The raw token (first part) is what's indexed in the DB by_token
|
|
@@ -24,12 +24,12 @@ export async function getSessionToken() {
|
|
|
24
24
|
export const getCurrentUser = async () => {
|
|
25
25
|
try {
|
|
26
26
|
const sessionToken = await getSessionToken()
|
|
27
|
-
if (!sessionToken) return null
|
|
27
|
+
if (!sessionToken) {return null}
|
|
28
28
|
|
|
29
29
|
const session = await fetchQuery(api.auth.sessions.getSessionWithUser, {
|
|
30
30
|
sessionToken,
|
|
31
31
|
})
|
|
32
|
-
if (!session?.user) return null
|
|
32
|
+
if (!session?.user) {return null}
|
|
33
33
|
return session.user
|
|
34
34
|
} catch (error) {
|
|
35
35
|
console.error("Error getting current user:", error)
|
|
@@ -43,12 +43,12 @@ export const getCurrentUser = async () => {
|
|
|
43
43
|
export async function getSession() {
|
|
44
44
|
try {
|
|
45
45
|
const sessionToken = await getSessionToken()
|
|
46
|
-
if (!sessionToken) return null
|
|
46
|
+
if (!sessionToken) {return null}
|
|
47
47
|
|
|
48
48
|
const session = await fetchQuery(api.auth.sessions.getSessionWithUser, {
|
|
49
49
|
sessionToken,
|
|
50
50
|
})
|
|
51
|
-
if (!session?.user) return null
|
|
51
|
+
if (!session?.user) {return null}
|
|
52
52
|
return session
|
|
53
53
|
} catch (error) {
|
|
54
54
|
console.error("[getSession] Error:", error)
|
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
+
import { api } from "@convex/_generated/api"
|
|
3
4
|
import { useMutation, useQuery } from "convex/react"
|
|
4
5
|
import { usePathname, useRouter } from "next/navigation"
|
|
5
|
-
import { useEffect, useState } from "react"
|
|
6
|
+
import { useEffect, useRef, useState } from "react"
|
|
6
7
|
|
|
7
|
-
import { api } from "@convex/_generated/api"
|
|
8
8
|
import { useSession } from "~/auth/client"
|
|
9
9
|
|
|
10
|
-
function LoadingSpinner() {
|
|
11
|
-
return (
|
|
12
|
-
<svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
13
|
-
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
14
|
-
<path
|
|
15
|
-
className="opacity-75"
|
|
16
|
-
fill="currentColor"
|
|
17
|
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
18
|
-
/>
|
|
19
|
-
</svg>
|
|
20
|
-
)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
10
|
/**
|
|
24
11
|
* Landing UI for a project that has not been bootstrapped yet (no admin user),
|
|
25
12
|
* and the "Go to Admin" / "Sign in" entry point once it has.
|
|
@@ -41,33 +28,44 @@ export function WelcomePage() {
|
|
|
41
28
|
const [promoting, setPromoting] = useState(false)
|
|
42
29
|
const [navigating, setNavigating] = useState(false)
|
|
43
30
|
|
|
44
|
-
// Reset navigating
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
31
|
+
// Reset `navigating` when the user lands back on the home page (e.g. they
|
|
32
|
+
// dismissed the intercepting auth dialog). Adjusting state during render on
|
|
33
|
+
// a changed input is the pattern React documents for this; an effect would
|
|
34
|
+
// paint the stale spinner for a frame first.
|
|
35
|
+
const [lastPathname, setLastPathname] = useState(pathname)
|
|
36
|
+
if (lastPathname !== pathname) {
|
|
37
|
+
setLastPathname(pathname)
|
|
38
|
+
if (pathname === "/") {setNavigating(false)}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Run-once latch. `promoting` and `navigating` both render, so they stay
|
|
42
|
+
// state; the guard does not, so it is a ref — that keeps it out of the
|
|
43
|
+
// dependency array and stops the effect re-running just to re-read it.
|
|
44
|
+
const promotingRef = useRef(false)
|
|
51
45
|
|
|
52
|
-
// If the user is signed in and this is a fresh project, try to promote them
|
|
53
46
|
useEffect(() => {
|
|
54
|
-
if (session?.user
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
47
|
+
if (!session?.user || isBootstrapped !== false || promotingRef.current) {return}
|
|
48
|
+
|
|
49
|
+
promotingRef.current = true
|
|
50
|
+
setPromoting(true)
|
|
51
|
+
setNavigating(true)
|
|
52
|
+
|
|
53
|
+
promoteFirstAdmin()
|
|
54
|
+
.then((result) => {
|
|
55
|
+
if (result.promoted) {
|
|
56
|
+
router.push("/admin")
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
promotingRef.current = false
|
|
60
|
+
setPromoting(false)
|
|
61
|
+
setNavigating(false)
|
|
62
|
+
})
|
|
63
|
+
.catch(() => {
|
|
64
|
+
promotingRef.current = false
|
|
65
|
+
setPromoting(false)
|
|
66
|
+
setNavigating(false)
|
|
67
|
+
})
|
|
68
|
+
}, [session, isBootstrapped, promoteFirstAdmin, router])
|
|
71
69
|
|
|
72
70
|
// Treat undefined (query still loading) as not bootstrapped — show the welcome page immediately
|
|
73
71
|
const bootstrapped = isBootstrapped === true
|
|
@@ -93,17 +91,17 @@ export function WelcomePage() {
|
|
|
93
91
|
|
|
94
92
|
<div className="flex gap-4">
|
|
95
93
|
{session?.user ? (
|
|
96
|
-
<button onClick={() => handleNavigate("/admin")}
|
|
94
|
+
<button className={buttonClass} disabled={navigating} onClick={() => handleNavigate("/admin")}>
|
|
97
95
|
{navigating && <LoadingSpinner />}
|
|
98
96
|
{navigating ? "Loading..." : "Go to Admin Panel"}
|
|
99
97
|
</button>
|
|
100
98
|
) : bootstrapped ? (
|
|
101
|
-
<button onClick={() => handleNavigate("/auth/sign-in")}
|
|
99
|
+
<button className={buttonClass} disabled={navigating} onClick={() => handleNavigate("/auth/sign-in")}>
|
|
102
100
|
{navigating && <LoadingSpinner />}
|
|
103
101
|
{navigating ? "Loading..." : "Sign In"}
|
|
104
102
|
</button>
|
|
105
103
|
) : (
|
|
106
|
-
<button onClick={() => handleNavigate("/auth/sign-up")}
|
|
104
|
+
<button className={buttonClass} disabled={navigating} onClick={() => handleNavigate("/auth/sign-up")}>
|
|
107
105
|
{navigating && <LoadingSpinner />}
|
|
108
106
|
{navigating ? "Loading..." : "Create Admin Account"}
|
|
109
107
|
</button>
|
|
@@ -114,3 +112,16 @@ export function WelcomePage() {
|
|
|
114
112
|
</div>
|
|
115
113
|
)
|
|
116
114
|
}
|
|
115
|
+
|
|
116
|
+
function LoadingSpinner() {
|
|
117
|
+
return (
|
|
118
|
+
<svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
119
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
120
|
+
<path
|
|
121
|
+
className="opacity-75"
|
|
122
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
123
|
+
fill="currentColor"
|
|
124
|
+
/>
|
|
125
|
+
</svg>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
@@ -13,7 +13,7 @@ export const users = defineCollection({
|
|
|
13
13
|
plural: "Users",
|
|
14
14
|
},
|
|
15
15
|
fields: {
|
|
16
|
-
name: text({ required: true }),
|
|
16
|
+
name: text({ label: "Name", required: true }),
|
|
17
17
|
// Add custom user fields here — the auth adapter already contributes
|
|
18
18
|
// name/email/roles/etc. from Better Auth's schema.
|
|
19
19
|
},
|
|
@@ -5,14 +5,14 @@ import { TABLE_SLUG_ACCOUNTS, TABLE_SLUG_USERS } from "~/db/constants"
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
apikey,
|
|
8
|
+
footers,
|
|
9
|
+
headers,
|
|
8
10
|
images,
|
|
9
11
|
jwks,
|
|
10
|
-
session,
|
|
11
|
-
verification,
|
|
12
12
|
pages,
|
|
13
|
-
|
|
14
|
-
footers,
|
|
13
|
+
session,
|
|
15
14
|
themes,
|
|
15
|
+
verification,
|
|
16
16
|
} from "./vex.schema"
|
|
17
17
|
|
|
18
18
|
export default defineSchema({
|