r9stack 0.4.1
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/LICENSE +190 -0
- package/README.md +217 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +239 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/payload/assets/.gitkeep +0 -0
- package/dist/payload/assets/favicon.ico +0 -0
- package/dist/payload/assets/images/r9stack-logo-markonly-circle.png +0 -0
- package/dist/payload/assets/images/r9stack-logo-markonly-whitebg.png +0 -0
- package/dist/payload/assets/images/r9stack-logo-markonly.png +0 -0
- package/dist/payload/assets/images/r9stack-logo.png +0 -0
- package/dist/payload/assets/logo192.png +0 -0
- package/dist/payload/assets/logo512.png +0 -0
- package/dist/payload/assets/manifest.json +26 -0
- package/dist/payload/assets/robots.txt +3 -0
- package/dist/payload/templates/.gitkeep +0 -0
- package/dist/payload/templates/config/components.json +25 -0
- package/dist/payload/templates/config/env.example +14 -0
- package/dist/payload/templates/config/tsconfig.json +26 -0
- package/dist/payload/templates/config/vite.config.ts +23 -0
- package/dist/payload/templates/convex/auth.config.ts +7 -0
- package/dist/payload/templates/convex/messages.ts +28 -0
- package/dist/payload/templates/convex/schema.ts +24 -0
- package/dist/payload/templates/convex/tsconfig.json +21 -0
- package/dist/payload/templates/src/components/AppShell.tsx +21 -0
- package/dist/payload/templates/src/components/AuthProvider.tsx +50 -0
- package/dist/payload/templates/src/components/ConvexClientProvider.tsx +20 -0
- package/dist/payload/templates/src/components/NavGroup.tsx +46 -0
- package/dist/payload/templates/src/components/NavItem.tsx +36 -0
- package/dist/payload/templates/src/components/Sidebar.tsx +76 -0
- package/dist/payload/templates/src/components/UserMenu.tsx +102 -0
- package/dist/payload/templates/src/components/ui/button.tsx +59 -0
- package/dist/payload/templates/src/lib/auth-client.ts +29 -0
- package/dist/payload/templates/src/lib/auth-server.ts +97 -0
- package/dist/payload/templates/src/lib/auth.ts +15 -0
- package/dist/payload/templates/src/lib/utils.ts +7 -0
- package/dist/payload/templates/src/router.tsx +18 -0
- package/dist/payload/templates/src/routes/__root.tsx +53 -0
- package/dist/payload/templates/src/routes/app/demo/convex.messages.tsx +66 -0
- package/dist/payload/templates/src/routes/app/index.tsx +20 -0
- package/dist/payload/templates/src/routes/app/route.tsx +23 -0
- package/dist/payload/templates/src/routes/auth/callback.tsx +36 -0
- package/dist/payload/templates/src/routes/auth/sign-in.tsx +22 -0
- package/dist/payload/templates/src/routes/auth/sign-out.tsx +22 -0
- package/dist/payload/templates/src/routes/index.tsx +85 -0
- package/dist/payload/templates/src/styles.css +141 -0
- package/dist/utils/exec.d.ts +17 -0
- package/dist/utils/exec.d.ts.map +1 -0
- package/dist/utils/exec.js +49 -0
- package/dist/utils/exec.js.map +1 -0
- package/dist/utils/flight-rules.d.ts +5 -0
- package/dist/utils/flight-rules.d.ts.map +1 -0
- package/dist/utils/flight-rules.js +23 -0
- package/dist/utils/flight-rules.js.map +1 -0
- package/dist/utils/github.d.ts +17 -0
- package/dist/utils/github.d.ts.map +1 -0
- package/dist/utils/github.js +64 -0
- package/dist/utils/github.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +27 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/starters.d.ts +20 -0
- package/dist/utils/starters.d.ts.map +1 -0
- package/dist/utils/starters.js +43 -0
- package/dist/utils/starters.js.map +1 -0
- package/dist/utils/templates.d.ts +12 -0
- package/dist/utils/templates.d.ts.map +1 -0
- package/dist/utils/templates.js +77 -0
- package/dist/utils/templates.js.map +1 -0
- package/package.json +46 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { initCommand } from "./commands/init.js";
|
|
5
|
+
import { fetchStarters } from "./utils/starters.js";
|
|
6
|
+
import { logger } from "./utils/logger.js";
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program
|
|
9
|
+
.name("r9stack")
|
|
10
|
+
.description("CLI tool that scaffolds opinionated SaaS projects")
|
|
11
|
+
.version("0.4.0");
|
|
12
|
+
// Starter list option
|
|
13
|
+
program
|
|
14
|
+
.option("--starter-list", "List available starters")
|
|
15
|
+
.hook("preAction", async (thisCommand) => {
|
|
16
|
+
if (thisCommand.opts().starterList) {
|
|
17
|
+
await listStarters();
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
program
|
|
22
|
+
.command("init [project-name]")
|
|
23
|
+
.description("Scaffold a new r9stack project")
|
|
24
|
+
.option("-y, --yes", "Skip confirmation prompts")
|
|
25
|
+
.option("-s, --starter <id>", "Use a specific starter (e.g., 'standard')")
|
|
26
|
+
.option("--no-flight-rules", "Skip Flight Rules installation")
|
|
27
|
+
.option("--github", "Create GitHub repository")
|
|
28
|
+
.option("--no-github", "Skip GitHub repository creation")
|
|
29
|
+
.option("--private", "Make GitHub repository private (default)")
|
|
30
|
+
.option("--public", "Make GitHub repository public")
|
|
31
|
+
.action(initCommand);
|
|
32
|
+
// Make init the default command when no command is specified
|
|
33
|
+
program.action(async (options) => {
|
|
34
|
+
if (options.starterList) {
|
|
35
|
+
// Already handled by hook
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
await initCommand(undefined, {});
|
|
39
|
+
});
|
|
40
|
+
async function listStarters() {
|
|
41
|
+
logger.banner("r9stack - Available Starters");
|
|
42
|
+
try {
|
|
43
|
+
const starters = await fetchStarters();
|
|
44
|
+
if (starters.length === 0) {
|
|
45
|
+
logger.info("No starters available.");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
for (const starter of starters) {
|
|
49
|
+
console.log();
|
|
50
|
+
console.log(` ${pc.cyan(starter.name)} ${pc.dim(`v${starter.version}`)}`);
|
|
51
|
+
console.log(` ${pc.dim(starter.description)}`);
|
|
52
|
+
console.log(` ${pc.dim("ID:")} ${starter.id}`);
|
|
53
|
+
}
|
|
54
|
+
console.log();
|
|
55
|
+
logger.info("Create a project with:");
|
|
56
|
+
console.log(` ${pc.dim("$")} ${pc.cyan("npx r9stack init my-project")}`);
|
|
57
|
+
console.log();
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
logger.error("Failed to fetch starters. Please check your internet connection.");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
program.parse();
|
|
65
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"short_name": "{{PROJECT_NAME}}",
|
|
3
|
+
"name": "{{PROJECT_NAME}}",
|
|
4
|
+
"icons": [
|
|
5
|
+
{
|
|
6
|
+
"src": "favicon.ico",
|
|
7
|
+
"sizes": "64x64 32x32 24x24 16x16",
|
|
8
|
+
"type": "image/x-icon"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"src": "logo192.png",
|
|
12
|
+
"type": "image/png",
|
|
13
|
+
"sizes": "192x192"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"src": "logo512.png",
|
|
17
|
+
"type": "image/png",
|
|
18
|
+
"sizes": "512x512"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"start_url": ".",
|
|
22
|
+
"display": "standalone",
|
|
23
|
+
"theme_color": "#000000",
|
|
24
|
+
"background_color": "#ffffff"
|
|
25
|
+
}
|
|
26
|
+
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "radix-vega",
|
|
4
|
+
"rsc": false,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/styles.css",
|
|
9
|
+
"baseColor": "gray",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"iconLibrary": "lucide",
|
|
14
|
+
"aliases": {
|
|
15
|
+
"components": "@/components",
|
|
16
|
+
"utils": "@/lib/utils",
|
|
17
|
+
"ui": "@/components/ui",
|
|
18
|
+
"lib": "@/lib",
|
|
19
|
+
"hooks": "@/hooks"
|
|
20
|
+
},
|
|
21
|
+
"menuColor": "default",
|
|
22
|
+
"menuAccent": "subtle",
|
|
23
|
+
"registries": {}
|
|
24
|
+
}
|
|
25
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Convex
|
|
2
|
+
# This is auto-populated when you run `npx convex dev`
|
|
3
|
+
VITE_CONVEX_URL=
|
|
4
|
+
|
|
5
|
+
# WorkOS
|
|
6
|
+
# Get these from https://dashboard.workos.com
|
|
7
|
+
WORKOS_API_KEY=
|
|
8
|
+
WORKOS_CLIENT_ID=
|
|
9
|
+
WORKOS_REDIRECT_URI=http://localhost:3000/auth/callback
|
|
10
|
+
|
|
11
|
+
# Session
|
|
12
|
+
# Generate with: openssl rand -base64 32
|
|
13
|
+
WORKOS_COOKIE_PASSWORD=
|
|
14
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"include": ["**/*.ts", "**/*.tsx", "eslint.config.js", "prettier.config.js", "vite.config.js"],
|
|
3
|
+
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"target": "ES2022",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
9
|
+
"types": ["vite/client"],
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"verbatimModuleSyntax": false,
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"strict": true,
|
|
16
|
+
"noUnusedLocals": true,
|
|
17
|
+
"noUnusedParameters": true,
|
|
18
|
+
"noFallthroughCasesInSwitch": true,
|
|
19
|
+
"noUncheckedSideEffectImports": true,
|
|
20
|
+
"baseUrl": ".",
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./src/*"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import { devtools } from '@tanstack/devtools-vite'
|
|
3
|
+
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
|
4
|
+
import viteReact from '@vitejs/plugin-react'
|
|
5
|
+
import viteTsConfigPaths from 'vite-tsconfig-paths'
|
|
6
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
7
|
+
import { nitro } from 'nitro/vite'
|
|
8
|
+
|
|
9
|
+
const config = defineConfig({
|
|
10
|
+
plugins: [
|
|
11
|
+
devtools(),
|
|
12
|
+
nitro(),
|
|
13
|
+
viteTsConfigPaths({
|
|
14
|
+
projects: ['./tsconfig.json'],
|
|
15
|
+
}),
|
|
16
|
+
tailwindcss(),
|
|
17
|
+
tanstackStart(),
|
|
18
|
+
viteReact(),
|
|
19
|
+
],
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export default config
|
|
23
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { query, mutation } from "./_generated/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
|
|
4
|
+
export const list = query({
|
|
5
|
+
args: {},
|
|
6
|
+
handler: async (ctx) => {
|
|
7
|
+
const messages = await ctx.db
|
|
8
|
+
.query("messages")
|
|
9
|
+
.withIndex("by_created_at")
|
|
10
|
+
.order("desc")
|
|
11
|
+
.collect();
|
|
12
|
+
return messages;
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const send = mutation({
|
|
17
|
+
args: {
|
|
18
|
+
text: v.string(),
|
|
19
|
+
},
|
|
20
|
+
handler: async (ctx, args) => {
|
|
21
|
+
const messageId = await ctx.db.insert("messages", {
|
|
22
|
+
text: args.text,
|
|
23
|
+
createdAt: Date.now(),
|
|
24
|
+
});
|
|
25
|
+
return messageId;
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineSchema, defineTable } from 'convex/server'
|
|
2
|
+
import { v } from 'convex/values'
|
|
3
|
+
|
|
4
|
+
export default defineSchema({
|
|
5
|
+
// Demo messages table
|
|
6
|
+
messages: defineTable({
|
|
7
|
+
text: v.string(),
|
|
8
|
+
createdAt: v.number(),
|
|
9
|
+
}).index('by_created_at', ['createdAt']),
|
|
10
|
+
|
|
11
|
+
// Users table for storing WorkOS user information
|
|
12
|
+
users: defineTable({
|
|
13
|
+
workosId: v.string(),
|
|
14
|
+
email: v.string(),
|
|
15
|
+
firstName: v.optional(v.string()),
|
|
16
|
+
lastName: v.optional(v.string()),
|
|
17
|
+
profilePictureUrl: v.optional(v.string()),
|
|
18
|
+
createdAt: v.number(),
|
|
19
|
+
updatedAt: v.number(),
|
|
20
|
+
})
|
|
21
|
+
.index('by_workos_id', ['workosId'])
|
|
22
|
+
.index('by_email', ['email']),
|
|
23
|
+
})
|
|
24
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"lib": ["ES2021", "dom"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"incremental": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"module": "ESNext",
|
|
13
|
+
"moduleResolution": "Bundler",
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"jsx": "react-jsx"
|
|
17
|
+
},
|
|
18
|
+
"include": ["./**/*"],
|
|
19
|
+
"exclude": ["_generated"]
|
|
20
|
+
}
|
|
21
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useState, type ReactNode } from 'react'
|
|
2
|
+
import { Sidebar } from './Sidebar'
|
|
3
|
+
|
|
4
|
+
interface AppShellProps {
|
|
5
|
+
children: ReactNode
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function AppShell({ children }: AppShellProps) {
|
|
9
|
+
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="flex h-screen bg-background text-foreground">
|
|
13
|
+
<Sidebar
|
|
14
|
+
collapsed={sidebarCollapsed}
|
|
15
|
+
onToggle={() => setSidebarCollapsed(!sidebarCollapsed)}
|
|
16
|
+
/>
|
|
17
|
+
<main className="flex-1 overflow-auto">{children}</main>
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useState, useEffect, type ReactNode } from 'react'
|
|
2
|
+
import { AuthContext, signIn, signOut } from '../lib/auth-client'
|
|
3
|
+
import { getCurrentUser } from '../lib/auth-server'
|
|
4
|
+
import type { User } from '../lib/auth'
|
|
5
|
+
|
|
6
|
+
interface AuthProviderProps {
|
|
7
|
+
children: ReactNode
|
|
8
|
+
initialUser?: User | null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function AuthProvider({ children, initialUser = null }: AuthProviderProps) {
|
|
12
|
+
const [user, setUser] = useState<User | null>(initialUser)
|
|
13
|
+
const [isLoading, setIsLoading] = useState(initialUser === null)
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (initialUser !== null) {
|
|
17
|
+
setUser(initialUser)
|
|
18
|
+
setIsLoading(false)
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function fetchUser() {
|
|
23
|
+
try {
|
|
24
|
+
const currentUser = await getCurrentUser()
|
|
25
|
+
setUser(currentUser)
|
|
26
|
+
} catch {
|
|
27
|
+
setUser(null)
|
|
28
|
+
} finally {
|
|
29
|
+
setIsLoading(false)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fetchUser()
|
|
34
|
+
}, [initialUser])
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<AuthContext.Provider
|
|
38
|
+
value={{
|
|
39
|
+
user,
|
|
40
|
+
isAuthenticated: !!user,
|
|
41
|
+
isLoading,
|
|
42
|
+
signIn,
|
|
43
|
+
signOut,
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</AuthContext.Provider>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
|
2
|
+
import { ReactNode, useEffect, useState } from "react";
|
|
3
|
+
|
|
4
|
+
export function ConvexClientProvider({ children }: { children: ReactNode }) {
|
|
5
|
+
const [client, setClient] = useState<ConvexReactClient | null>(null);
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const convexUrl = import.meta.env.VITE_CONVEX_URL as string;
|
|
9
|
+
if (convexUrl) {
|
|
10
|
+
setClient(new ConvexReactClient(convexUrl));
|
|
11
|
+
}
|
|
12
|
+
}, []);
|
|
13
|
+
|
|
14
|
+
if (!client) {
|
|
15
|
+
return <>{children}</>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return <ConvexProvider client={client}>{children}</ConvexProvider>;
|
|
19
|
+
}
|
|
20
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useState, type ReactNode } from 'react'
|
|
2
|
+
import { ChevronDown, ChevronRight } from 'lucide-react'
|
|
3
|
+
import { cn } from '../lib/utils'
|
|
4
|
+
|
|
5
|
+
interface NavGroupProps {
|
|
6
|
+
label: string
|
|
7
|
+
children: ReactNode
|
|
8
|
+
collapsed?: boolean
|
|
9
|
+
defaultExpanded?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function NavGroup({
|
|
13
|
+
label,
|
|
14
|
+
children,
|
|
15
|
+
collapsed,
|
|
16
|
+
defaultExpanded = true,
|
|
17
|
+
}: NavGroupProps) {
|
|
18
|
+
const [expanded, setExpanded] = useState(defaultExpanded)
|
|
19
|
+
|
|
20
|
+
if (collapsed) {
|
|
21
|
+
return <div className="space-y-1">{children}</div>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="space-y-1">
|
|
26
|
+
<button
|
|
27
|
+
onClick={() => setExpanded(!expanded)}
|
|
28
|
+
className={cn(
|
|
29
|
+
'flex items-center justify-between w-full px-3 py-2',
|
|
30
|
+
'text-xs font-semibold uppercase tracking-wider',
|
|
31
|
+
'text-sidebar-foreground/50 hover:text-sidebar-foreground/70',
|
|
32
|
+
'transition-colors'
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
<span>{label}</span>
|
|
36
|
+
{expanded ? (
|
|
37
|
+
<ChevronDown className="h-3 w-3" />
|
|
38
|
+
) : (
|
|
39
|
+
<ChevronRight className="h-3 w-3" />
|
|
40
|
+
)}
|
|
41
|
+
</button>
|
|
42
|
+
{expanded && <div className="space-y-1">{children}</div>}
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Link } from '@tanstack/react-router'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { cn } from '../lib/utils'
|
|
4
|
+
|
|
5
|
+
interface NavItemProps {
|
|
6
|
+
to: string
|
|
7
|
+
icon: ReactNode
|
|
8
|
+
label: string
|
|
9
|
+
collapsed?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function NavItem({ to, icon, label, collapsed }: NavItemProps) {
|
|
13
|
+
return (
|
|
14
|
+
<Link
|
|
15
|
+
to={to}
|
|
16
|
+
className={cn(
|
|
17
|
+
'flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium',
|
|
18
|
+
'text-sidebar-foreground/70 hover:text-sidebar-foreground',
|
|
19
|
+
'hover:bg-sidebar-accent transition-colors',
|
|
20
|
+
collapsed && 'justify-center px-2'
|
|
21
|
+
)}
|
|
22
|
+
activeProps={{
|
|
23
|
+
className: cn(
|
|
24
|
+
'flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium',
|
|
25
|
+
'bg-sidebar-accent text-sidebar-accent-foreground',
|
|
26
|
+
'transition-colors',
|
|
27
|
+
collapsed && 'justify-center px-2'
|
|
28
|
+
),
|
|
29
|
+
}}
|
|
30
|
+
>
|
|
31
|
+
<span className="flex-shrink-0">{icon}</span>
|
|
32
|
+
{!collapsed && <span>{label}</span>}
|
|
33
|
+
</Link>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Home, MessageSquare, PanelLeftClose, PanelLeft } from 'lucide-react'
|
|
2
|
+
import { cn } from '../lib/utils'
|
|
3
|
+
import { NavGroup } from './NavGroup'
|
|
4
|
+
import { NavItem } from './NavItem'
|
|
5
|
+
import { UserMenu } from './UserMenu'
|
|
6
|
+
|
|
7
|
+
interface SidebarProps {
|
|
8
|
+
collapsed: boolean
|
|
9
|
+
onToggle: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
|
13
|
+
return (
|
|
14
|
+
<aside
|
|
15
|
+
className={cn(
|
|
16
|
+
'flex flex-col h-full bg-sidebar border-r border-sidebar-border',
|
|
17
|
+
'transition-all duration-200 ease-in-out',
|
|
18
|
+
collapsed ? 'w-14' : 'w-60'
|
|
19
|
+
)}
|
|
20
|
+
>
|
|
21
|
+
{/* Header with collapse toggle */}
|
|
22
|
+
<div
|
|
23
|
+
className={cn(
|
|
24
|
+
'flex items-center h-14 px-3 border-b border-sidebar-border',
|
|
25
|
+
collapsed ? 'justify-center' : 'justify-between'
|
|
26
|
+
)}
|
|
27
|
+
>
|
|
28
|
+
{!collapsed && (
|
|
29
|
+
<span className="text-lg font-semibold text-sidebar-foreground">
|
|
30
|
+
{{PROJECT_NAME}}
|
|
31
|
+
</span>
|
|
32
|
+
)}
|
|
33
|
+
<button
|
|
34
|
+
onClick={onToggle}
|
|
35
|
+
className={cn(
|
|
36
|
+
'p-2 rounded-md',
|
|
37
|
+
'text-sidebar-foreground/70 hover:text-sidebar-foreground',
|
|
38
|
+
'hover:bg-sidebar-accent transition-colors'
|
|
39
|
+
)}
|
|
40
|
+
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
41
|
+
>
|
|
42
|
+
{collapsed ? (
|
|
43
|
+
<PanelLeft className="w-4 h-4" />
|
|
44
|
+
) : (
|
|
45
|
+
<PanelLeftClose className="w-4 h-4" />
|
|
46
|
+
)}
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{/* Navigation */}
|
|
51
|
+
<nav className="flex-1 p-2 space-y-4 overflow-y-auto">
|
|
52
|
+
<NavItem
|
|
53
|
+
to="/app"
|
|
54
|
+
icon={<Home className="w-4 h-4" />}
|
|
55
|
+
label="Home"
|
|
56
|
+
collapsed={collapsed}
|
|
57
|
+
/>
|
|
58
|
+
|
|
59
|
+
<NavGroup label="Demos" collapsed={collapsed}>
|
|
60
|
+
<NavItem
|
|
61
|
+
to="/app/demo/convex/messages"
|
|
62
|
+
icon={<MessageSquare className="w-4 h-4" />}
|
|
63
|
+
label="Messages"
|
|
64
|
+
collapsed={collapsed}
|
|
65
|
+
/>
|
|
66
|
+
</NavGroup>
|
|
67
|
+
</nav>
|
|
68
|
+
|
|
69
|
+
{/* User menu at bottom */}
|
|
70
|
+
<div className="p-2 border-t border-sidebar-border">
|
|
71
|
+
<UserMenu collapsed={collapsed} />
|
|
72
|
+
</div>
|
|
73
|
+
</aside>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { ChevronUp, LogOut } from 'lucide-react'
|
|
2
|
+
import { useState, useRef, useEffect } from 'react'
|
|
3
|
+
import { cn } from '../lib/utils'
|
|
4
|
+
import { useAuth } from '../lib/auth-client'
|
|
5
|
+
|
|
6
|
+
interface UserMenuProps {
|
|
7
|
+
collapsed?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function UserMenu({ collapsed }: UserMenuProps) {
|
|
11
|
+
const { user, signOut } = useAuth()
|
|
12
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
13
|
+
const menuRef = useRef<HTMLDivElement>(null)
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
function handleClickOutside(event: MouseEvent) {
|
|
17
|
+
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
|
18
|
+
setIsOpen(false)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
22
|
+
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
23
|
+
}, [])
|
|
24
|
+
|
|
25
|
+
const displayName = user?.firstName || user?.email?.split('@')[0] || 'User'
|
|
26
|
+
const initials = user?.firstName?.[0]?.toUpperCase() || user?.email?.[0]?.toUpperCase() || 'U'
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="relative" ref={menuRef}>
|
|
30
|
+
<button
|
|
31
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
32
|
+
className={cn(
|
|
33
|
+
'flex items-center gap-3 w-full p-3 rounded-md',
|
|
34
|
+
'hover:bg-sidebar-accent transition-colors',
|
|
35
|
+
'text-sidebar-foreground',
|
|
36
|
+
collapsed && 'justify-center'
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-sidebar-primary flex items-center justify-center overflow-hidden">
|
|
40
|
+
{user?.profilePictureUrl ? (
|
|
41
|
+
<img
|
|
42
|
+
src={user.profilePictureUrl}
|
|
43
|
+
alt={displayName}
|
|
44
|
+
className="w-8 h-8 rounded-full object-cover"
|
|
45
|
+
/>
|
|
46
|
+
) : (
|
|
47
|
+
<span className="text-sm font-medium text-sidebar-primary-foreground">
|
|
48
|
+
{initials}
|
|
49
|
+
</span>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
{!collapsed && (
|
|
53
|
+
<>
|
|
54
|
+
<div className="flex-1 text-left min-w-0">
|
|
55
|
+
<p className="text-sm font-medium truncate">{displayName}</p>
|
|
56
|
+
<p className="text-xs text-sidebar-foreground/50 truncate">
|
|
57
|
+
{user?.email}
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
<ChevronUp
|
|
61
|
+
className={cn(
|
|
62
|
+
'w-4 h-4 text-sidebar-foreground/50 transition-transform',
|
|
63
|
+
isOpen && 'rotate-180'
|
|
64
|
+
)}
|
|
65
|
+
/>
|
|
66
|
+
</>
|
|
67
|
+
)}
|
|
68
|
+
</button>
|
|
69
|
+
|
|
70
|
+
{isOpen && (
|
|
71
|
+
<div
|
|
72
|
+
className={cn(
|
|
73
|
+
'absolute bottom-full mb-2 bg-popover border border-border rounded-lg shadow-lg py-1 min-w-[200px]',
|
|
74
|
+
collapsed ? 'left-full ml-2' : 'left-0 right-0'
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
<div className="px-3 py-2 border-b border-border">
|
|
78
|
+
<p className="text-sm font-medium text-foreground truncate">
|
|
79
|
+
{user?.firstName && user?.lastName
|
|
80
|
+
? `${user.firstName} ${user.lastName}`
|
|
81
|
+
: displayName}
|
|
82
|
+
</p>
|
|
83
|
+
<p className="text-xs text-muted-foreground truncate">
|
|
84
|
+
{user?.email}
|
|
85
|
+
</p>
|
|
86
|
+
</div>
|
|
87
|
+
<button
|
|
88
|
+
onClick={() => {
|
|
89
|
+
setIsOpen(false)
|
|
90
|
+
signOut()
|
|
91
|
+
}}
|
|
92
|
+
className="w-full px-3 py-2 text-left text-sm text-foreground hover:bg-accent flex items-center gap-2"
|
|
93
|
+
>
|
|
94
|
+
<LogOut className="w-4 h-4" />
|
|
95
|
+
Sign out
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|