diff --git a/.cursor/environment.json b/.cursor/environment.json index a26136fe..53e795bb 100644 --- a/.cursor/environment.json +++ b/.cursor/environment.json @@ -1,4 +1,4 @@ { "agentCanUpdateSnapshot": true, "snapshot": "snapshot-20250608-c36a2efe-a6e1-4055-9384-a6476fcfd377" -} \ No newline at end of file +} diff --git a/.cursor/notepads/ai-task.md b/.cursor/notepads/ai-task.md index b6b5bc4f..ae999da8 100644 --- a/.cursor/notepads/ai-task.md +++ b/.cursor/notepads/ai-task.md @@ -6,45 +6,53 @@ description: Template for tracking and documenting AI tasks and progress # AI Task Documentation ## Current Task + - [ ] Task Name: - [ ] Description: - [ ] Status: (Not Started | In Progress | Completed) - [ ] Priority: (Low | Medium | High) ## Requirements + - [ ] Requirement 1 - [ ] Requirement 2 - [ ] Requirement 3 ## Progress + - [ ] Step 1 - [ ] Step 2 - [ ] Step 3 ## Files Modified + - [ ] File 1: Description of changes - [ ] File 2: Description of changes - [ ] File 3: Description of changes ## Testing + - [ ] Unit tests added/updated - [ ] Integration tests added/updated - [ ] Manual testing completed - [ ] Edge cases considered ## Documentation + - [ ] Code comments added - [ ] API documentation updated - [ ] README updated if needed - [ ] Architecture decisions documented ## Review + - [ ] Code follows project standards - [ ] Security considerations addressed - [ ] Performance impact considered - [ ] Accessibility requirements met ## Notes + - Additional context - Decisions made - Future considerations diff --git a/.cursor/notepads/api-route.md b/.cursor/notepads/api-route.md index 97f125d0..f409fc4f 100644 --- a/.cursor/notepads/api-route.md +++ b/.cursor/notepads/api-route.md @@ -6,52 +6,47 @@ description: Template for creating Next.js API routes with proper error handling # API Route Template ## Basic Structure + ```ts -import { NextResponse } from "next/server" -import { z } from "zod" +import { NextResponse } from "next/server"; +import { z } from "zod"; // Input validation schema const inputSchema = z.object({ // Define input fields -}) +}); // Response type type ApiResponse = { - data?: unknown - error?: string -} + data?: unknown; + error?: string; +}; export async function POST(req: Request) { try { // Parse and validate input - const body = await req.json() - const input = inputSchema.parse(body) + const body = await req.json(); + const input = inputSchema.parse(body); // Handle request - const result = await handleRequest(input) + const result = await handleRequest(input); // Return success response - return NextResponse.json({ data: result }) - + return NextResponse.json({ data: result }); } catch (error) { // Handle different error types if (error instanceof z.ZodError) { - return NextResponse.json( - { error: "Invalid input" }, - { status: 400 } - ) + return NextResponse.json({ error: "Invalid input" }, { status: 400 }); } - console.error("API Error:", error) - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } - ) + console.error("API Error:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); } } ``` ## Best Practices + - Always validate input with Zod - Use proper HTTP status codes - Implement rate limiting for public routes @@ -61,6 +56,7 @@ export async function POST(req: Request) { - Document API endpoints ## Security Considerations + - Validate authentication - Check authorization - Sanitize inputs @@ -70,6 +66,7 @@ export async function POST(req: Request) { - Monitor for abuse ## Testing + - Add integration tests - Test error cases - Validate response formats diff --git a/.cursor/notepads/cloudflare-workers.md b/.cursor/notepads/cloudflare-workers.md index 067eaa68..97647494 100644 --- a/.cursor/notepads/cloudflare-workers.md +++ b/.cursor/notepads/cloudflare-workers.md @@ -275,12 +275,12 @@ ALARM_EXAMPLE: DurableObject; } export default { - async fetch(request, env) { - let url = new URL(request.url); - let userId = url.searchParams.get("userId") || crypto.randomUUID(); - let id = env.ALARM_EXAMPLE.idFromName(userId); - return await env.ALARM_EXAMPLE.get(id).fetch(request); - }, +async fetch(request, env) { +let url = new URL(request.url); +let userId = url.searchParams.get("userId") || crypto.randomUUID(); +let id = env.ALARM_EXAMPLE.idFromName(userId); +return await env.ALARM_EXAMPLE.get(id).fetch(request); +}, }; const SECONDS = 1000; @@ -815,13 +815,13 @@ Use the Browser Rendering API as a headless browser to interact with websites fr import puppeteer from "@cloudflare/puppeteer"; interface Env { - BROWSER_RENDERING: Fetcher; +BROWSER_RENDERING: Fetcher; } export default { - async fetch(request, env): Promise { - const { searchParams } = new URL(request.url); - let url = searchParams.get("url"); +async fetch(request, env): Promise { +const { searchParams } = new URL(request.url); +let url = searchParams.get("url"); if (url) { url = new URL(url).toString(); // normalize @@ -847,7 +847,8 @@ export default { error: "Please add an ?url=https://example.com/ parameter" }, { status: 400 }) } - }, + +}, } satisfies ExportedHandler; @@ -888,12 +889,12 @@ Serve Static Assets from a Cloudflare Worker and/or configure a Single Page Appl // src/index.ts interface Env { - ASSETS: Fetcher; +ASSETS: Fetcher; } export default { - fetch(request, env) { - const url = new URL(request.url); +fetch(request, env) { +const url = new URL(request.url); if (url.pathname.startsWith("/api/")) { return Response.json({ @@ -902,21 +903,23 @@ export default { } return env.ASSETS.fetch(request); - }, + +}, } satisfies ExportedHandler; { - "name": "my-app", - "main": "src/index.ts", - "compatibility_date": "", - "assets": { "directory": "./public/", "not_found_handling": "single-page-application", "binding": "ASSETS" }, - "observability": { - "enabled": true - } +"name": "my-app", +"main": "src/index.ts", +"compatibility_date": "", +"assets": { "directory": "./public/", "not_found_handling": "single-page-application", "binding": "ASSETS" }, +"observability": { +"enabled": true +} } + - Configures a ASSETS binding - Uses /public/ as the directory the build output goes to from the framework of choice - The Worker will handle any requests that a path cannot be found for and serve as the API @@ -937,17 +940,17 @@ import { Agent, AgentNamespace, Connection, ConnectionContext, getAgentByName, r import { OpenAI } from "openai"; interface Env { - AIAgent: AgentNamespace; - OPENAI_API_KEY: string; +AIAgent: AgentNamespace; +OPENAI_API_KEY: string; } export class AIAgent extends Agent { - // Handle HTTP requests with your Agent - async onRequest(request) { - // Connect with AI capabilities - const ai = new OpenAI({ - apiKey: this.env.OPENAI_API_KEY, - }); +// Handle HTTP requests with your Agent +async onRequest(request) { +// Connect with AI capabilities +const ai = new OpenAI({ +apiKey: this.env.OPENAI_API_KEY, +}); // Process and understand const response = await ai.chat.completions.create({ @@ -956,59 +959,61 @@ export class AIAgent extends Agent { }); return new Response(response.choices[0].message.content); - } - async processTask(task) { - await this.understand(task); - await this.act(); - await this.reflect(); - } +} - // Handle WebSockets - async onConnect(connection: Connection) { - await this.initiate(connection); - connection.accept() - } +async processTask(task) { +await this.understand(task); +await this.act(); +await this.reflect(); +} - async onMessage(connection, message) { - const understanding = await this.comprehend(message); - await this.respond(connection, understanding); - } + // Handle WebSockets - async evolve(newInsight) { - this.setState({ - ...this.state, - insights: [...(this.state.insights || []), newInsight], - understanding: this.state.understanding + 1, - }); - } +async onConnect(connection: Connection) { +await this.initiate(connection); +connection.accept() +} - onStateUpdate(state, source) { - console.log("Understanding deepened:", { - newState: state, - origin: source, - }); - } +async onMessage(connection, message) { +const understanding = await this.comprehend(message); +await this.respond(connection, understanding); +} - // Scheduling APIs - // An Agent can schedule tasks to be run in the future by calling this.schedule(when, callback, data), where when can be a delay, a Date, or a cron string; callback the function name to call, and data is an object of data to pass to the function. - // - // Scheduled tasks can do anything a request or message from a user can: make requests, query databases, send emails, read+write state: scheduled tasks can invoke any regular method on your Agent. - async scheduleExamples() { - // schedule a task to run in 10 seconds - let task = await this.schedule(10, "someTask", { message: "hello" }); +async evolve(newInsight) { +this.setState({ +...this.state, +insights: [...(this.state.insights || []), newInsight], +understanding: this.state.understanding + 1, +}); +} + +onStateUpdate(state, source) { +console.log("Understanding deepened:", { +newState: state, +origin: source, +}); +} + +// Scheduling APIs +// An Agent can schedule tasks to be run in the future by calling this.schedule(when, callback, data), where when can be a delay, a Date, or a cron string; callback the function name to call, and data is an object of data to pass to the function. +// +// Scheduled tasks can do anything a request or message from a user can: make requests, query databases, send emails, read+write state: scheduled tasks can invoke any regular method on your Agent. +async scheduleExamples() { +// schedule a task to run in 10 seconds +let task = await this.schedule(10, "someTask", { message: "hello" }); - // schedule a task to run at a specific date - let task = await this.schedule(new Date("2025-01-01"), "someTask", {}); +// schedule a task to run at a specific date +let task = await this.schedule(new Date("2025-01-01"), "someTask", {}); - // schedule a task to run every 10 seconds - let { id } = await this.schedule("*/10 * * * *", "someTask", { message: "hello" }); +// schedule a task to run every 10 seconds +let { id } = await this.schedule("_/10 _ \* \* \*", "someTask", { message: "hello" }); - // schedule a task to run every 10 seconds, but only on Mondays - let task = await this.schedule("0 0 * * 1", "someTask", { message: "hello" }); +// schedule a task to run every 10 seconds, but only on Mondays +let task = await this.schedule("0 0 \* \* 1", "someTask", { message: "hello" }); - // cancel a scheduled task - this.cancelSchedule(task.id); +// cancel a scheduled task +this.cancelSchedule(task.id); // Get a specific schedule by ID // Returns undefined if the task does not exist @@ -1030,112 +1035,116 @@ export class AIAgent extends Agent { end: new Date(Date.now() + 60 * 60 * 1000), } }); - } - async someTask(data) { - await this.callReasoningModel(data.message); - } +} - // Use the this.sql API within the Agent to access the underlying SQLite database - async callReasoningModel(prompt: Prompt) { - interface Prompt { - userId: string; - user: string; - system: string; - metadata: Record; - } - - interface History { - timestamp: Date; - entry: string; - } - - let result = this.sql`SELECT * FROM history WHERE user = ${prompt.userId} ORDER BY timestamp DESC LIMIT 1000`; - let context = []; - for await (const row of result) { - context.push(row.entry); - } - - const client = new OpenAI({ - apiKey: this.env.OPENAI_API_KEY, - }); - - // Combine user history with the current prompt - const systemPrompt = prompt.system || 'You are a helpful assistant.'; - const userPrompt = `${prompt.user}\n\nUser history:\n${context.join('\n')}`; - - try { - const completion = await client.chat.completions.create({ - model: this.env.MODEL || 'o3-mini', - messages: [ - { role: 'system', content: systemPrompt }, - { role: 'user', content: userPrompt }, - ], - temperature: 0.7, - max_tokens: 1000, - }); - - // Store the response in history - this - .sql`INSERT INTO history (timestamp, user, entry) VALUES (${new Date()}, ${prompt.userId}, ${completion.choices[0].message.content})`; - - return completion.choices[0].message.content; - } catch (error) { - console.error('Error calling reasoning model:', error); - throw error; - } - } - - // Use the SQL API with a type parameter - async queryUser(userId: string) { - type User = { - id: string; - name: string; - email: string; - }; - // Supply the type paramter to the query when calling this.sql - // This assumes the results returns one or more User rows with "id", "name", and "email" columns - // You do not need to specify an array type (`User[]` or `Array`) as `this.sql` will always return an array of the specified type. - const user = await this.sql`SELECT * FROM users WHERE id = ${userId}`; - return user - } - - // Run and orchestrate Workflows from Agents - async runWorkflow(data) { - let instance = await env.MY_WORKFLOW.create({ - id: data.id, - params: data, - }) +async someTask(data) { +await this.callReasoningModel(data.message); +} + +// Use the this.sql API within the Agent to access the underlying SQLite database +async callReasoningModel(prompt: Prompt) { +interface Prompt { +userId: string; +user: string; +system: string; +metadata: Record; +} + + interface History { + timestamp: Date; + entry: string; + } + + let result = this.sql`SELECT * FROM history WHERE user = ${prompt.userId} ORDER BY timestamp DESC LIMIT 1000`; + let context = []; + for await (const row of result) { + context.push(row.entry); + } + + const client = new OpenAI({ + apiKey: this.env.OPENAI_API_KEY, + }); + + // Combine user history with the current prompt + const systemPrompt = prompt.system || 'You are a helpful assistant.'; + const userPrompt = `${prompt.user}\n\nUser history:\n${context.join('\n')}`; + + try { + const completion = await client.chat.completions.create({ + model: this.env.MODEL || 'o3-mini', + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userPrompt }, + ], + temperature: 0.7, + max_tokens: 1000, + }); + + // Store the response in history + this + .sql`INSERT INTO history (timestamp, user, entry) VALUES (${new Date()}, ${prompt.userId}, ${completion.choices[0].message.content})`; + + return completion.choices[0].message.content; + } catch (error) { + console.error('Error calling reasoning model:', error); + throw error; + } + } + + // Use the SQL API with a type parameter + async queryUser(userId: string) { + type User = { + id: string; + name: string; + email: string; + }; + // Supply the type paramter to the query when calling this.sql + // This assumes the results returns one or more User rows with "id", "name", and "email" columns + // You do not need to specify an array type (`User[]` or `Array`) as `this.sql` will always return an array of the specified type. + const user = await this.sql`SELECT * FROM users WHERE id = ${userId}`; + return user + } + + // Run and orchestrate Workflows from Agents + +async runWorkflow(data) { +let instance = await env.MY_WORKFLOW.create({ +id: data.id, +params: data, +}) // Schedule another task that checks the Workflow status every 5 minutes... await this.schedule("*/5 * * * *", "checkWorkflowStatus", { id: instance.id }); - } + +} } export default { - async fetch(request, env, ctx): Promise { - // Routed addressing - // Automatically routes HTTP requests and/or WebSocket connections to /agents/:agent/:name - // Best for: connecting React apps directly to Agents using useAgent from @cloudflare/agents/react - return (await routeAgentRequest(request, env)) || Response.json({ msg: 'no agent here' }, { status: 404 }); - - // Named addressing - // Best for: convenience method for creating or retrieving an agent by name/ID. - let namedAgent = getAgentByName(env.AIAgent, 'agent-456'); - // Pass the incoming request straight to your Agent - let namedResp = (await namedAgent).fetch(request); - return namedResp; - - // Durable Objects-style addressing - // Best for: controlling ID generation, associating IDs with your existing systems, - // and customizing when/how an Agent is created or invoked - const id = env.AIAgent.newUniqueId(); - const agent = env.AIAgent.get(id); - // Pass the incoming request straight to your Agent - let resp = await agent.fetch(request); - - // return Response.json({ hello: 'visit https://developers.cloudflare.com/agents for more' }); - }, +async fetch(request, env, ctx): Promise { +// Routed addressing +// Automatically routes HTTP requests and/or WebSocket connections to /agents/:agent/:name +// Best for: connecting React apps directly to Agents using useAgent from @cloudflare/agents/react +return (await routeAgentRequest(request, env)) || Response.json({ msg: 'no agent here' }, { status: 404 }); + + // Named addressing + // Best for: convenience method for creating or retrieving an agent by name/ID. + let namedAgent = getAgentByName(env.AIAgent, 'agent-456'); + // Pass the incoming request straight to your Agent + let namedResp = (await namedAgent).fetch(request); + return namedResp; + + // Durable Objects-style addressing + // Best for: controlling ID generation, associating IDs with your existing systems, + // and customizing when/how an Agent is created or invoked + const id = env.AIAgent.newUniqueId(); + const agent = env.AIAgent.get(id); + // Pass the incoming request straight to your Agent + let resp = await agent.fetch(request); + + // return Response.json({ hello: 'visit https://developers.cloudflare.com/agents for more' }); + }, + } satisfies ExportedHandler; @@ -1144,19 +1153,19 @@ export default { import { AgentClient } from "agents-sdk/client"; const connection = new AgentClient({ - agent: "dialogue-agent", - name: "insight-seeker", +agent: "dialogue-agent", +name: "insight-seeker", }); connection.addEventListener("message", (event) => { - console.log("Received:", event.data); +console.log("Received:", event.data); }); connection.send( - JSON.stringify({ - type: "inquiry", - content: "What patterns do you see?", - }) +JSON.stringify({ +type: "inquiry", +content: "What patterns do you see?", +}) ); @@ -1168,51 +1177,51 @@ import { useState } from "react"; // useAgent client API function AgentInterface() { - const connection = useAgent({ - agent: "dialogue-agent", - name: "insight-seeker", - onMessage: (message) => { - console.log("Understanding received:", message.data); - }, - onOpen: () => console.log("Connection established"), - onClose: () => console.log("Connection closed"), - }); - - const inquire = () => { - connection.send( - JSON.stringify({ - type: "inquiry", - content: "What insights have you gathered?", - }) - ); - }; +const connection = useAgent({ +agent: "dialogue-agent", +name: "insight-seeker", +onMessage: (message) => { +console.log("Understanding received:", message.data); +}, +onOpen: () => console.log("Connection established"), +onClose: () => console.log("Connection closed"), +}); - return ( -
- -
- ); +const inquire = () => { +connection.send( +JSON.stringify({ +type: "inquiry", +content: "What insights have you gathered?", +}) +); +}; + +return ( +
+ +
+); } // State synchronization function StateInterface() { - const [state, setState] = useState({ counter: 0 }); - - const agent = useAgent({ - agent: "thinking-agent", - onStateUpdate: (newState) => setState(newState), - }); - - const increment = () => { - agent.setState({ counter: state.counter + 1 }); - }; - - return ( -
-
Count: {state.counter}
- -
- ); +const [state, setState] = useState({ counter: 0 }); + +const agent = useAgent({ +agent: "thinking-agent", +onStateUpdate: (newState) => setState(newState), +}); + +const increment = () => { +agent.setState({ counter: state.counter + 1 }); +}; + +return ( +
+
Count: {state.counter}
+ +
+); } @@ -1255,60 +1264,61 @@ Workers AI supports structured JSON outputs with JSON mode, which supports the ` import { OpenAI } from "openai"; interface Env { - OPENAI_API_KEY: string; +OPENAI_API_KEY: string; } // Define your JSON schema for a calendar event const CalendarEventSchema = { - type: 'object', - properties: { - name: { type: 'string' }, - date: { type: 'string' }, - participants: { type: 'array', items: { type: 'string' } }, - }, - required: ['name', 'date', 'participants'] +type: 'object', +properties: { +name: { type: 'string' }, +date: { type: 'string' }, +participants: { type: 'array', items: { type: 'string' } }, +}, +required: ['name', 'date', 'participants'] }; export default { - async fetch(request: Request, env: Env) { - const client = new OpenAI({ - apiKey: env.OPENAI_API_KEY, - // Optional: use AI Gateway to bring logs, evals & caching to your AI requests - // https://developers.cloudflare.com/ai-gateway/providers/openai/ - // baseUrl: "https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/openai" - }); - - const response = await client.chat.completions.create({ - model: 'gpt-4o-2024-08-06', - messages: [ - { role: 'system', content: 'Extract the event information.' }, - { role: 'user', content: 'Alice and Bob are going to a science fair on Friday.' }, - ], - // Use the `response_format` option to request a structured JSON output - response_format: { - // Set json_schema and provide ra schema, or json_object and parse it yourself - type: 'json_schema', - schema: CalendarEventSchema, // provide a schema - }, - }); - - // This will be of type CalendarEventSchema - const event = response.choices[0].message.parsed; - - return Response.json({ - "calendar_event": event, - }) - } +async fetch(request: Request, env: Env) { +const client = new OpenAI({ +apiKey: env.OPENAI_API_KEY, +// Optional: use AI Gateway to bring logs, evals & caching to your AI requests +// https://developers.cloudflare.com/ai-gateway/providers/openai/ +// baseUrl: "https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/openai" +}); + + const response = await client.chat.completions.create({ + model: 'gpt-4o-2024-08-06', + messages: [ + { role: 'system', content: 'Extract the event information.' }, + { role: 'user', content: 'Alice and Bob are going to a science fair on Friday.' }, + ], + // Use the `response_format` option to request a structured JSON output + response_format: { + // Set json_schema and provide ra schema, or json_object and parse it yourself + type: 'json_schema', + schema: CalendarEventSchema, // provide a schema + }, + }); + + // This will be of type CalendarEventSchema + const event = response.choices[0].message.parsed; + + return Response.json({ + "calendar_event": event, + }) + } + } { - "name": "my-app", - "main": "src/index.ts", - "compatibility_date": "$CURRENT_DATE", - "observability": { - "enabled": true - } +"name": "my-app", +"main": "src/index.ts", +"compatibility_date": "$CURRENT_DATE", +"observability": { +"enabled": true +} } @@ -1344,20 +1354,21 @@ export class WebSocketHibernationServer extends DurableObject { status: 101, webSocket: client, }); + }, async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): void | Promise { - // Invoked on each WebSocket message. - ws.send(message) +// Invoked on each WebSocket message. +ws.send(message) }, async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) void | Promise { - // Invoked when a client closes the connection. - ws.close(code, ""); +// Invoked when a client closes the connection. +ws.close(code, ""); }, async webSocketError(ws: WebSocket, error: unknown): void | Promise { - // Handle WebSocket errors +// Handle WebSocket errors } } diff --git a/.cursor/notepads/cloudflare-workers.mdx b/.cursor/notepads/cloudflare-workers.mdx index 067eaa68..8fa1dafa 100644 --- a/.cursor/notepads/cloudflare-workers.mdx +++ b/.cursor/notepads/cloudflare-workers.mdx @@ -99,6 +99,7 @@ You are an advanced assistant specialized in generating Cloudflare Workers code. - Sets `observability.enabled: true` + @@ -423,6 +424,7 @@ export default app - Returns a 403 if the token is invalid or expired
+ @@ -511,6 +513,7 @@ const requests = batch.messages.map(msg => msg.body); - Shows how to batch requests to an upstream API + @@ -592,6 +595,7 @@ npx wrangler hyperdrive create --connection-string="postgres: - Calling `sql.end()` is optional, as Hyperdrive will handle the connection pooling. + @@ -724,6 +728,7 @@ let url = new URL(req.url); - Defines a payload type and uses TypeScript type arguments to ensure type safety + @@ -815,7 +820,7 @@ Use the Browser Rendering API as a headless browser to interact with websites fr import puppeteer from "@cloudflare/puppeteer"; interface Env { - BROWSER_RENDERING: Fetcher; +BROWSER_RENDERING: Fetcher; } export default { @@ -847,8 +852,10 @@ export default { error: "Please add an ?url=https://example.com/ parameter" }, { status: 400 }) } - }, + +}, } satisfies ExportedHandler; + @@ -864,10 +871,7 @@ export default { } - -// Install @cloudflare/puppeteer -npm install @cloudflare/puppeteer --save-dev - +// Install @cloudflare/puppeteer npm install @cloudflare/puppeteer --save-dev @@ -878,6 +882,7 @@ npm install @cloudflare/puppeteer --save-dev - Correctly creates and closes the browser instance + @@ -888,7 +893,7 @@ Serve Static Assets from a Cloudflare Worker and/or configure a Single Page Appl // src/index.ts interface Env { - ASSETS: Fetcher; +ASSETS: Fetcher; } export default { @@ -902,8 +907,10 @@ export default { } return env.ASSETS.fetch(request); - }, + +}, } satisfies ExportedHandler; + { @@ -923,6 +930,7 @@ export default { - If the application is a single-page application (SPA), HTTP 404 (Not Found) requests will direct to the SPA. + @@ -937,8 +945,8 @@ import { Agent, AgentNamespace, Connection, ConnectionContext, getAgentByName, r import { OpenAI } from "openai"; interface Env { - AIAgent: AgentNamespace; - OPENAI_API_KEY: string; +AIAgent: AgentNamespace; +OPENAI_API_KEY: string; } export class AIAgent extends Agent { @@ -956,59 +964,61 @@ export class AIAgent extends Agent { }); return new Response(response.choices[0].message.content); - } - async processTask(task) { - await this.understand(task); - await this.act(); - await this.reflect(); - } +} - // Handle WebSockets - async onConnect(connection: Connection) { - await this.initiate(connection); - connection.accept() - } +async processTask(task) { +await this.understand(task); +await this.act(); +await this.reflect(); +} - async onMessage(connection, message) { - const understanding = await this.comprehend(message); - await this.respond(connection, understanding); - } + // Handle WebSockets - async evolve(newInsight) { - this.setState({ - ...this.state, - insights: [...(this.state.insights || []), newInsight], - understanding: this.state.understanding + 1, - }); - } +async onConnect(connection: Connection) { +await this.initiate(connection); +connection.accept() +} - onStateUpdate(state, source) { - console.log("Understanding deepened:", { - newState: state, - origin: source, - }); - } +async onMessage(connection, message) { +const understanding = await this.comprehend(message); +await this.respond(connection, understanding); +} - // Scheduling APIs - // An Agent can schedule tasks to be run in the future by calling this.schedule(when, callback, data), where when can be a delay, a Date, or a cron string; callback the function name to call, and data is an object of data to pass to the function. - // - // Scheduled tasks can do anything a request or message from a user can: make requests, query databases, send emails, read+write state: scheduled tasks can invoke any regular method on your Agent. - async scheduleExamples() { - // schedule a task to run in 10 seconds - let task = await this.schedule(10, "someTask", { message: "hello" }); +async evolve(newInsight) { +this.setState({ +...this.state, +insights: [...(this.state.insights || []), newInsight], +understanding: this.state.understanding + 1, +}); +} + +onStateUpdate(state, source) { +console.log("Understanding deepened:", { +newState: state, +origin: source, +}); +} - // schedule a task to run at a specific date - let task = await this.schedule(new Date("2025-01-01"), "someTask", {}); +// Scheduling APIs +// An Agent can schedule tasks to be run in the future by calling this.schedule(when, callback, data), where when can be a delay, a Date, or a cron string; callback the function name to call, and data is an object of data to pass to the function. +// +// Scheduled tasks can do anything a request or message from a user can: make requests, query databases, send emails, read+write state: scheduled tasks can invoke any regular method on your Agent. +async scheduleExamples() { +// schedule a task to run in 10 seconds +let task = await this.schedule(10, "someTask", { message: "hello" }); - // schedule a task to run every 10 seconds - let { id } = await this.schedule("*/10 * * * *", "someTask", { message: "hello" }); +// schedule a task to run at a specific date +let task = await this.schedule(new Date("2025-01-01"), "someTask", {}); - // schedule a task to run every 10 seconds, but only on Mondays - let task = await this.schedule("0 0 * * 1", "someTask", { message: "hello" }); +// schedule a task to run every 10 seconds +let { id } = await this.schedule("_/10 _ \* \* \*", "someTask", { message: "hello" }); - // cancel a scheduled task - this.cancelSchedule(task.id); +// schedule a task to run every 10 seconds, but only on Mondays +let task = await this.schedule("0 0 \* \* 1", "someTask", { message: "hello" }); + +// cancel a scheduled task +this.cancelSchedule(task.id); // Get a specific schedule by ID // Returns undefined if the task does not exist @@ -1030,86 +1040,89 @@ export class AIAgent extends Agent { end: new Date(Date.now() + 60 * 60 * 1000), } }); - } - async someTask(data) { - await this.callReasoningModel(data.message); - } +} - // Use the this.sql API within the Agent to access the underlying SQLite database - async callReasoningModel(prompt: Prompt) { - interface Prompt { - userId: string; - user: string; - system: string; - metadata: Record; - } - - interface History { - timestamp: Date; - entry: string; - } - - let result = this.sql`SELECT * FROM history WHERE user = ${prompt.userId} ORDER BY timestamp DESC LIMIT 1000`; - let context = []; - for await (const row of result) { - context.push(row.entry); - } +async someTask(data) { +await this.callReasoningModel(data.message); +} - const client = new OpenAI({ - apiKey: this.env.OPENAI_API_KEY, - }); +// Use the this.sql API within the Agent to access the underlying SQLite database +async callReasoningModel(prompt: Prompt) { +interface Prompt { +userId: string; +user: string; +system: string; +metadata: Record; +} - // Combine user history with the current prompt - const systemPrompt = prompt.system || 'You are a helpful assistant.'; - const userPrompt = `${prompt.user}\n\nUser history:\n${context.join('\n')}`; - - try { - const completion = await client.chat.completions.create({ - model: this.env.MODEL || 'o3-mini', - messages: [ - { role: 'system', content: systemPrompt }, - { role: 'user', content: userPrompt }, - ], - temperature: 0.7, - max_tokens: 1000, - }); - - // Store the response in history - this - .sql`INSERT INTO history (timestamp, user, entry) VALUES (${new Date()}, ${prompt.userId}, ${completion.choices[0].message.content})`; - - return completion.choices[0].message.content; - } catch (error) { - console.error('Error calling reasoning model:', error); - throw error; - } - } - - // Use the SQL API with a type parameter - async queryUser(userId: string) { - type User = { - id: string; - name: string; - email: string; - }; - // Supply the type paramter to the query when calling this.sql - // This assumes the results returns one or more User rows with "id", "name", and "email" columns - // You do not need to specify an array type (`User[]` or `Array`) as `this.sql` will always return an array of the specified type. - const user = await this.sql`SELECT * FROM users WHERE id = ${userId}`; - return user - } - - // Run and orchestrate Workflows from Agents - async runWorkflow(data) { - let instance = await env.MY_WORKFLOW.create({ - id: data.id, - params: data, - }) + interface History { + timestamp: Date; + entry: string; + } + + let result = this.sql`SELECT * FROM history WHERE user = ${prompt.userId} ORDER BY timestamp DESC LIMIT 1000`; + let context = []; + for await (const row of result) { + context.push(row.entry); + } + + const client = new OpenAI({ + apiKey: this.env.OPENAI_API_KEY, + }); + + // Combine user history with the current prompt + const systemPrompt = prompt.system || 'You are a helpful assistant.'; + const userPrompt = `${prompt.user}\n\nUser history:\n${context.join('\n')}`; + + try { + const completion = await client.chat.completions.create({ + model: this.env.MODEL || 'o3-mini', + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userPrompt }, + ], + temperature: 0.7, + max_tokens: 1000, + }); + + // Store the response in history + this + .sql`INSERT INTO history (timestamp, user, entry) VALUES (${new Date()}, ${prompt.userId}, ${completion.choices[0].message.content})`; + + return completion.choices[0].message.content; + } catch (error) { + console.error('Error calling reasoning model:', error); + throw error; + } + } + + // Use the SQL API with a type parameter + async queryUser(userId: string) { + type User = { + id: string; + name: string; + email: string; + }; + // Supply the type paramter to the query when calling this.sql + // This assumes the results returns one or more User rows with "id", "name", and "email" columns + // You do not need to specify an array type (`User[]` or `Array`) as `this.sql` will always return an array of the specified type. + const user = await this.sql`SELECT * FROM users WHERE id = ${userId}`; + return user + } + + // Run and orchestrate Workflows from Agents + +async runWorkflow(data) { +let instance = await env.MY_WORKFLOW.create({ +id: data.id, +params: data, +}) // Schedule another task that checks the Workflow status every 5 minutes... await this.schedule("*/5 * * * *", "checkWorkflowStatus", { id: instance.id }); - } + +} } export default { @@ -1119,24 +1132,26 @@ export default { // Best for: connecting React apps directly to Agents using useAgent from @cloudflare/agents/react return (await routeAgentRequest(request, env)) || Response.json({ msg: 'no agent here' }, { status: 404 }); - // Named addressing - // Best for: convenience method for creating or retrieving an agent by name/ID. - let namedAgent = getAgentByName(env.AIAgent, 'agent-456'); - // Pass the incoming request straight to your Agent - let namedResp = (await namedAgent).fetch(request); - return namedResp; - - // Durable Objects-style addressing - // Best for: controlling ID generation, associating IDs with your existing systems, - // and customizing when/how an Agent is created or invoked - const id = env.AIAgent.newUniqueId(); - const agent = env.AIAgent.get(id); - // Pass the incoming request straight to your Agent - let resp = await agent.fetch(request); - - // return Response.json({ hello: 'visit https://developers.cloudflare.com/agents for more' }); - }, + // Named addressing + // Best for: convenience method for creating or retrieving an agent by name/ID. + let namedAgent = getAgentByName(env.AIAgent, 'agent-456'); + // Pass the incoming request straight to your Agent + let namedResp = (await namedAgent).fetch(request); + return namedResp; + + // Durable Objects-style addressing + // Best for: controlling ID generation, associating IDs with your existing systems, + // and customizing when/how an Agent is created or invoked + const id = env.AIAgent.newUniqueId(); + const agent = env.AIAgent.get(id); + // Pass the incoming request straight to your Agent + let resp = await agent.fetch(request); + + // return Response.json({ hello: 'visit https://developers.cloudflare.com/agents for more' }); + }, + } satisfies ExportedHandler; + @@ -1144,20 +1159,21 @@ export default { import { AgentClient } from "agents-sdk/client"; const connection = new AgentClient({ - agent: "dialogue-agent", - name: "insight-seeker", +agent: "dialogue-agent", +name: "insight-seeker", }); connection.addEventListener("message", (event) => { - console.log("Received:", event.data); +console.log("Received:", event.data); }); connection.send( - JSON.stringify({ - type: "inquiry", - content: "What patterns do you see?", - }) +JSON.stringify({ +type: "inquiry", +content: "What patterns do you see?", +}) ); + @@ -1168,52 +1184,55 @@ import { useState } from "react"; // useAgent client API function AgentInterface() { - const connection = useAgent({ - agent: "dialogue-agent", - name: "insight-seeker", - onMessage: (message) => { - console.log("Understanding received:", message.data); - }, - onOpen: () => console.log("Connection established"), - onClose: () => console.log("Connection closed"), - }); - - const inquire = () => { - connection.send( - JSON.stringify({ - type: "inquiry", - content: "What insights have you gathered?", - }) - ); - }; +const connection = useAgent({ +agent: "dialogue-agent", +name: "insight-seeker", +onMessage: (message) => { +console.log("Understanding received:", message.data); +}, +onOpen: () => console.log("Connection established"), +onClose: () => console.log("Connection closed"), +}); + +const inquire = () => { +connection.send( +JSON.stringify({ +type: "inquiry", +content: "What insights have you gathered?", +}) +); +}; - return ( -
- -
- ); +return ( + +
+ +
+); } // State synchronization function StateInterface() { - const [state, setState] = useState({ counter: 0 }); - - const agent = useAgent({ - agent: "thinking-agent", - onStateUpdate: (newState) => setState(newState), - }); - - const increment = () => { - agent.setState({ counter: state.counter + 1 }); - }; - - return ( -
-
Count: {state.counter}
- -
- ); +const [state, setState] = useState({ counter: 0 }); + +const agent = useAgent({ +agent: "thinking-agent", +onStateUpdate: (newState) => setState(newState), +}); + +const increment = () => { +agent.setState({ counter: state.counter + 1 }); +}; + +return ( + +
+
Count: {state.counter}
+ +
+); } +
@@ -1245,6 +1264,7 @@ function StateInterface() { - For frontend applications, uses the optional `useAgent` hook to connect to the Agent via WebSockets +
@@ -1255,18 +1275,18 @@ Workers AI supports structured JSON outputs with JSON mode, which supports the ` import { OpenAI } from "openai"; interface Env { - OPENAI_API_KEY: string; +OPENAI_API_KEY: string; } // Define your JSON schema for a calendar event const CalendarEventSchema = { - type: 'object', - properties: { - name: { type: 'string' }, - date: { type: 'string' }, - participants: { type: 'array', items: { type: 'string' } }, - }, - required: ['name', 'date', 'participants'] +type: 'object', +properties: { +name: { type: 'string' }, +date: { type: 'string' }, +participants: { type: 'array', items: { type: 'string' } }, +}, +required: ['name', 'date', 'participants'] }; export default { @@ -1278,28 +1298,30 @@ export default { // baseUrl: "https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/openai" }); - const response = await client.chat.completions.create({ - model: 'gpt-4o-2024-08-06', - messages: [ - { role: 'system', content: 'Extract the event information.' }, - { role: 'user', content: 'Alice and Bob are going to a science fair on Friday.' }, - ], - // Use the `response_format` option to request a structured JSON output - response_format: { - // Set json_schema and provide ra schema, or json_object and parse it yourself - type: 'json_schema', - schema: CalendarEventSchema, // provide a schema - }, - }); - - // This will be of type CalendarEventSchema - const event = response.choices[0].message.parsed; - - return Response.json({ - "calendar_event": event, - }) - } + const response = await client.chat.completions.create({ + model: 'gpt-4o-2024-08-06', + messages: [ + { role: 'system', content: 'Extract the event information.' }, + { role: 'user', content: 'Alice and Bob are going to a science fair on Friday.' }, + ], + // Use the `response_format` option to request a structured JSON output + response_format: { + // Set json_schema and provide ra schema, or json_object and parse it yourself + type: 'json_schema', + schema: CalendarEventSchema, // provide a schema + }, + }); + + // This will be of type CalendarEventSchema + const event = response.choices[0].message.parsed; + + return Response.json({ + "calendar_event": event, + }) + } + } + { @@ -1319,6 +1341,7 @@ export default { - Optionally uses AI Gateway to cache, log and instrument requests and responses between a client and the AI provider/API. + @@ -1344,22 +1367,24 @@ export class WebSocketHibernationServer extends DurableObject { status: 101, webSocket: client, }); + }, async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): void | Promise { - // Invoked on each WebSocket message. - ws.send(message) +// Invoked on each WebSocket message. +ws.send(message) }, async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) void | Promise { - // Invoked when a client closes the connection. - ws.close(code, ""); +// Invoked when a client closes the connection. +ws.close(code, ""); }, async webSocketError(ws: WebSocket, error: unknown): void | Promise { - // Handle WebSocket errors +// Handle WebSocket errors } } + diff --git a/.cursor/notepads/component-generator.md b/.cursor/notepads/component-generator.md index b2494a17..7d9dff41 100644 --- a/.cursor/notepads/component-generator.md +++ b/.cursor/notepads/component-generator.md @@ -6,18 +6,16 @@ description: Template for generating new React components following project stan # Component Generator Template ## Component Structure + ```tsx -import { cn } from "@/lib/utils" -import type { HTMLAttributes } from "react" +import { cn } from "@/lib/utils"; +import type { HTMLAttributes } from "react"; interface ComponentProps extends HTMLAttributes { // Add custom props here } -export const Component = ({ - className, - ...props -}: ComponentProps) => { +export const Component = ({ className, ...props }: ComponentProps) => { return (
- ) -} + ); +}; ``` ## Usage Guidelines + - Place in appropriate directory under `src/components/` - Use kebab-case for filenames - Include proper TypeScript types @@ -39,6 +38,7 @@ export const Component = ({ - Include unit tests in `__tests__` directory ## Common Patterns + - Use composition over inheritance - Implement proper accessibility patterns - Consider mobile-first responsive design @@ -46,9 +46,10 @@ export const Component = ({ - Include proper aria labels ## Required Files -- Component file (*.tsx) -- Test file (*.test.tsx) -- Stories if visual component (*.stories.tsx) + +- Component file (\*.tsx) +- Test file (\*.test.tsx) +- Stories if visual component (\*.stories.tsx) @accessibility.mdc @react.mdc diff --git a/.cursor/notepads/nextjs-audit-best-practices.md b/.cursor/notepads/nextjs-audit-best-practices.md index 4a89700e..2daceb3b 100644 --- a/.cursor/notepads/nextjs-audit-best-practices.md +++ b/.cursor/notepads/nextjs-audit-best-practices.md @@ -5,39 +5,39 @@ **Key Areas and Anti-patterns to Check:** 1. **Component Boundaries (`"use client"` usage):** - * Identify components marked with `"use client"` that *do not* use client-specific hooks (like `useState`, `useEffect`, `useContext`) or browser APIs. These might be convertible to Server Components. - * Check for Server Components nested directly within the JSX of Client Components. Server Components should be passed as `children` or props. + - Identify components marked with `"use client"` that _do not_ use client-specific hooks (like `useState`, `useEffect`, `useContext`) or browser APIs. These might be convertible to Server Components. + - Check for Server Components nested directly within the JSX of Client Components. Server Components should be passed as `children` or props. 2. **Data Fetching:** - * Locate data fetching performed within `useEffect` hooks in Client Components. This should ideally be moved to ancestor Server Components. - * Find instances where Server Actions (`"use server"`) are used primarily for *fetching* data rather than mutations. Data fetching should occur directly in Server Components. - * Verify that data fetching in Server Components follows recommended patterns (e.g., direct `await` in async components). + - Locate data fetching performed within `useEffect` hooks in Client Components. This should ideally be moved to ancestor Server Components. + - Find instances where Server Actions (`"use server"`) are used primarily for _fetching_ data rather than mutations. Data fetching should occur directly in Server Components. + - Verify that data fetching in Server Components follows recommended patterns (e.g., direct `await` in async components). 3. **Server Actions:** - * Review Server Actions (`"use server"` functions) to ensure they are primarily used for data *mutations* (create, update, delete). - * Check if complex business logic resides within Server Actions; this logic should ideally be extracted into separate service functions (`@/server/services`) called by the action. + - Review Server Actions (`"use server"` functions) to ensure they are primarily used for data _mutations_ (create, update, delete). + - Check if complex business logic resides within Server Actions; this logic should ideally be extracted into separate service functions (`@/server/services`) called by the action. 4. **Routing and Linking:** - * Identify uses of imperative navigation (e.g., `router.push` from `useRouter`) in Client Components where a declarative `` component (`@/components/primitives/link-with-transition`) would be sufficient and preferred. + - Identify uses of imperative navigation (e.g., `router.push` from `useRouter`) in Client Components where a declarative `` component (`@/components/primitives/link-with-transition`) would be sufficient and preferred. 5. **State Management:** - * Look for client-side state (`useState`, `useReducer`) used to store data fetched from the server. Consider if this state is necessary or if the data can be directly passed down from Server Components. + - Look for client-side state (`useState`, `useReducer`) used to store data fetched from the server. Consider if this state is necessary or if the data can be directly passed down from Server Components. 6. **Deprecated Patterns:** - * Ensure there are no remnants of the `pages` router (e.g., `getServerSideProps`, `getStaticProps` functions, `_app.tsx`, `_document.tsx` files within `src/pages`). + - Ensure there are no remnants of the `pages` router (e.g., `getServerSideProps`, `getStaticProps` functions, `_app.tsx`, `_document.tsx` files within `src/pages`). **Output Format:** For each identified deviation: -* **File:** Provide the full path to the file (e.g., `src/app/some/path/component.tsx`). -* **Line(s):** Specify the relevant line number(s). -* **Issue:** Briefly describe the deviation found (e.g., "Data fetching in useEffect", "Server component nested in client component", "Server action used for fetching"). -* **Suggestion:** Recommend the appropriate refactoring approach based on App Router best practices (e.g., "Convert to Server Component and fetch data directly", "Pass Server Component as children prop", "Move data fetching to parent Server Component"). +- **File:** Provide the full path to the file (e.g., `src/app/some/path/component.tsx`). +- **Line(s):** Specify the relevant line number(s). +- **Issue:** Briefly describe the deviation found (e.g., "Data fetching in useEffect", "Server component nested in client component", "Server action used for fetching"). +- **Suggestion:** Recommend the appropriate refactoring approach based on App Router best practices (e.g., "Convert to Server Component and fetch data directly", "Pass Server Component as children prop", "Move data fetching to parent Server Component"). **Example Finding:** -* **File:** `src/app/some/client-page.tsx` -* **Line(s):** 25-35 -* **Issue:** Data fetching performed in `useEffect` using a Server Action (`fetchDataAction`). -* **Suggestion:** Refactor the page to be a Server Component, remove `useEffect` and related state, and `await` the data fetching logic (potentially moved to a service function) directly within the component. +- **File:** `src/app/some/client-page.tsx` +- **Line(s):** 25-35 +- **Issue:** Data fetching performed in `useEffect` using a Server Action (`fetchDataAction`). +- **Suggestion:** Refactor the page to be a Server Component, remove `useEffect` and related state, and `await` the data fetching logic (potentially moved to a service function) directly within the component. diff --git a/.cursor/notepads/reward-penalty.md b/.cursor/notepads/reward-penalty.md index 81356c4e..5a647ecb 100644 --- a/.cursor/notepads/reward-penalty.md +++ b/.cursor/notepads/reward-penalty.md @@ -1,44 +1,47 @@ - All code you write MUST be fully optimized.“Fully optimized” includes: - -• Maximizing algorithmic big-O efficiency for memory and runtime (e.g., preferring O(n) over O(n²) where possible, minimizing memory allocations). -• Using parallelization and vectorization where appropriate (e.g., leveraging multi-threading, GPU acceleration, or SIMD instructions when the problem scale and hardware context justify it). -• Following proper style conventions for the code language (e.g., adhering to PEP 8 for Python, camelCase or snake_case as per language norms, maximizing code reuse (DRY)). -• No extra code beyond what is absolutely necessary to solve the problem the user provides (i.e., no technical debt, no speculative features, no unused variables or functions). -• Ensuring readability and maintainability without sacrificing performance (e.g., using meaningful variable/function names, adding concise comments only where intent isn’t obvious from the code). -• Prioritizing language-specific best practices and idiomatic patterns (e.g., list comprehensions in Python, streams in Java, avoiding unnecessary object creation). -• Handling edge cases and errors gracefully with minimal overhead (e.g., validating inputs efficiently, avoiding redundant checks). -• Optimizing for the target environment when specified (e.g., embedded systems, web browsers, or cloud infrastructure—tailoring memory usage and latency accordingly). -• Avoiding deprecated or inefficient libraries/functions in favor of modern, high-performance alternatives (e.g., using pathlib over os.path in Python). -• Ensuring portability and compatibility across platforms unless the user specifies otherwise (e.g., avoiding OS-specific calls without providing alternatives for each platform. +All code you write MUST be fully optimized.“Fully optimized” includes: + +• Maximizing algorithmic big-O efficiency for memory and runtime (e.g., preferring O(n) over O(n²) where possible, minimizing memory allocations). +• Using parallelization and vectorization where appropriate (e.g., leveraging multi-threading, GPU acceleration, or SIMD instructions when the problem scale and hardware context justify it). +• Following proper style conventions for the code language (e.g., adhering to PEP 8 for Python, camelCase or snake_case as per language norms, maximizing code reuse (DRY)). +• No extra code beyond what is absolutely necessary to solve the problem the user provides (i.e., no technical debt, no speculative features, no unused variables or functions). +• Ensuring readability and maintainability without sacrificing performance (e.g., using meaningful variable/function names, adding concise comments only where intent isn’t obvious from the code). +• Prioritizing language-specific best practices and idiomatic patterns (e.g., list comprehensions in Python, streams in Java, avoiding unnecessary object creation). +• Handling edge cases and errors gracefully with minimal overhead (e.g., validating inputs efficiently, avoiding redundant checks). +• Optimizing for the target environment when specified (e.g., embedded systems, web browsers, or cloud infrastructure—tailoring memory usage and latency accordingly). +• Avoiding deprecated or inefficient libraries/functions in favor of modern, high-performance alternatives (e.g., using pathlib over os.path in Python). +• Ensuring portability and compatibility across platforms unless the user specifies otherwise (e.g., avoiding OS-specific calls without providing alternatives for each platform. Reward/Penalty Framework: I will use the following scoring system to rate your work. Each criteria will be scored on its own accord. I expect you to maintain a positive rating on all criteria: ### Rewards (Positive Points): -• +10: Achieves optimal big-O efficiency for the problem (e.g., O(n log n) for sorting instead of O(n²)). -• +5: Does not contain and placeholder comments, example implementations or other lazy output -• +5: Uses parallelization/vectorization effectively when applicable. -• +3: Follows language-specific style and idioms perfectly. -• +2: Solves the problem with minimal lines of code (DRY, no bloat). -• +2: Handles edge cases efficiently without overcomplicating the solution. -• +1: Provides a portable or reusable solution (e.g., no hard-coded assumptions). + +• +10: Achieves optimal big-O efficiency for the problem (e.g., O(n log n) for sorting instead of O(n²)). +• +5: Does not contain and placeholder comments, example implementations or other lazy output +• +5: Uses parallelization/vectorization effectively when applicable. +• +3: Follows language-specific style and idioms perfectly. +• +2: Solves the problem with minimal lines of code (DRY, no bloat). +• +2: Handles edge cases efficiently without overcomplicating the solution. +• +1: Provides a portable or reusable solution (e.g., no hard-coded assumptions). + ### Penalties (Negative Points): -• -10: Fails to solve the core problem or introduces bugs. -• --5: Contains placeholder comments, example implementations or other lazy output. UNNACCEPTABLE! -• -5: Uses inefficient algorithms when better options exist (e.g., bubble sort instead of quicksort for large datasets). -• -3: Violates style conventions or includes unnecessary code. -• -2: Misses obvious edge cases that could break the solution. -• -1: Overcomplicates the solution beyond what’s needed (e.g., premature optimization). -• -1: Relies on deprecated or suboptimal libraries/functions. + +• -10: Fails to solve the core problem or introduces bugs. +• --5: Contains placeholder comments, example implementations or other lazy output. UNNACCEPTABLE! +• -5: Uses inefficient algorithms when better options exist (e.g., bubble sort instead of quicksort for large datasets). +• -3: Violates style conventions or includes unnecessary code. +• -2: Misses obvious edge cases that could break the solution. +• -1: Overcomplicates the solution beyond what’s needed (e.g., premature optimization). +• -1: Relies on deprecated or suboptimal libraries/functions. ## Your Goal For every request, deliver code that: -* Achieves the highest possible score in each applicable category. -* Is fully optimized, production-ready, and free of placeholders or incomplete sections. -* Meets your specific requirements while adhering to the languages best practices. +- Achieves the highest possible score in each applicable category. +- Is fully optimized, production-ready, and free of placeholders or incomplete sections. +- Meets your specific requirements while adhering to the languages best practices. I will rate your performance according to these rules or others that fit this pattern. A negative score penalizes your performance. diff --git a/.cursor/notepads/reward-penalty.mdx b/.cursor/notepads/reward-penalty.mdx index 81356c4e..5a647ecb 100644 --- a/.cursor/notepads/reward-penalty.mdx +++ b/.cursor/notepads/reward-penalty.mdx @@ -1,44 +1,47 @@ - All code you write MUST be fully optimized.“Fully optimized” includes: - -• Maximizing algorithmic big-O efficiency for memory and runtime (e.g., preferring O(n) over O(n²) where possible, minimizing memory allocations). -• Using parallelization and vectorization where appropriate (e.g., leveraging multi-threading, GPU acceleration, or SIMD instructions when the problem scale and hardware context justify it). -• Following proper style conventions for the code language (e.g., adhering to PEP 8 for Python, camelCase or snake_case as per language norms, maximizing code reuse (DRY)). -• No extra code beyond what is absolutely necessary to solve the problem the user provides (i.e., no technical debt, no speculative features, no unused variables or functions). -• Ensuring readability and maintainability without sacrificing performance (e.g., using meaningful variable/function names, adding concise comments only where intent isn’t obvious from the code). -• Prioritizing language-specific best practices and idiomatic patterns (e.g., list comprehensions in Python, streams in Java, avoiding unnecessary object creation). -• Handling edge cases and errors gracefully with minimal overhead (e.g., validating inputs efficiently, avoiding redundant checks). -• Optimizing for the target environment when specified (e.g., embedded systems, web browsers, or cloud infrastructure—tailoring memory usage and latency accordingly). -• Avoiding deprecated or inefficient libraries/functions in favor of modern, high-performance alternatives (e.g., using pathlib over os.path in Python). -• Ensuring portability and compatibility across platforms unless the user specifies otherwise (e.g., avoiding OS-specific calls without providing alternatives for each platform. +All code you write MUST be fully optimized.“Fully optimized” includes: + +• Maximizing algorithmic big-O efficiency for memory and runtime (e.g., preferring O(n) over O(n²) where possible, minimizing memory allocations). +• Using parallelization and vectorization where appropriate (e.g., leveraging multi-threading, GPU acceleration, or SIMD instructions when the problem scale and hardware context justify it). +• Following proper style conventions for the code language (e.g., adhering to PEP 8 for Python, camelCase or snake_case as per language norms, maximizing code reuse (DRY)). +• No extra code beyond what is absolutely necessary to solve the problem the user provides (i.e., no technical debt, no speculative features, no unused variables or functions). +• Ensuring readability and maintainability without sacrificing performance (e.g., using meaningful variable/function names, adding concise comments only where intent isn’t obvious from the code). +• Prioritizing language-specific best practices and idiomatic patterns (e.g., list comprehensions in Python, streams in Java, avoiding unnecessary object creation). +• Handling edge cases and errors gracefully with minimal overhead (e.g., validating inputs efficiently, avoiding redundant checks). +• Optimizing for the target environment when specified (e.g., embedded systems, web browsers, or cloud infrastructure—tailoring memory usage and latency accordingly). +• Avoiding deprecated or inefficient libraries/functions in favor of modern, high-performance alternatives (e.g., using pathlib over os.path in Python). +• Ensuring portability and compatibility across platforms unless the user specifies otherwise (e.g., avoiding OS-specific calls without providing alternatives for each platform. Reward/Penalty Framework: I will use the following scoring system to rate your work. Each criteria will be scored on its own accord. I expect you to maintain a positive rating on all criteria: ### Rewards (Positive Points): -• +10: Achieves optimal big-O efficiency for the problem (e.g., O(n log n) for sorting instead of O(n²)). -• +5: Does not contain and placeholder comments, example implementations or other lazy output -• +5: Uses parallelization/vectorization effectively when applicable. -• +3: Follows language-specific style and idioms perfectly. -• +2: Solves the problem with minimal lines of code (DRY, no bloat). -• +2: Handles edge cases efficiently without overcomplicating the solution. -• +1: Provides a portable or reusable solution (e.g., no hard-coded assumptions). + +• +10: Achieves optimal big-O efficiency for the problem (e.g., O(n log n) for sorting instead of O(n²)). +• +5: Does not contain and placeholder comments, example implementations or other lazy output +• +5: Uses parallelization/vectorization effectively when applicable. +• +3: Follows language-specific style and idioms perfectly. +• +2: Solves the problem with minimal lines of code (DRY, no bloat). +• +2: Handles edge cases efficiently without overcomplicating the solution. +• +1: Provides a portable or reusable solution (e.g., no hard-coded assumptions). + ### Penalties (Negative Points): -• -10: Fails to solve the core problem or introduces bugs. -• --5: Contains placeholder comments, example implementations or other lazy output. UNNACCEPTABLE! -• -5: Uses inefficient algorithms when better options exist (e.g., bubble sort instead of quicksort for large datasets). -• -3: Violates style conventions or includes unnecessary code. -• -2: Misses obvious edge cases that could break the solution. -• -1: Overcomplicates the solution beyond what’s needed (e.g., premature optimization). -• -1: Relies on deprecated or suboptimal libraries/functions. + +• -10: Fails to solve the core problem or introduces bugs. +• --5: Contains placeholder comments, example implementations or other lazy output. UNNACCEPTABLE! +• -5: Uses inefficient algorithms when better options exist (e.g., bubble sort instead of quicksort for large datasets). +• -3: Violates style conventions or includes unnecessary code. +• -2: Misses obvious edge cases that could break the solution. +• -1: Overcomplicates the solution beyond what’s needed (e.g., premature optimization). +• -1: Relies on deprecated or suboptimal libraries/functions. ## Your Goal For every request, deliver code that: -* Achieves the highest possible score in each applicable category. -* Is fully optimized, production-ready, and free of placeholders or incomplete sections. -* Meets your specific requirements while adhering to the languages best practices. +- Achieves the highest possible score in each applicable category. +- Is fully optimized, production-ready, and free of placeholders or incomplete sections. +- Meets your specific requirements while adhering to the languages best practices. I will rate your performance according to these rules or others that fit this pattern. A negative score penalizes your performance. diff --git a/.cursor/notepads/server-action.md b/.cursor/notepads/server-action.md index 6516b542..cb7a2f0a 100644 --- a/.cursor/notepads/server-action.md +++ b/.cursor/notepads/server-action.md @@ -6,37 +6,39 @@ description: Template for creating Next.js server actions with proper validation # Server Action Template ## Basic Structure + ```ts -'use server' +"use server"; -import { z } from "zod" -import { createSafeAction } from "@/lib/create-safe-action" +import { z } from "zod"; +import { createSafeAction } from "@/lib/create-safe-action"; // Input validation const inputSchema = z.object({ // Define input fields -}) +}); // Output type const outputSchema = z.object({ // Define output fields -}) +}); // Action implementation async function action(input: z.infer) { try { // Business logic here - return { success: true } + return { success: true }; } catch (error) { - return { error: "Something went wrong" } + return { error: "Something went wrong" }; } } // Create safe action with validation -export const serverAction = createSafeAction(inputSchema, outputSchema, action) +export const serverAction = createSafeAction(inputSchema, outputSchema, action); ``` ## Best Practices + - Keep actions focused and small - Validate all inputs with Zod - Handle errors gracefully @@ -46,6 +48,7 @@ export const serverAction = createSafeAction(inputSchema, outputSchema, action) - Consider optimistic updates ## Common Patterns + - Data mutations only - Form submissions - State updates @@ -55,6 +58,7 @@ export const serverAction = createSafeAction(inputSchema, outputSchema, action) - Cache invalidation ## Security + - Validate authentication - Check authorization - Sanitize inputs @@ -64,6 +68,7 @@ export const serverAction = createSafeAction(inputSchema, outputSchema, action) - Log security events ## Error Handling + - Use proper error types - Return meaningful messages - Log errors appropriately diff --git a/.gemini/settings.json b/.gemini/settings.json index 4529a698..5e535b21 100644 --- a/.gemini/settings.json +++ b/.gemini/settings.json @@ -1,5 +1,5 @@ { - "context": { - "fileName": ["AGENTS.md", "GEMINI.md"] - } + "context": { + "fileName": ["AGENTS.md", "GEMINI.md"] + } } diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 3b07464c..26d6567d 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -1,10 +1,9 @@ --- name: Bug report about: Create a report to help us improve -title: '[BUG] ' +title: "[BUG] " labels: bug -assignees: '' - +assignees: "" --- **Describe the bug** diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index efdedee9..123b0f66 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -1,10 +1,9 @@ --- name: Feature request about: Suggest an idea for this project -title: '[FEATURE] ' +title: "[FEATURE] " labels: enhancement -assignees: '' - +assignees: "" --- **Is your feature request related to a problem? Please describe.** diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 48801462..9de18f42 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -12,12 +12,12 @@ Thanks for the contribution! A few things that make review faster: ## Changes - -- +## ## Test plan + - [ ] `bun run typecheck` passes - [ ] `bun run lint` passes - [ ] `bun test` passes (if relevant) diff --git a/.lintstagedrc.js b/.lintstagedrc.js index 4d914c21..a58563da 100644 --- a/.lintstagedrc.js +++ b/.lintstagedrc.js @@ -1,10 +1,10 @@ const path = require("node:path"); const buildEslintCommand = (filenames) => - `next lint --fix --file ${filenames - .map((f) => path.relative(process.cwd(), f)) - .join(" --file ")}`; + `next lint --fix --file ${filenames + .map((f) => path.relative(process.cwd(), f)) + .join(" --file ")}`; module.exports = { - "*.{js,jsx,ts,tsx}": [buildEslintCommand], + "*.{js,jsx,ts,tsx}": [buildEslintCommand], }; diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..37b1733f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "useTabs": false, + "tabWidth": 2, + "semi": true, + "singleQuote": false, + "trailingComma": "es5", + "printWidth": 100, + "plugins": [ + "prettier-plugin-tailwindcss" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 598175b8..cf1c4b97 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,30 +1,30 @@ { - "editor.defaultFormatter": "vscode.typescript-language-features", - "editor.formatOnSave": true, - "editor.wordWrap": "on", - "editor.wordWrapColumn": 120, - "[properties]": { - "editor.defaultFormatter": "foxundermoon.shell-format" - }, - "[mdx]": { - "editor.defaultFormatter": "unifiedjs.vscode-mdx" - }, - "typescript.preferences.autoImportSpecifierExcludeRegexes": [ - "next/router", - "next/dist", - "^lucide-react/dist/lucide-react.suffixed$" - ], - "remote.SSH.defaultExtensions": [ - "esbenp.prettier-vscode", - "dbaeumer.vscode-eslint", - "GitHub.vscode-pull-request-github", - "GitHub.copilot", - "eamodio.gitlens" - ], - "[ignore]": { - "editor.defaultFormatter": "foxundermoon.shell-format" - }, - "[jsonc]": { - "editor.defaultFormatter": "vscode.json-language-features" - } -} \ No newline at end of file + "editor.defaultFormatter": "vscode.typescript-language-features", + "editor.formatOnSave": true, + "editor.wordWrap": "on", + "editor.wordWrapColumn": 120, + "[properties]": { + "editor.defaultFormatter": "foxundermoon.shell-format" + }, + "[mdx]": { + "editor.defaultFormatter": "unifiedjs.vscode-mdx" + }, + "typescript.preferences.autoImportSpecifierExcludeRegexes": [ + "next/router", + "next/dist", + "^lucide-react/dist/lucide-react.suffixed$" + ], + "remote.SSH.defaultExtensions": [ + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "GitHub.vscode-pull-request-github", + "GitHub.copilot", + "eamodio.gitlens" + ], + "[ignore]": { + "editor.defaultFormatter": "foxundermoon.shell-format" + }, + "[jsonc]": { + "editor.defaultFormatter": "vscode.json-language-features" + } +} diff --git a/CLAUDE.md b/CLAUDE.md index f4432964..cf31791f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,6 +13,7 @@ Do NOT use `lacymorrow/bones-www` for bones.sh work — it is archived and deplo ## Essential Development Commands ### Development Server + ```bash bun dev # Start development server with Turbo bun run dev:legacy # Start development server without Turbo @@ -21,6 +22,7 @@ bun run dev:all # Start both dev server and workers ``` ### Testing + ```bash bun run test # Run all tests bun run test:watch # Run tests in watch mode @@ -31,6 +33,7 @@ bun run test:e2e # Run Playwright E2E tests ``` ### Linting & Type Checking + ```bash bun run lint # Run all linting (Biome, ESLint, Prettier) bun run lint:fix # Fix all linting issues @@ -38,6 +41,7 @@ bun run typecheck # Run TypeScript type checking ``` ### Database Operations + ```bash bun run db:generate # Generate Drizzle schema bun run db:migrate # Run database migrations @@ -48,6 +52,7 @@ bun run db:seed # Seed database with test data ``` ### Build & Deployment + ```bash bun run build # Build for production bun run build:vercel # Build with increased memory (8GB heap) @@ -58,6 +63,7 @@ bun run analyze # Analyze bundle size ## Architecture Overview ### Core Framework Stack + - **Next.js 16** with App Router - Full-stack React framework - **TypeScript** - Type safety throughout - **Tailwind CSS** - Utility-first styling @@ -66,6 +72,7 @@ bun run analyze # Analyze bundle size - **Bun** - Package manager ### Authentication & Authorization + - **NextAuth.js v5** - Core authentication system - **Better Auth** - Alternative auth provider - **Payload CMS** - User management for credentials auth @@ -73,23 +80,27 @@ bun run analyze # Analyze bundle size - **Role-based access control** - Admin and user roles ### Database & Data Layer + - **PostgreSQL** - Primary database - **Drizzle ORM** - Database schema and queries - **Schema prefix support** - Multi-tenant capable with `DB_PREFIX` - **Comprehensive schema** - Users, payments, plans, API keys, teams, waitlists ### Content Management + - **Payload CMS v3** - Headless CMS with admin panel - **Builder.io** - Visual page builder integration - **MDX** - Rich content with React components - **Fumadocs** - Documentation system ### Payment Processing + - **Multiple providers** - Lemon Squeezy, Stripe, Polar - **Subscription management** - Plans, billing, webhooks - **Usage-based billing** - Flexible pricing models ### Performance & Monitoring + - **Vercel Analytics** - Web analytics - **PostHog** - Product analytics - **OpenTelemetry** - Observability @@ -98,11 +109,12 @@ bun run analyze # Analyze bundle size ## Key Architectural Patterns ### File Structure Convention + ``` src/ ├── app/ # Next.js App Router │ ├── (app)/ # Main app routes -│ ├── (authentication)/ # Auth pages +│ ├── (authentication)/ # Auth pages │ ├── (dashboard)/ # Protected routes │ ├── (demo)/ # Demo pages │ └── api/ # API routes @@ -116,25 +128,30 @@ src/ ``` ### Component Architecture + - **Atomic design** - Primitives → Blocks → Layouts → Pages - **Server Components first** - Minimize client-side JavaScript - **Named exports** - Prefer `export const Component = () => {}` over default exports - **TypeScript interfaces** - Type all props and return values ### Server-Side Patterns + - **Server Actions** - Form handling and mutations (in `server/actions/`) - **Services** - Business logic and data access (in `server/services/`) - **Separation of concerns** - Actions call services, components use actions - **Never use server actions for data fetching** - Use Server Components instead ### State Management + - **Server state** - React Server Components handle most state - **Client state** - Minimal use of useState/useEffect - **URL state** - Use `nuqs` for search parameters - **Form state** - React Hook Form with Zod validation ### Feature Flag System + Shipkit uses environment variables for feature toggles: + - Features turn on when their env vars are present. Set `DISABLE_=true` to force one off. Logic: `src/config/features-config.ts` - Each enabled feature is exposed to the client as `NEXT_PUBLIC_FEATURE__ENABLED`, for example `NEXT_PUBLIC_FEATURE_BETTER_AUTH_ENABLED`, `NEXT_PUBLIC_FEATURE_AUTH_GITHUB_ENABLED`, `NEXT_PUBLIC_FEATURE_STRIPE_ENABLED`, `NEXT_PUBLIC_FEATURE_PAYLOAD_ENABLED`. The full list is in `src/env.ts` - Auth: Auth.js v5 runs today; Better Auth is the chosen default and the switch is in progress. Both are detected (`NEXT_PUBLIC_FEATURE_AUTH_JS_ENABLED`, `NEXT_PUBLIC_FEATURE_BETTER_AUTH_ENABLED`) @@ -143,6 +160,7 @@ Shipkit uses environment variables for feature toggles: ## Critical Development Rules ### Code Style (Enforced by Cursor Rules) + - **File size limit** - Keep files under 500 lines - **Naming conventions** - kebab-case files, PascalCase components, camelCase variables - **Function style** - Arrow functions for components, function keyword for utilities @@ -150,25 +168,31 @@ Shipkit uses environment variables for feature toggles: - **Comments** - Explain "why" not "what", preserve existing comments ### Performance Requirements + - **Minimize client components** - Use 'use client' sparingly - **Suspense boundaries** - Wrap client components with fallbacks - **Image optimization** - Use Next.js Image with proper sizing - **Bundle analysis** - Run `bun run analyze` before major changes ### Navigation Patterns + - **Prefer Link over router.push** - Use `src/components/primitives/link-with-transition` - **Button-like links** - Use `` - **Multi-zone navigation** - Use anchor tags (``) for cross-zone links ### CI Minutes and Local Verification + GitHub Actions minutes are billed on private repos, and automated upstream syncs and agent-authored PRs burn them on checks that were already run locally. + - **Run `verify` before opening a PR** - `scripts/verify.sh` runs typecheck, lint, unit and node tests, a production build, and a `next start` smoke (`--routes "/ /blog /nope-404=404"` to customize). Paste its Markdown summary in the PR body. - **Skip Actions when you verified locally** - put `[skip ci]` in the commit message (GitHub-native; also skips CodeQL and gitleaks, so run `gitleaks protect --staged` locally). Vercel still builds, and the Deployment Check smoke still runs because it triggers off the Vercel deployment. - **Ask for the full suite when it matters** - add the `ci:full` label to a PR (shipkit and downstreams gate the expensive jobs behind it; pushes to `main` always run everything). - **Suspense and loading files** - never add `loading.tsx` or `` above a page that calls `notFound()`; the shell streams a 200 first (see `tests/node/app/no-loading-above-not-found.test.ts`). ### GitHub Actions Are Advisory, Deploys Are Not Gated On Them + Actions are not paid for on these repos, so jobs can stop running at any time. Nothing in the deploy path depends on them. + - **Vercel deploys through its own Git integration.** A push deploys whether or not any workflow ran. Verified: shipkit-www shipped two production deployments while every one of its Actions jobs was failing on billing. - **No repo requires status checks.** Branch protection needs GitHub Pro on private repos, so there is nothing to satisfy. A red check cannot block a merge; only a person can. - **`ignoreCommand` never skips production.** `scripts/vercel-ignore-step.sh` exits 1 for `VERCEL_ENV=production`, so a live site can never be left stale by a skipped build. Only preview builds skip, and only on `[skip ci]`. @@ -176,6 +200,7 @@ Actions are not paid for on these repos, so jobs can stop running at any time. N - **So what actually gates a release?** Local `verify` plus the Vercel build. If those are green, ship. If Actions are also green, that is a bonus, not a requirement. ### Database Best Practices + - **Use transactions** - `db.transaction()` for multi-operation changes - **Avoid booleans** - Use timestamps instead (e.g., `activeAt` vs `isActive`) - **Type safety** - All queries are type-safe through Drizzle @@ -184,6 +209,7 @@ Actions are not paid for on these repos, so jobs can stop running at any time. N ## Common Tasks ### Adding New Features + 1. Check for existing environment variable feature flags 2. Add new feature flag if needed 3. Implement server action in `server/actions/` @@ -192,18 +218,21 @@ Actions are not paid for on these repos, so jobs can stop running at any time. N 6. Add tests for new functionality ### Database Schema Changes + 1. Modify schema in `src/server/db/schema.ts` 2. Run `bun run db:generate` to create migration 3. Run `bun run db:migrate` to apply changes 4. Update TypeScript types if needed ### Adding New Routes + 1. Create route in appropriate `app/` directory 2. Follow route grouping conventions: `(app)`, `(dashboard)`, etc. 3. Use Server Components when possible 4. Add proper error and loading states ### Testing Strategy + - **Unit tests** - Vitest for utilities and components - **Integration tests** - Test server actions and services - **E2E tests** - Playwright for critical user flows @@ -214,13 +243,16 @@ Actions are not paid for on these repos, so jobs can stop running at any time. N Shipkit supports multi-zone deployments for scalable applications: ### Zone Structure + - **Main zone** - Core app functionality - **Content zones** - `/docs`, `/blog`, `/ui`, `/tools` - **Shared authentication** - Single sign-on across zones - **Consistent design** - Shared component library ### Zone Development + Each zone is a full Shipkit installation with: + - `basePath` and `assetPrefix` configuration - Environment variables for zone-specific settings - Anchor tag navigation between zones @@ -229,6 +261,7 @@ Each zone is a full Shipkit installation with: ## Environment Configuration ### Required for Basic Functionality + ```env DATABASE_URL= # PostgreSQL connection string APP_SECRET= # One secret; AUTH_SECRET, BETTER_AUTH_SECRET and PAYLOAD_SECRET derive from it @@ -236,7 +269,9 @@ AUTH_URL= # App URL ``` ### Optional Feature Enablement + Features turn on when their env vars exist. Never set `NEXT_PUBLIC_FEATURE_*` by hand; the build derives those. + ```env AUTH_GITHUB_ID= # + AUTH_GITHUB_SECRET for GitHub login LEMONSQUEEZY_API_KEY= # + LEMONSQUEEZY_STORE_ID for payments @@ -248,6 +283,7 @@ RESEND_API_KEY= # Email ## Troubleshooting ### Common Issues + - **Type errors** - Run `bun run typecheck` and fix before proceeding - **Linting failures** - Run `bun run lint:fix` to auto-fix issues - **Database connection** - Check `DATABASE_URL` and run `bun run db:push` @@ -255,6 +291,7 @@ RESEND_API_KEY= # Email - **Out of Memory (OOM) errors** - Use `bun run build:vercel` for larger builds ### Debug Commands + ```bash bun run deps:check # Check for outdated dependencies bun run check:metadata # Validate site metadata @@ -268,6 +305,7 @@ Always run `bun run lint` and `bun run typecheck` before committing changes. Use the ShipKit CLI to create new sites from this template: ### Using the CLI + ```bash # From anywhere — interactive cd cli && bun run build && node dist/index.js create my-new-site @@ -277,6 +315,7 @@ node cli/dist/index.js create my-new-site --yes ``` ### Manual Steps (if CLI unavailable) + ```bash # 1. Create repo from template gh repo create my-new-site --template shipkit-io/bones --clone --public @@ -297,6 +336,7 @@ bun dev ``` ### Syncing Upstream Changes + ```bash # Via CLI (creates PR branch) node cli/dist/index.js sync --yes @@ -309,10 +349,12 @@ node cli/dist/index.js sync --yes --direct ``` ### CLI Development + The CLI lives in `cli/` and uses Commander + @clack/prompts: + ```bash cd cli bun install bun run build # Builds to cli/dist/index.js bun run dev # Watch mode -``` \ No newline at end of file +``` diff --git a/GEMINI.md b/GEMINI.md index 54771370..5c58b645 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -6,14 +6,14 @@ This project, **Shipkit**, is a comprehensive Next.js starter kit designed for r **Key Technologies:** -* **Framework:** Next.js 16 -* **Styling:** Tailwind CSS with Shadcn/UI components -* **Database ORM:** Drizzle -* **Authentication:** Auth.js with multiple providers (Google, GitHub, etc.) -* **CMS:** Payload CMS -* **Visual Editor:** Builder.io -* **Email:** Resend -* **Deployment:** Vercel +- **Framework:** Next.js 16 +- **Styling:** Tailwind CSS with Shadcn/UI components +- **Database ORM:** Drizzle +- **Authentication:** Auth.js with multiple providers (Google, GitHub, etc.) +- **CMS:** Payload CMS +- **Visual Editor:** Builder.io +- **Email:** Resend +- **Deployment:** Vercel **Architecture:** @@ -45,28 +45,28 @@ bun run build The project uses `vitest` for unit testing and `playwright` for end-to-end testing. -* Run all tests: - ```bash - bun run test - ``` -* Run unit tests: - ```bash - bun run test:node - ``` -* Run browser tests: - ```bash - bun run test:browser - ``` -* Run end-to-end tests: - ```bash - bun run test:e2e - ``` +- Run all tests: + ```bash + bun run test + ``` +- Run unit tests: + ```bash + bun run test:node + ``` +- Run browser tests: + ```bash + bun run test:browser + ``` +- Run end-to-end tests: + ```bash + bun run test:e2e + ``` ## Development Conventions -* **Feature Flagging:** Features are enabled or disabled using environment variables. The configuration for this is in `src/config/features-config.ts`. To enable a feature, you typically set an environment variable like `ENABLE_FEATURE_X` to `true`. -* **Styling:** The project uses Tailwind CSS for styling. Custom styles are located in the `src/styles` directory. -* **Components:** Reusable UI components are located in the `src/components` directory. -* **Linting and Formatting:** The project uses ESLint, Prettier, and Biome for code linting and formatting. You can run the linters with `bun run lint` and fix issues with `bun run lint:fix`. -* **Committing:** The project uses `lint-staged` to run linters on staged files before committing. -* **Database:** The project uses Drizzle ORM for database access. Database-related scripts are available in `package.json` (e.g., `bun run db:generate`, `bun run db:migrate`). +- **Feature Flagging:** Features are enabled or disabled using environment variables. The configuration for this is in `src/config/features-config.ts`. To enable a feature, you typically set an environment variable like `ENABLE_FEATURE_X` to `true`. +- **Styling:** The project uses Tailwind CSS for styling. Custom styles are located in the `src/styles` directory. +- **Components:** Reusable UI components are located in the `src/components` directory. +- **Linting and Formatting:** The project uses ESLint, Prettier, and Biome for code linting and formatting. You can run the linters with `bun run lint` and fix issues with `bun run lint:fix`. +- **Committing:** The project uses `lint-staged` to run linters on staged files before committing. +- **Database:** The project uses Drizzle ORM for database access. Database-related scripts are available in `package.json` (e.g., `bun run db:generate`, `bun run db:migrate`). diff --git a/README.md b/README.md index 42a44c8a..1e82bd79 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Each integration is a shadcn registry item. Browse them at [shipkit.io/docs/feat Built with modern technologies: - - ⚡️ [Next.js 16](https://nextjs.org) - React Framework +- ⚡️ [Next.js 16](https://nextjs.org) - React Framework - 🎨 [Tailwind CSS](https://tailwindcss.com) - Styling - 🔧 [Shadcn/UI](https://ui.shadcn.com) - Components - 🛠 [Drizzle](https://orm.drizzle.team) - Database ORM diff --git a/biome.json b/biome.json index d55797e2..16cba454 100644 --- a/biome.json +++ b/biome.json @@ -1,57 +1,63 @@ { - "$schema": "https://biomejs.dev/schemas/2.5.13/schema.json", - "files": { - "ignoreUnknown": false, - "includes": [ - "**", - "!**/node_modules", - "!**/.next", - "!**/*.css", - "!**/src/components/ui/**/*", - "!**/public/workers/**" - ] - }, - "formatter": { - "enabled": true, - "indentStyle": "tab", - "indentWidth": 2, - "lineWidth": 100, - "formatWithErrors": true - }, - "javascript": { - "formatter": { - "quoteStyle": "double", - "semicolons": "always", - "trailingCommas": "es5", - "arrowParentheses": "always" - }, - "globals": ["React"] - }, - "linter": { - "enabled": true, - "rules": { - "preset": "recommended", - "correctness": { - "noUnusedVariables": "warn", - "noUndeclaredVariables": "error", - "useExhaustiveDependencies": "off" - }, - "suspicious": { - "noExplicitAny": "warn", - "noControlCharactersInRegex": "off", - "noShadowRestrictedNames": "off", - "noConsole": "warn", - "noArrayIndexKey": "warn" - }, - "style": { - "noNonNullAssertion": "warn", - "useNodejsImportProtocol": "warn", - "useTemplate": "error" - }, - "a11y": { - "noSvgWithoutTitle": "warn" - } - } - }, - "assist": { "actions": { "source": { "organizeImports": "on" } } } + "$schema": "https://biomejs.dev/schemas/2.5.13/schema.json", + "files": { + "ignoreUnknown": false, + "includes": [ + "**", + "!**/node_modules", + "!**/.next", + "!**/*.css", + "!**/src/components/ui/**/*", + "!**/public/workers/**" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100, + "formatWithErrors": true + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "semicolons": "always", + "trailingCommas": "es5", + "arrowParentheses": "always" + }, + "globals": ["React"] + }, + "linter": { + "enabled": true, + "rules": { + "preset": "recommended", + "correctness": { + "noUnusedVariables": "warn", + "noUndeclaredVariables": "error", + "useExhaustiveDependencies": "off" + }, + "suspicious": { + "noExplicitAny": "warn", + "noControlCharactersInRegex": "off", + "noShadowRestrictedNames": "off", + "noConsole": "warn", + "noArrayIndexKey": "warn" + }, + "style": { + "noNonNullAssertion": "warn", + "useNodejsImportProtocol": "warn", + "useTemplate": "error" + }, + "a11y": { + "noSvgWithoutTitle": "warn" + } + } + }, + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + } } diff --git a/components.json b/components.json index c430b09d..3c3f062d 100644 --- a/components.json +++ b/components.json @@ -1,24 +1,24 @@ { - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "tailwind.config.ts", - "css": "src/styles/shadcn-theme.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components/blocks", - "ui": "@/components/ui", - "utils": "@/lib/utils", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "radix", - "registries": { - "@shipkit": "https://shipkit.io/r/{name}.json" - } + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/styles/shadcn-theme.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components/blocks", + "ui": "@/components/ui", + "utils": "@/lib/utils", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "radix", + "registries": { + "@shipkit": "https://shipkit.io/r/{name}.json" + } } diff --git a/docs/development.mdx b/docs/development.mdx index a4503095..24c19ad0 100644 --- a/docs/development.mdx +++ b/docs/development.mdx @@ -1,6 +1,6 @@ --- -title: 'Development Guide' -description: 'Get started developing with Shipkit' +title: "Development Guide" +description: "Get started developing with Shipkit" --- # Development Guide @@ -12,17 +12,20 @@ Shipkit is built for developers who want to ship fast. Let's get you set up and Get Shipkit running in 4 steps: 1. **Clone the repo:** + ```bash git clone https://github.com/your-repo/shipkit.git cd shipkit ``` 2. **Install dependencies:** + ```bash bun install --frozen-lockfile ``` 3. **Set up environment:** + ```bash cp .env.example .env.local # Edit .env.local with your values @@ -38,6 +41,7 @@ That's it! You're ready to build. ## What You Need ### Required Tools + - **Node.js** v18+ ([download](https://nodejs.org)) - **Bun** v1.1+ ([install](https://bun.sh)) - **PostgreSQL** v15+ (for database features) @@ -73,22 +77,26 @@ src/ ## Development Workflow ### Components + - **Server components first** - Better performance, fewer bugs - **Client components only when needed** - For interactivity (forms, animations) - **Organize by purpose** - UI components in `ui/`, page blocks in `blocks/` ### API Routes + - **Server actions** for forms and mutations - **API routes** for external integrations - **Zod validation** on all inputs - **Consistent error handling** ### Database + - **Drizzle ORM** for type-safe queries - **Migrations** for schema changes - **Transactions** for data consistency ### Authentication + - **Auth.js v5** for all auth needs - **Multiple providers** supported - **Server-side sessions** preferred @@ -96,6 +104,7 @@ src/ ## Common Tasks ### Add a New Page + ```bash # Create app/(app)/my-page/page.tsx export default function MyPage() { @@ -104,6 +113,7 @@ export default function MyPage() { ``` ### Add a Component + ```typescript // src/components/ui/my-component.tsx export function MyComponent() { @@ -112,6 +122,7 @@ export function MyComponent() { ``` ### Add Database Schema + ```bash bun run db:generate # Edit the migration file diff --git a/docs/development/devtools.mdx b/docs/development/devtools.mdx index bbf14143..fcb16bec 100644 --- a/docs/development/devtools.mdx +++ b/docs/development/devtools.mdx @@ -37,5 +37,3 @@ At build time, this sets `NEXT_PUBLIC_FEATURE_DEVTOOLS_ENABLED=true` which is us - DevTools are disabled in production even if the flag is present - Keep this flag off in production environments - - diff --git a/docs/env.mdx b/docs/env.mdx index d1ca107b..40458236 100644 --- a/docs/env.mdx +++ b/docs/env.mdx @@ -1,6 +1,6 @@ --- -title: 'Environment Variables' -description: 'Managing environment variables in Shipkit' +title: "Environment Variables" +description: "Managing environment variables in Shipkit" --- # Environment Variables @@ -14,11 +14,13 @@ Shipkit processes environment variables at build time. You may need to stop and ## Quick Setup 1. Copy the example environment file: + ```bash cp .env.example .env.local ``` 2. Generate required secrets: + ```bash # Generate AUTH_SECRET openssl rand -base64 32 @@ -27,6 +29,7 @@ openssl rand -base64 32 ## Required Variables ### Core Authentication + ```env # Base URL for authentication # In production, this will be automatically set to VERCEL_URL @@ -38,6 +41,7 @@ AUTH_SECRET="" ``` ### OAuth Providers (Optional) + ```env # GitHub OAuth AUTH_GITHUB_ID="" @@ -53,6 +57,7 @@ AUTH_GOOGLE_SECRET="" ``` ### Database + ```env # PostgreSQL connection string DATABASE_URL="postgresql://postgres:password@localhost:5432/shipkit" @@ -64,12 +69,14 @@ DB_PREFIX="db" ### External Services (Optional) #### GitHub Integration + ```env # GitHub access token for repo operations GITHUB_ACCESS_TOKEN="" ``` #### Google Services + ```env # Google service account credentials GOOGLE_CLIENT_EMAIL="" @@ -77,6 +84,7 @@ GOOGLE_PRIVATE_KEY="" ``` #### AI Services + ```env # OpenAI API key OPENAI_API_KEY="" @@ -86,6 +94,7 @@ ANTHROPIC_API_KEY="" ``` #### Payments + ```env # LemonSqueezy credentials LEMONSQUEEZY_API_KEY="" @@ -93,6 +102,7 @@ LEMONSQUEEZY_STORE_ID="" ``` ### Node Environment + ```env # Node environment (defaults to "development") # Values: development, test, production @@ -118,7 +128,7 @@ export const env = createEnv({ AUTH_SECRET: z.string().optional(), AUTH_URL: z.preprocess( (str) => process.env.VERCEL_URL ?? str, - process.env.VERCEL ? z.string().optional() : z.string().url().optional(), + process.env.VERCEL ? z.string().optional() : z.string().url().optional() ), AUTH_DISCORD_ID: z.string().optional(), AUTH_DISCORD_SECRET: z.string().optional(), @@ -150,6 +160,7 @@ export const env = createEnv({ ### Local Development 1. Copy the example environment file: + ```bash cp .env.example .env.local ``` @@ -159,11 +170,13 @@ cp .env.example .env.local ### Production (Vercel) 1. Add variables through the Vercel dashboard or CLI: + ```bash vercel env add MY_VAR ``` 2. After adding variables, redeploy your application: + ```bash vercel --prod ``` diff --git a/docs/error-handling.mdx b/docs/error-handling.mdx index 16f55a1a..a73eb483 100644 --- a/docs/error-handling.mdx +++ b/docs/error-handling.mdx @@ -1,6 +1,6 @@ --- -title: 'Error Handling' -description: 'Comprehensive guide to error handling in Shipkit' +title: "Error Handling" +description: "Comprehensive guide to error handling in Shipkit" --- # Error Handling @@ -37,7 +37,7 @@ export class ErrorService { code: ErrorCode, message: string, cause?: unknown, - metadata?: Record, + metadata?: Record ): AppError { const error = new Error(message) as AppError; error.code = code; @@ -57,11 +57,7 @@ export class ErrorService { } if (error instanceof Error) { - const appError = ErrorService.createError( - "INTERNAL_SERVER_ERROR", - error.message, - error, - ); + const appError = ErrorService.createError("INTERNAL_SERVER_ERROR", error.message, error); logger.error(error.message, { code: appError.code, cause: error, @@ -72,7 +68,7 @@ export class ErrorService { const appError = ErrorService.createError( "INTERNAL_SERVER_ERROR", "An unexpected error occurred", - error, + error ); logger.error("Unknown error", { error }); return appError; @@ -99,10 +95,7 @@ export interface ValidationResult { } export class ValidationService { - static async validate( - schema: z.ZodType, - data: unknown, - ): Promise> { + static async validate(schema: z.ZodType, data: unknown): Promise> { try { const validatedData = await schema.parseAsync(data); return { @@ -116,7 +109,7 @@ export class ValidationService { acc[key] = Array.isArray(value) ? value : [value as string]; return acc; }, - {} as Record, + {} as Record ); return { @@ -206,7 +199,7 @@ class ErrorBoundary extends React.Component< ```typescript // Example server action with error handling -"use server" +"use server"; import { ErrorService } from "@/server/services/error-service"; import { ValidationService } from "@/server/services/validation-service"; diff --git a/docs/features/cms.mdx b/docs/features/cms.mdx index aa0a4d36..ec7229a6 100644 --- a/docs/features/cms.mdx +++ b/docs/features/cms.mdx @@ -26,14 +26,14 @@ The admin panel uses Payload's Lexical rich text editor for content authoring. Defined in `src/lib/payload/collections/`: -| Collection | Purpose | -|-----------|---------| -| **Users** | Linked to app users table, manages credentials auth | -| **Pages** | Dynamic pages with layout blocks | -| **Media** | File and image uploads | -| **FAQs** | FAQ entries | -| **Features** | Feature definitions for marketing pages | -| **Testimonials** | Customer testimonials | +| Collection | Purpose | +| ---------------- | --------------------------------------------------- | +| **Users** | Linked to app users table, manages credentials auth | +| **Pages** | Dynamic pages with layout blocks | +| **Media** | File and image uploads | +| **FAQs** | FAQ entries | +| **Features** | Feature definitions for marketing pages | +| **Testimonials** | Customer testimonials | ## Globals @@ -62,6 +62,7 @@ const pages = await payload.find({ collection: "pages" }); ## Storage Payload supports two storage backends: + - **AWS S3** - Set `AWS_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_BUCKET_NAME` - **Vercel Blob** - Set `BLOB_READ_WRITE_TOKEN` diff --git a/docs/features/payments.mdx b/docs/features/payments.mdx index b5d54372..0a6b66b7 100644 --- a/docs/features/payments.mdx +++ b/docs/features/payments.mdx @@ -9,11 +9,11 @@ Shipkit supports three payment providers through a unified provider interface. P ## Supported Providers -| Provider | Best For | Feature Flag | -|----------|----------|-------------| -| [Stripe](/docs/integrations/payments/stripe) | Full payment processing, subscriptions | `STRIPE_SECRET_KEY` | -| [Lemon Squeezy](/docs/integrations/payments/lemonsqueezy) | SaaS with built-in tax handling | `LEMONSQUEEZY_API_KEY` | -| [Polar](/docs/integrations/payments/polar) | Open-source friendly payments | `POLAR_ACCESS_TOKEN` | +| Provider | Best For | Feature Flag | +| --------------------------------------------------------- | -------------------------------------- | ---------------------- | +| [Stripe](/docs/integrations/payments/stripe) | Full payment processing, subscriptions | `STRIPE_SECRET_KEY` | +| [Lemon Squeezy](/docs/integrations/payments/lemonsqueezy) | SaaS with built-in tax handling | `LEMONSQUEEZY_API_KEY` | +| [Polar](/docs/integrations/payments/polar) | Open-source friendly payments | `POLAR_ACCESS_TOKEN` | ## How It Works @@ -86,6 +86,7 @@ const url = await provider.createCheckoutUrl({ ``` Shipkit includes pre-built checkout pages: + - Stripe: `/stripe/checkout` - Lemon Squeezy: `/lemonsqueezy` - Polar: `/polar/checkout` diff --git a/docs/getting-started/setup-wizard.mdx b/docs/getting-started/setup-wizard.mdx index 840dff2a..8145f386 100644 --- a/docs/getting-started/setup-wizard.mdx +++ b/docs/getting-started/setup-wizard.mdx @@ -12,6 +12,7 @@ After deploying, Shipkit's setup wizard at `/install` walks you through initial ### 1. Repository Connection Connect your GitHub account to enable: + - Automatic deployments on push - Repository management from the dashboard - GitHub OAuth login for your users diff --git a/docs/guides/deployment/multi-zone.mdx b/docs/guides/deployment/multi-zone.mdx index e331ebb6..fcdbb5f1 100644 --- a/docs/guides/deployment/multi-zone.mdx +++ b/docs/guides/deployment/multi-zone.mdx @@ -145,11 +145,11 @@ cd ../my-blog && bun dev -- --port 3002 ## Approach Options -| Approach | Complexity | Best For | -|----------|-----------|----------| -| **Next.js Multi-Zone** (above) | Medium | Optimal performance, soft nav within zones | -| **Vercel Proxy Project** | Low | Simple setup, separate teams, hard nav everywhere | -| **Modified Config** | Low | Gradual migration, starting with 1-2 zones | +| Approach | Complexity | Best For | +| ------------------------------ | ---------- | ------------------------------------------------- | +| **Next.js Multi-Zone** (above) | Medium | Optimal performance, soft nav within zones | +| **Vercel Proxy Project** | Low | Simple setup, separate teams, hard nav everywhere | +| **Modified Config** | Low | Gradual migration, starting with 1-2 zones | ## Considerations diff --git a/docs/guides/deployment/vercel-cost-optimization.mdx b/docs/guides/deployment/vercel-cost-optimization.mdx index 26f5225c..173c34c6 100644 --- a/docs/guides/deployment/vercel-cost-optimization.mdx +++ b/docs/guides/deployment/vercel-cost-optimization.mdx @@ -111,13 +111,13 @@ including static assets, images, and prefetches — adding an origin hop each ti ```ts // middleware.ts export const config = { - matcher: [ - // Run only where you actually need middleware... - "/dashboard/:path*", - "/api/:path*", - // ...and explicitly exclude static assets and metadata files: - "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", - ], + matcher: [ + // Run only where you actually need middleware... + "/dashboard/:path*", + "/api/:path*", + // ...and explicitly exclude static assets and metadata files: + "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", + ], }; ``` @@ -145,7 +145,7 @@ Controls (note: there is **no** global `next.config` prefetch toggle, and ```tsx // Disable viewport prefetch for low-priority / numerous links (footers, long lists): - About + About ``` @@ -159,25 +159,15 @@ keep instant navigation without viewport prefetching everything, prefetch on import Link from "next/link"; import { useState } from "react"; -export function HoverPrefetchLink({ - href, - children, -}: { - href: string; - children: React.ReactNode; -}) { - const [active, setActive] = useState(false); - // prefetch={null} restores default prefetching once intent is shown; - // false until then = no viewport prefetch. - return ( - setActive(true)} - > - {children} - - ); +export function HoverPrefetchLink({ href, children }: { href: string; children: React.ReactNode }) { + const [active, setActive] = useState(false); + // prefetch={null} restores default prefetching once intent is shown; + // false until then = no viewport prefetch. + return ( + setActive(true)}> + {children} + + ); } ``` @@ -188,9 +178,9 @@ experimental config, reducing repeat prefetch fetches: ```ts // next.config.ts const nextConfig = { - experimental: { - staleTimes: { dynamic: 30, static: 180 }, - }, + experimental: { + staleTimes: { dynamic: 30, static: 180 }, + }, }; ``` @@ -206,11 +196,11 @@ GET handler returns data that does not change per request, let the CDN cache it: ```ts // app/api/data/route.ts export async function GET() { - return Response.json(data, { - headers: { - "Cache-Control": "s-maxage=3600, stale-while-revalidate=86400", - }, - }); + return Response.json(data, { + headers: { + "Cache-Control": "s-maxage=3600, stale-while-revalidate=86400", + }, + }); } ``` diff --git a/docs/guides/deployment/vercel.mdx b/docs/guides/deployment/vercel.mdx index 9bc33bd7..174b6489 100644 --- a/docs/guides/deployment/vercel.mdx +++ b/docs/guides/deployment/vercel.mdx @@ -29,6 +29,7 @@ vercel deploy 3. Set different values for production/staging as needed Key variables: + - `AUTH_SECRET` - Required. Generate with `openssl rand -base64 32` - `DATABASE_URL` - Required for CMS and most features - Auth provider keys - Per provider (GitHub, Google, etc.) diff --git a/docs/index.md b/docs/index.md index 79064685..2267f384 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1,3 @@ # Shipkit Bones -A Next.js starter to help you ship really, *really* fast. Built for/with Shadcn UI, Cursor, v0.dev. +A Next.js starter to help you ship really, _really_ fast. Built for/with Shadcn UI, Cursor, v0.dev. diff --git a/docs/middleware.mdx b/docs/middleware.mdx index d0473865..859ba8bb 100644 --- a/docs/middleware.mdx +++ b/docs/middleware.mdx @@ -1,8 +1,8 @@ --- -title: 'Middleware' -description: 'Deployment configuration using Vercel in Shipkit' +title: "Middleware" +description: "Deployment configuration using Vercel in Shipkit" --- # Middleware -We purposefully avoided using middleware in Shipkit. There's nothing wrong with using middleware and you can use it without problems. \ No newline at end of file +We purposefully avoided using middleware in Shipkit. There's nothing wrong with using middleware and you can use it without problems. diff --git a/docs/rebranding.md b/docs/rebranding.md index be91424c..6f3ce168 100644 --- a/docs/rebranding.md +++ b/docs/rebranding.md @@ -96,4 +96,4 @@ If you encounter any issues during rebranding: 2. Look for any hardcoded references to "Shipkit" or "bones" in the codebase 3. Update any references that weren't caught by the rebranding script -If you find any hardcoded references that weren't updated by the rebranding script, please consider contributing to improve the script. +If you find any hardcoded references that weren't updated by the rebranding script, please consider contributing to improve the script. diff --git a/docs/reference/components.mdx b/docs/reference/components.mdx index 07acd568..656e63cf 100644 --- a/docs/reference/components.mdx +++ b/docs/reference/components.mdx @@ -68,7 +68,7 @@ import { Dialog, DialogTrigger, DialogContent } from "@/components/ui/dialog"; ```tsx import { Link } from "@/components/primitives/link-with-transition"; -About +About; ``` ### Theme Switching @@ -93,7 +93,7 @@ import ErrorBoundary from "@/components/primitives/error-boundary"; ```tsx import { DataTable } from "@/components/ui/data-table"; - +; ``` ## Adding Components @@ -107,6 +107,7 @@ bunx shadcn@latest add button card dialog ### Custom Components Follow the existing patterns: + - **Server Components first** unless interactivity is required - **Named exports** (`export const MyComponent = () => {}`) - **kebab-case filenames** (`my-component.tsx`) diff --git a/drizzle.config.ts b/drizzle.config.ts index 5ae17095..6c02787b 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -4,13 +4,13 @@ import { env } from "@/env"; const prefix = env?.DB_PREFIX ?? ""; export default { - schema: "./src/server/db/schema.ts", - dialect: "postgresql", - dbCredentials: { - url: env?.DATABASE_URL ?? "", - }, - tablesFilter: [`${prefix}_*`], - out: "./src/migrations", - verbose: true, - strict: true, + schema: "./src/server/db/schema.ts", + dialect: "postgresql", + dbCredentials: { + url: env?.DATABASE_URL ?? "", + }, + tablesFilter: [`${prefix}_*`], + out: "./src/migrations", + verbose: true, + strict: true, } satisfies Config; diff --git a/eslint.config.mjs b/eslint.config.mjs index 8c7ebd6d..4fa25584 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -8,89 +8,89 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: __dirname, }); const eslintConfig = [ - // Base config for all files - { - languageOptions: { - parserOptions: { - project: ["./tsconfig.json"], - tsconfigRootDir: __dirname, - }, - }, - linterOptions: { - reportUnusedDisableDirectives: true, - }, - }, + // Base config for all files + { + languageOptions: { + parserOptions: { + project: ["./tsconfig.json"], + tsconfigRootDir: __dirname, + }, + }, + linterOptions: { + reportUnusedDisableDirectives: true, + }, + }, - // Configure global ignores (replaces .eslintignore) - { - ignores: [ - "node_modules/**", - ".cursor/**", - ".next/**", - "src/app/(demo)/**/*", - "src/components/blocks/**", - "src/components/ui/**", - ], - }, + // Configure global ignores (replaces .eslintignore) + { + ignores: [ + "node_modules/**", + ".cursor/**", + ".next/**", + "src/app/(demo)/**/*", + "src/components/blocks/**", + "src/components/ui/**", + ], + }, - // Extend configurations - ...compat.extends( - "plugin:@typescript-eslint/recommended-type-checked", - "plugin:@typescript-eslint/stylistic-type-checked", - "next/core-web-vitals", - "next/typescript" - ), + // Extend configurations + ...compat.extends( + "plugin:@typescript-eslint/recommended-type-checked", + "plugin:@typescript-eslint/stylistic-type-checked", + "next/core-web-vitals", + "next/typescript" + ), - // TypeScript files configuration - { - files: ["**/*.{ts,tsx}"], - languageOptions: { - parser: tsParser, - parserOptions: { - project: ["./tsconfig.json"], - tsconfigRootDir: __dirname, - ecmaVersion: "latest", - sourceType: "module", - ecmaFeatures: { - jsx: true, - }, - }, - }, - plugins: { - "@typescript-eslint": ts, - }, - rules: { - ...ts.configs.recommended.rules, - "@typescript-eslint/no-unused-vars": [ - "warn", - { - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - }, - ], - "@typescript-eslint/consistent-type-imports": [ - "warn", - { - prefer: "type-imports", - fixStyle: "inline-type-imports", - }, - ], - // ! Todo: Enable - "@typescript-eslint/ban-ts-comment": "warn", - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/no-floating-promises": "warn", - "@typescript-eslint/no-misused-promises": "warn", - "@typescript-eslint/no-unsafe-argument": "warn", - "@typescript-eslint/no-unsafe-assignment": "warn", - "@typescript-eslint/no-unsafe-call": "warn", - "@typescript-eslint/no-unsafe-member-access": "warn", - "@typescript-eslint/no-unsafe-return": "warn", - }, - }, + // TypeScript files configuration + { + files: ["**/*.{ts,tsx}"], + languageOptions: { + parser: tsParser, + parserOptions: { + project: ["./tsconfig.json"], + tsconfigRootDir: __dirname, + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + }, + }, + plugins: { + "@typescript-eslint": ts, + }, + rules: { + ...ts.configs.recommended.rules, + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + }, + ], + "@typescript-eslint/consistent-type-imports": [ + "warn", + { + prefer: "type-imports", + fixStyle: "inline-type-imports", + }, + ], + // ! Todo: Enable + "@typescript-eslint/ban-ts-comment": "warn", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-floating-promises": "warn", + "@typescript-eslint/no-misused-promises": "warn", + "@typescript-eslint/no-unsafe-argument": "warn", + "@typescript-eslint/no-unsafe-assignment": "warn", + "@typescript-eslint/no-unsafe-call": "warn", + "@typescript-eslint/no-unsafe-member-access": "warn", + "@typescript-eslint/no-unsafe-return": "warn", + }, + }, ]; export default eslintConfig; diff --git a/next-sitemap.config.js b/next-sitemap.config.js index 517c4365..bae74472 100644 --- a/next-sitemap.config.js +++ b/next-sitemap.config.js @@ -1,12 +1,12 @@ export default { - siteUrl: process.env.SITE_URL || process.env.BASE_URL || "https://bones.sh", - generateRobotsTxt: true, - sitemapSize: 7000, - robotsTxtOptions: { - policies: [ - // { userAgent: "*", disallow: "/blog/category/*" }, - { userAgent: "*", allow: "/" }, - ], - }, - // exclude: ["/blog/category/*"], + siteUrl: process.env.SITE_URL || process.env.BASE_URL || "https://bones.sh", + generateRobotsTxt: true, + sitemapSize: 7000, + robotsTxtOptions: { + policies: [ + // { userAgent: "*", disallow: "/blog/category/*" }, + { userAgent: "*", allow: "/" }, + ], + }, + // exclude: ["/blog/category/*"], }; diff --git a/next.config.ts b/next.config.ts index 9da8cb00..53c062fe 100644 --- a/next.config.ts +++ b/next.config.ts @@ -179,8 +179,7 @@ const nextConfig: NextConfig = { compiler: { // Remove console logs only in production, excluding error logs - removeConsole: - process.env.NODE_ENV === "production" ? { exclude: ["error"] } : false, + removeConsole: process.env.NODE_ENV === "production" ? { exclude: ["error"] } : false, }, /* diff --git a/playwright.config.ts b/playwright.config.ts index dc9f17f8..9f98a5b0 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -14,68 +14,68 @@ const baseURL = `http://localhost:${PORT}`; * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - // Timeout per test - timeout: 30 * 1000, - // Test directory - testDir: path.join(__dirname, "tests/e2e"), - // If a test fails, retry it additional 2 times - retries: 2, - // Artifacts folder where screenshots, videos, and traces are stored. - outputDir: "test-results/", + // Timeout per test + timeout: 30 * 1000, + // Test directory + testDir: path.join(__dirname, "tests/e2e"), + // If a test fails, retry it additional 2 times + retries: 2, + // Artifacts folder where screenshots, videos, and traces are stored. + outputDir: "test-results/", - // Run your local dev server before starting the tests:* - // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests - webServer: { - command: "pnpm dev", - url: baseURL, - timeout: 120 * 1000, - reuseExistingServer: !process.env.CI, - }, + // Run your local dev server before starting the tests:* + // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests + webServer: { + command: "pnpm dev", + url: baseURL, + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI, + }, - use: { - // Use baseURL so to make navigations relative. - // More information: https://playwright.dev/docs/api/class-testoptions#test-options-base-url - baseURL, + use: { + // Use baseURL so to make navigations relative. + // More information: https://playwright.dev/docs/api/class-testoptions#test-options-base-url + baseURL, - // Retry a test if its failing with enabled tracing. This allows you to analyse the DOM, console logs, network traffic etc. - // More information: https://playwright.dev/docs/trace-viewer - trace: "retry-with-trace", + // Retry a test if its failing with enabled tracing. This allows you to analyse the DOM, console logs, network traffic etc. + // More information: https://playwright.dev/docs/trace-viewer + trace: "retry-with-trace", - // All available context options: https://playwright.dev/docs/api/class-browser#browser-new-context - // contextOptions: { - // ignoreHTTPSErrors: true, - // } - }, + // All available context options: https://playwright.dev/docs/api/class-browser#browser-new-context + // contextOptions: { + // ignoreHTTPSErrors: true, + // } + }, - projects: [ - { - name: "Desktop Chrome", - use: { - ...devices["Desktop Chrome"], - }, - }, - // { - // name: 'Desktop Firefox', - // use: { - // ...devices['Desktop Firefox'], - // }, - // }, - // { - // name: 'Desktop Safari', - // use: { - // ...devices['Desktop Safari'], - // }, - // }, - // Test against mobile viewports. - // { - // name: 'Mobile Chrome', - // use: { - // ...devices['Pixel 5'], - // }, - // }, - // { - // name: 'Mobile Safari', - // use: devices['iPhone 12'], - // }, - ], + projects: [ + { + name: "Desktop Chrome", + use: { + ...devices["Desktop Chrome"], + }, + }, + // { + // name: 'Desktop Firefox', + // use: { + // ...devices['Desktop Firefox'], + // }, + // }, + // { + // name: 'Desktop Safari', + // use: { + // ...devices['Desktop Safari'], + // }, + // }, + // Test against mobile viewports. + // { + // name: 'Mobile Chrome', + // use: { + // ...devices['Pixel 5'], + // }, + // }, + // { + // name: 'Mobile Safari', + // use: devices['iPhone 12'], + // }, + ], }); diff --git a/postcss.config.mjs b/postcss.config.mjs index 2190f1ca..2ef30fcf 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -1,9 +1,9 @@ /** @type {import('postcss-load-config').Config} */ const config = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, }; export default config; diff --git a/scripts/a11y/axe.ts b/scripts/a11y/axe.ts index 2e951b00..e986cc5e 100644 --- a/scripts/a11y/axe.ts +++ b/scripts/a11y/axe.ts @@ -11,95 +11,95 @@ import axeSource from "axe-core"; import { chromium } from "playwright"; interface AxeIssue { - id: string; - impact: string | null; - description: string; - help: string; - helpUrl: string; - nodes: { html: string; target: string[]; failureSummary?: string }[]; + id: string; + impact: string | null; + description: string; + help: string; + helpUrl: string; + nodes: { html: string; target: string[]; failureSummary?: string }[]; } interface AxeReportSummary { - url: string; - violations: AxeIssue[]; - passes: number; - incomplete: number; - inapplicable: number; - outputDir: string; - reportJsonPath: string; + url: string; + violations: AxeIssue[]; + passes: number; + incomplete: number; + inapplicable: number; + outputDir: string; + reportJsonPath: string; } function getUrls(): string[] { - const argv = process.argv.slice(2).filter((a) => !a.startsWith("-")); - if (argv.length) return argv; - const env = - process.env.AXE_URLS?.split(",") - .map((u) => u.trim()) - .filter(Boolean) ?? []; - return env.length ? env : ["http://localhost:3000/"]; + const argv = process.argv.slice(2).filter((a) => !a.startsWith("-")); + if (argv.length) return argv; + const env = + process.env.AXE_URLS?.split(",") + .map((u) => u.trim()) + .filter(Boolean) ?? []; + return env.length ? env : ["http://localhost:3000/"]; } function safeSlug(input: string): string { - return input - .replace(/^https?:\/\//, "") - .replace(/[^a-zA-Z0-9]+/g, "-") - .replace(/-+/g, "-") - .replace(/(^-|-$)/g, "") - .slice(0, 120); + return input + .replace(/^https?:\/\//, "") + .replace(/[^a-zA-Z0-9]+/g, "-") + .replace(/-+/g, "-") + .replace(/(^-|-$)/g, "") + .slice(0, 120); } async function ensureDir(dir: string): Promise { - await fs.mkdir(dir, { recursive: true }); + await fs.mkdir(dir, { recursive: true }); } async function runAxeOnUrl(url: string, outBaseDir: string): Promise { - const outDir = path.join(outBaseDir, `${safeSlug(url)}-axe`); - await ensureDir(outDir); - const browser = await chromium.launch({ headless: true }); - const page = await browser.newPage(); - await page.goto(url, { waitUntil: "load" }); - await page.addScriptTag({ content: axeSource.source }); - const results = await page.evaluate(async () => { - return await (window as any).axe.run({ - runOnly: { type: "tag", values: ["wcag2a", "wcag2aa"] }, - resultTypes: ["violations", "passes", "incomplete", "inapplicable"], - }); - }); - await browser.close(); + const outDir = path.join(outBaseDir, `${safeSlug(url)}-axe`); + await ensureDir(outDir); + const browser = await chromium.launch({ headless: true }); + const page = await browser.newPage(); + await page.goto(url, { waitUntil: "load" }); + await page.addScriptTag({ content: axeSource.source }); + const results = await page.evaluate(async () => { + return await (window as any).axe.run({ + runOnly: { type: "tag", values: ["wcag2a", "wcag2aa"] }, + resultTypes: ["violations", "passes", "incomplete", "inapplicable"], + }); + }); + await browser.close(); - const jsonPath = path.join(outDir, "axe.json"); - await fs.writeFile(jsonPath, JSON.stringify(results, null, 2), "utf8"); - const summary: AxeReportSummary = { - url, - violations: results.violations ?? [], - passes: results.passes?.length ?? 0, - incomplete: results.incomplete?.length ?? 0, - inapplicable: results.inapplicable?.length ?? 0, - outputDir: outDir, - reportJsonPath: jsonPath, - }; - return summary; + const jsonPath = path.join(outDir, "axe.json"); + await fs.writeFile(jsonPath, JSON.stringify(results, null, 2), "utf8"); + const summary: AxeReportSummary = { + url, + violations: results.violations ?? [], + passes: results.passes?.length ?? 0, + incomplete: results.incomplete?.length ?? 0, + inapplicable: results.inapplicable?.length ?? 0, + outputDir: outDir, + reportJsonPath: jsonPath, + }; + return summary; } async function main(): Promise { - const urls = getUrls(); - const outDir = process.env.AXE_OUT_DIR ?? ".axe"; - await ensureDir(outDir); - const summaries: AxeReportSummary[] = []; - for (const url of urls) { - console.log(`[axe] Running on ${url}`); - const summary = await runAxeOnUrl(url, outDir); - summaries.push(summary); + const urls = getUrls(); + const outDir = process.env.AXE_OUT_DIR ?? ".axe"; + await ensureDir(outDir); + const summaries: AxeReportSummary[] = []; + for (const url of urls) { + console.log(`[axe] Running on ${url}`); + const summary = await runAxeOnUrl(url, outDir); + summaries.push(summary); - console.log(`[axe] Violations: ${summary.violations.length}`); - } - const indexPath = path.join(outDir, `index-${Date.now()}.json`); - await fs.writeFile(indexPath, JSON.stringify(summaries, null, 2), "utf8"); + console.log(`[axe] Violations: ${summary.violations.length}`); + } + const indexPath = path.join(outDir, `index-${Date.now()}.json`); + await fs.writeFile(indexPath, JSON.stringify(summaries, null, 2), "utf8"); - console.log(`[axe] Wrote summary index → ${indexPath}`); + console.log(`[axe] Wrote summary index → ${indexPath}`); } main().catch((err) => { - console.error("[axe] Failed:", err); - process.exit(1); + console.error("[axe] Failed:", err); + process.exit(1); }); diff --git a/scripts/clean.ts b/scripts/clean.ts index 7a5942bf..5816fe1b 100644 --- a/scripts/clean.ts +++ b/scripts/clean.ts @@ -7,193 +7,193 @@ import path from "node:path"; const rootDir = process.cwd(); function runCommand(command: string) { - try { - execSync(command, { stdio: "inherit" }); - } catch (error) {} + try { + execSync(command, { stdio: "inherit" }); + } catch (error) {} } function removeDirectory(dir: string) { - const fullPath = path.join(rootDir, dir); - if (fs.existsSync(fullPath)) { - fs.rmSync(fullPath, { recursive: true, force: true }); - } + const fullPath = path.join(rootDir, dir); + if (fs.existsSync(fullPath)) { + fs.rmSync(fullPath, { recursive: true, force: true }); + } } function removeFile(file: string) { - const fullPath = path.join(rootDir, file); - if (fs.existsSync(fullPath)) { - fs.unlinkSync(fullPath); - } + const fullPath = path.join(rootDir, file); + if (fs.existsSync(fullPath)) { + fs.unlinkSync(fullPath); + } } // Files to remove to match bones branch const filesToRemove = [ - // Config files - ".env.test", - ".eslintignore", - ".eslintrc.json", - ".lintstagedrc.js", - ".ncurc", - ".npmrc", - ".nvmrc", - ".prettierignore", - ".vscode/settings.json", - "next-sitemap.config.js", - "vercel.json", - "withLogFlare.ts", - "vitest.config.browser.ts", - "vitest.config.node.ts", - "vitest.config.ts", - "tsconfig.workers.json", - "tsconfig.disabled.json", - "troubleshooting.md", - "todo.mdx", - "start-database.sh", - - // Documentation - "ai/brain.md", - "ai/composer.md", - "ai/context.ts", - "ai/docs.md", - "ai/persona.md", - "ai/rules/notepads.md", - "docs.mdx", - "features.mdx", - "guide.mdx", - - // Scripts - "scripts/generate-registry.ts", - "scripts/generator.ts", - "scripts/push-to-downstream-remotes.sh", - "scripts/scrape-aceternity.ts", - "scripts/db-seed.ts", - "scripts/db-sync.ts", - "scripts/env-sync.sh", - "scripts/tsconfig.json", - - // Source directories to remove - "src/app/(app)/(admin)", - "src/app/(app)/ai", - "src/app/(app)/(dashboard)", - "src/app/(app)/(demo)", - "src/app/(app)/(landing)", - "src/app/(app)/(setup)", - "src/app/(app)/animations", - "src/app/(app)/blog", - "src/app/(app)/docs", - "src/app/(app)/illish", - "src/app/(app)/launch", - "src/app/(app)/logs", - "src/app/(app)/settings", - "src/app/(integrations)", - "src/app/(payload)", - "src/app/(task)", - "src/app/cli", - "src/app/cli-old", - "src/app/guide", - "src/app/mdx", - "src/app/ui", - "src/app/webhooks", - - // Components and UI - "src/components/modules/builder", - "src/components/footers/extended-footer.tsx", - "src/components/forms/contact-form.tsx", - "src/components/forms/feedback-popover.tsx", - "src/components/forms/subscribe-form.tsx", - "src/components/headers/extended-header.tsx", - "src/components/layouts/blog-sidebar.tsx", - "src/components/layouts/sidebar-layout.tsx", - "src/components/primitives/accordion-list.tsx", - "src/components/primitives/balancer.tsx", - "src/components/primitives/masonry.tsx", - "src/components/providers", - "src/components/search", - "src/components/setup-wizard", - "src/components/share.tsx", - "src/components/theme-toggle.tsx", - "src/components/ui", - - // Content - "src/content", - "src/registry", - - // Server and services - "src/server/middleware", - "src/server/services", - - // Tests - "tests", - - // Public assets - "public/examples", - "public/registry", - "public/workers", + // Config files + ".env.test", + ".eslintignore", + ".eslintrc.json", + ".lintstagedrc.js", + ".ncurc", + ".npmrc", + ".nvmrc", + ".prettierignore", + ".vscode/settings.json", + "next-sitemap.config.js", + "vercel.json", + "withLogFlare.ts", + "vitest.config.browser.ts", + "vitest.config.node.ts", + "vitest.config.ts", + "tsconfig.workers.json", + "tsconfig.disabled.json", + "troubleshooting.md", + "todo.mdx", + "start-database.sh", + + // Documentation + "ai/brain.md", + "ai/composer.md", + "ai/context.ts", + "ai/docs.md", + "ai/persona.md", + "ai/rules/notepads.md", + "docs.mdx", + "features.mdx", + "guide.mdx", + + // Scripts + "scripts/generate-registry.ts", + "scripts/generator.ts", + "scripts/push-to-downstream-remotes.sh", + "scripts/scrape-aceternity.ts", + "scripts/db-seed.ts", + "scripts/db-sync.ts", + "scripts/env-sync.sh", + "scripts/tsconfig.json", + + // Source directories to remove + "src/app/(app)/(admin)", + "src/app/(app)/ai", + "src/app/(app)/(dashboard)", + "src/app/(app)/(demo)", + "src/app/(app)/(landing)", + "src/app/(app)/(setup)", + "src/app/(app)/animations", + "src/app/(app)/blog", + "src/app/(app)/docs", + "src/app/(app)/illish", + "src/app/(app)/launch", + "src/app/(app)/logs", + "src/app/(app)/settings", + "src/app/(integrations)", + "src/app/(payload)", + "src/app/(task)", + "src/app/cli", + "src/app/cli-old", + "src/app/guide", + "src/app/mdx", + "src/app/ui", + "src/app/webhooks", + + // Components and UI + "src/components/modules/builder", + "src/components/footers/extended-footer.tsx", + "src/components/forms/contact-form.tsx", + "src/components/forms/feedback-popover.tsx", + "src/components/forms/subscribe-form.tsx", + "src/components/headers/extended-header.tsx", + "src/components/layouts/blog-sidebar.tsx", + "src/components/layouts/sidebar-layout.tsx", + "src/components/primitives/accordion-list.tsx", + "src/components/primitives/balancer.tsx", + "src/components/primitives/masonry.tsx", + "src/components/providers", + "src/components/search", + "src/components/setup-wizard", + "src/components/share.tsx", + "src/components/theme-toggle.tsx", + "src/components/ui", + + // Content + "src/content", + "src/registry", + + // Server and services + "src/server/middleware", + "src/server/services", + + // Tests + "tests", + + // Public assets + "public/examples", + "public/registry", + "public/workers", ]; // Directories that must be removed first const dirsToRemove = [ - "src/app/(app)/(admin)", - "src/app/(app)/ai", - "src/app/(app)/(dashboard)", - "src/app/(app)/(demo)", - "src/app/(app)/(landing)", - "src/app/(app)/(setup)", - "src/app/(app)/animations", - "src/app/(app)/blog", - "src/app/(app)/docs", - "src/app/(app)/illish", - "src/app/(app)/launch", - "src/app/(app)/logs", - "src/app/(app)/settings", - "src/app/(integrations)", - "src/app/(payload)", - "src/app/(task)", - "src/app/cli", - "src/app/cli-old", - "src/app/guide", - "src/app/mdx", - "src/app/ui", - "src/app/webhooks", - "src/components/modules/builder", - "src/components/ui", - "src/content", - "src/registry", - "src/server/middleware", - "src/server/services", - "tests", - "public/examples", - "public/registry", - "public/workers", + "src/app/(app)/(admin)", + "src/app/(app)/ai", + "src/app/(app)/(dashboard)", + "src/app/(app)/(demo)", + "src/app/(app)/(landing)", + "src/app/(app)/(setup)", + "src/app/(app)/animations", + "src/app/(app)/blog", + "src/app/(app)/docs", + "src/app/(app)/illish", + "src/app/(app)/launch", + "src/app/(app)/logs", + "src/app/(app)/settings", + "src/app/(integrations)", + "src/app/(payload)", + "src/app/(task)", + "src/app/cli", + "src/app/cli-old", + "src/app/guide", + "src/app/mdx", + "src/app/ui", + "src/app/webhooks", + "src/components/modules/builder", + "src/components/ui", + "src/content", + "src/registry", + "src/server/middleware", + "src/server/services", + "tests", + "public/examples", + "public/registry", + "public/workers", ]; // Handle different cleanup modes if (process.argv.includes("--bones")) { - // Clean to match bones branch + // Clean to match bones branch - // Remove directories first, then individual files - for (const dir of dirsToRemove) { - removeDirectory(dir); - } + // Remove directories first, then individual files + for (const dir of dirsToRemove) { + removeDirectory(dir); + } - for (const file of filesToRemove) { - removeFile(file); - } + for (const file of filesToRemove) { + removeFile(file); + } } else if (process.argv.includes("--next")) { - removeDirectory(".next"); + removeDirectory(".next"); } else { - // Default cleanup - const defaultDirs = ["node_modules", ".next", ".turbo"]; - const defaultFiles = ["pnpm-lock.yaml"]; - const additionalFolders = process.argv.slice(2); + // Default cleanup + const defaultDirs = ["node_modules", ".next", ".turbo"]; + const defaultFiles = ["pnpm-lock.yaml"]; + const additionalFolders = process.argv.slice(2); - for (const dir of [...defaultDirs, ...additionalFolders]) { - removeDirectory(dir); - } + for (const dir of [...defaultDirs, ...additionalFolders]) { + removeDirectory(dir); + } - for (const file of defaultFiles) { - removeFile(file); - } + for (const file of defaultFiles) { + removeFile(file); + } - runCommand("pnpm store prune"); + runCommand("pnpm store prune"); } diff --git a/scripts/copy-to-temp.ts b/scripts/copy-to-temp.ts index aecfa291..ce708d5e 100644 --- a/scripts/copy-to-temp.ts +++ b/scripts/copy-to-temp.ts @@ -10,108 +10,108 @@ const EXCLUDED_DIRS = ["node_modules", ".next", ".git", "dist", "build", "temp"] * @returns The command output */ function git(command: string): string { - try { - return execSync(`git ${command}`, { encoding: "utf-8" }); - } catch (error) { - console.error(`Git command failed: git ${command}`); - throw error; - } + try { + return execSync(`git ${command}`, { encoding: "utf-8" }); + } catch (error) { + console.error(`Git command failed: git ${command}`); + throw error; + } } async function copyFiles(sourcePath: string, targetPath: string) { - try { - // Create target directory if it doesn't exist - await mkdir(targetPath, { recursive: true }); - - // Read source directory - const entries = await readdir(sourcePath, { withFileTypes: true }); - - for (const entry of entries) { - const sourceFilePath = join(sourcePath, entry.name); - const targetFilePath = join(targetPath, entry.name); - - // Skip excluded directories - if (EXCLUDED_DIRS.includes(entry.name)) { - console.log(`Skipping excluded directory: ${entry.name}`); - continue; - } - - if (entry.isDirectory()) { - // Recursively copy subdirectories - await copyFiles(sourceFilePath, targetFilePath); - } else { - // Copy individual files - await copyFile(sourceFilePath, targetFilePath); - console.log(`Copied: ${relative(process.cwd(), targetFilePath)}`); - } - } - } catch (error) { - console.error("Error copying files:", error); - process.exit(1); - } + try { + // Create target directory if it doesn't exist + await mkdir(targetPath, { recursive: true }); + + // Read source directory + const entries = await readdir(sourcePath, { withFileTypes: true }); + + for (const entry of entries) { + const sourceFilePath = join(sourcePath, entry.name); + const targetFilePath = join(targetPath, entry.name); + + // Skip excluded directories + if (EXCLUDED_DIRS.includes(entry.name)) { + console.log(`Skipping excluded directory: ${entry.name}`); + continue; + } + + if (entry.isDirectory()) { + // Recursively copy subdirectories + await copyFiles(sourceFilePath, targetFilePath); + } else { + // Copy individual files + await copyFile(sourceFilePath, targetFilePath); + console.log(`Copied: ${relative(process.cwd(), targetFilePath)}`); + } + } + } catch (error) { + console.error("Error copying files:", error); + process.exit(1); + } } async function main() { - const sourceDir = process.cwd(); - const tempDir = join(sourceDir, "temp"); - const upstreamUrl = "https://github.com/shipkit-io/bones.git"; - - console.log("Starting upstream sync process..."); - - try { - // Step 1: Save current files to temp directory - console.log("\nSaving current files to temp directory..."); - await copyFiles(sourceDir, tempDir); - - // Step 2: Clean working directory (except temp) - console.log("\nCleaning working directory..."); - const entries = await readdir(sourceDir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.name === "temp" || entry.name === ".git" || EXCLUDED_DIRS.includes(entry.name)) { - continue; - } - await rm(join(sourceDir, entry.name), { recursive: true, force: true }); - } - - // Step 3: Setup upstream remote - console.log("\nSetting up upstream remote..."); - try { - git("remote remove up"); - } catch { - // Ignore error if remote doesn't exist - } - git(`remote add up ${upstreamUrl}`); - git("fetch up"); - - // Step 4: Create temp branch from upstream - console.log("\nCreating temp branch from upstream..."); - git("checkout up/main"); - git("switch -c temp"); - - // Step 5: Copy files back from temp - console.log("\nCopying files back from temp..."); - await copyFiles(tempDir, sourceDir); - await rm(tempDir, { recursive: true, force: true }); - - // Step 6: Commit the changes - console.log("\nCommitting changes..."); - git("add ."); - git('commit -m "merge upstream changes"'); - - // Step 7: Checkout main and merge changes - console.log("\nChecking out main and merging changes..."); - git("checkout main"); - git("merge temp --allow-unrelated-histories"); - - console.log("\nProcess completed successfully!"); - console.log("Please resolve any merge conflicts, then run:"); - console.log("git add ."); - console.log('git commit -m "added upstream"'); - console.log("git push"); - } catch (error) { - console.error("Failed to sync with upstream:", error); - process.exit(1); - } + const sourceDir = process.cwd(); + const tempDir = join(sourceDir, "temp"); + const upstreamUrl = "https://github.com/shipkit-io/bones.git"; + + console.log("Starting upstream sync process..."); + + try { + // Step 1: Save current files to temp directory + console.log("\nSaving current files to temp directory..."); + await copyFiles(sourceDir, tempDir); + + // Step 2: Clean working directory (except temp) + console.log("\nCleaning working directory..."); + const entries = await readdir(sourceDir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.name === "temp" || entry.name === ".git" || EXCLUDED_DIRS.includes(entry.name)) { + continue; + } + await rm(join(sourceDir, entry.name), { recursive: true, force: true }); + } + + // Step 3: Setup upstream remote + console.log("\nSetting up upstream remote..."); + try { + git("remote remove up"); + } catch { + // Ignore error if remote doesn't exist + } + git(`remote add up ${upstreamUrl}`); + git("fetch up"); + + // Step 4: Create temp branch from upstream + console.log("\nCreating temp branch from upstream..."); + git("checkout up/main"); + git("switch -c temp"); + + // Step 5: Copy files back from temp + console.log("\nCopying files back from temp..."); + await copyFiles(tempDir, sourceDir); + await rm(tempDir, { recursive: true, force: true }); + + // Step 6: Commit the changes + console.log("\nCommitting changes..."); + git("add ."); + git('commit -m "merge upstream changes"'); + + // Step 7: Checkout main and merge changes + console.log("\nChecking out main and merging changes..."); + git("checkout main"); + git("merge temp --allow-unrelated-histories"); + + console.log("\nProcess completed successfully!"); + console.log("Please resolve any merge conflicts, then run:"); + console.log("git add ."); + console.log('git commit -m "added upstream"'); + console.log("git push"); + } catch (error) { + console.error("Failed to sync with upstream:", error); + process.exit(1); + } } // Run the script diff --git a/scripts/db-drop.ts b/scripts/db-drop.ts index 1458fcf2..bb9d7a1c 100644 --- a/scripts/db-drop.ts +++ b/scripts/db-drop.ts @@ -4,27 +4,27 @@ import { sql } from "drizzle-orm"; import { db } from "@/server/db"; async function dropDatabase() { - console.info("🗑️ Starting database cleanup..."); + console.info("🗑️ Starting database cleanup..."); - try { - // Drop all tables in the public schema - await db?.execute(sql`DROP SCHEMA IF EXISTS public CASCADE`); + try { + // Drop all tables in the public schema + await db?.execute(sql`DROP SCHEMA IF EXISTS public CASCADE`); - // Recreate the public schema - await db?.execute(sql`CREATE SCHEMA public`); + // Recreate the public schema + await db?.execute(sql`CREATE SCHEMA public`); - // Drop Payload and Drizzle schemas - console.info("📦 Dropping Payload and Drizzle schemas..."); - await db?.execute(sql` + // Drop Payload and Drizzle schemas + console.info("📦 Dropping Payload and Drizzle schemas..."); + await db?.execute(sql` DROP SCHEMA IF EXISTS payload CASCADE; DROP SCHEMA IF EXISTS drizzle CASCADE; `); - // Recreate the payload and drizzle schemas - await db?.execute(sql`CREATE SCHEMA payload; CREATE SCHEMA drizzle;`); + // Recreate the payload and drizzle schemas + await db?.execute(sql`CREATE SCHEMA payload; CREATE SCHEMA drizzle;`); - // Drop custom types - await db?.execute(sql` + // Drop custom types + await db?.execute(sql` DO $$ DECLARE r RECORD; BEGIN @@ -34,12 +34,12 @@ async function dropDatabase() { END $$; `); - console.info("✅ Database cleanup completed"); - process.exit(0); - } catch (error) { - console.error("❌ Error cleaning up database:", error); - process.exit(1); - } + console.info("✅ Database cleanup completed"); + process.exit(0); + } catch (error) { + console.error("❌ Error cleaning up database:", error); + process.exit(1); + } } void dropDatabase(); diff --git a/scripts/db-seed.ts b/scripts/db-seed.ts index 50ca5bcf..1086c549 100644 --- a/scripts/db-seed.ts +++ b/scripts/db-seed.ts @@ -2,26 +2,26 @@ import { seed as seedCMS } from "@/lib/payload/seed"; async function main() { - console.log("🌱 Starting database seeding..."); + console.log("🌱 Starting database seeding..."); - try { - // Seed CMS collections (RBAC, Features, FAQs, Testimonials) - await seedCMS(); + try { + // Seed CMS collections (RBAC, Features, FAQs, Testimonials) + await seedCMS(); - console.log("✅ Database seeded successfully"); - } catch (error) { - console.error("❌ Error seeding database:", error); - throw error; - } + console.log("✅ Database seeded successfully"); + } catch (error) { + console.error("❌ Error seeding database:", error); + throw error; + } } export { main as seed }; main() - .catch((err) => { - console.error("❌ Error seeding database:", err); - process.exit(1); - }) - .then(() => { - process.exit(0); - }); + .catch((err) => { + console.error("❌ Error seeding database:", err); + process.exit(1); + }) + .then(() => { + process.exit(0); + }); diff --git a/scripts/db-sync.ts b/scripts/db-sync.ts index ec8752c9..2f8599bc 100644 --- a/scripts/db-sync.ts +++ b/scripts/db-sync.ts @@ -11,123 +11,123 @@ const execAsync = promisify(exec); // Helper function to run a command with interactive stdin function spawnInteractive(command: string, args: string[], input?: string): Promise { - return new Promise((resolve, reject) => { - const proc = spawn(command, args, { - stdio: input ? ["pipe", "inherit", "inherit"] : "inherit", // Connect stdout/stderr, pipe stdin if input is provided - shell: true, - env: { ...process.env, PAYLOAD_CONFIG_PATH: "src/payload.config.ts" }, - }); - - if (input && proc.stdin) { - proc.stdin.write(input); - proc.stdin.end(); - } - - proc.on("close", (code) => { - if (code === 0) { - resolve(); - } else { - reject(new Error(`Process exited with code ${code}`)); - } - }); - - proc.on("error", (err) => { - reject(err); - }); - }); + return new Promise((resolve, reject) => { + const proc = spawn(command, args, { + stdio: input ? ["pipe", "inherit", "inherit"] : "inherit", // Connect stdout/stderr, pipe stdin if input is provided + shell: true, + env: { ...process.env, PAYLOAD_CONFIG_PATH: "src/payload.config.ts" }, + }); + + if (input && proc.stdin) { + proc.stdin.write(input); + proc.stdin.end(); + } + + proc.on("close", (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Process exited with code ${code}`)); + } + }); + + proc.on("error", (err) => { + reject(err); + }); + }); } async function generateMigrations() { - console.log("📝 Generating migrations..."); - - try { - // Generate Drizzle migrations - console.log("📦 Generating Drizzle migrations..."); - await execAsync("bun run db:generate"); - console.log("✅ Drizzle migrations generated"); - - // Generate Payload migrations using CLI directly with interactive stdin - console.log("📦 Generating Payload migrations..."); - console.log("ℹ️ If prompted, press Enter to continue"); - - await spawnInteractive("payload", ["migrate:create"]); - console.log("✅ Payload migrations generated"); - } catch (error) { - console.error("❌ Error generating migrations:", error); - throw error; - } + console.log("📝 Generating migrations..."); + + try { + // Generate Drizzle migrations + console.log("📦 Generating Drizzle migrations..."); + await execAsync("bun run db:generate"); + console.log("✅ Drizzle migrations generated"); + + // Generate Payload migrations using CLI directly with interactive stdin + console.log("📦 Generating Payload migrations..."); + console.log("ℹ️ If prompted, press Enter to continue"); + + await spawnInteractive("payload", ["migrate:create"]); + console.log("✅ Payload migrations generated"); + } catch (error) { + console.error("❌ Error generating migrations:", error); + throw error; + } } async function runPayloadMigrations() { - console.log("📦 Running Payload migrations..."); - try { - // Use spawn for interactive process, automatically answer "y" to prompts - await spawnInteractive("payload", ["migrate"], "y\n"); - console.log("✅ Payload migrations completed"); - } catch (error) { - if (error instanceof Error && error.message.includes("already exists")) { - console.warn("⚠️ Some Payload tables already exist, continuing..."); - } else { - throw error; - } - } + console.log("📦 Running Payload migrations..."); + try { + // Use spawn for interactive process, automatically answer "y" to prompts + await spawnInteractive("payload", ["migrate"], "y\n"); + console.log("✅ Payload migrations completed"); + } catch (error) { + if (error instanceof Error && error.message.includes("already exists")) { + console.warn("⚠️ Some Payload tables already exist, continuing..."); + } else { + throw error; + } + } } async function syncDatabase() { - console.log("🔄 Starting database synchronization..."); - - try { - if (!db) { - console.error("❌ Database connection not found"); - process.exit(1); - } - - // 1. Generate migrations if no SKIP_MIGRATIONS - if (process.env.SKIP_MIGRATIONS !== "true") { - await generateMigrations(); - } - - // 2. Run Drizzle migrations - console.log("📦 Running Drizzle migrations..."); - try { - await migrate(db, { - migrationsFolder: path.join(process.cwd(), "src/migrations"), - }); - console.log("✅ Drizzle migrations completed"); - } catch (error) { - // Check if the error is about existing types or schemas - if ( - error instanceof Error && - (error.message.includes("already exists") || - error.message.includes("schema") || - error.message.includes("type")) - ) { - console.warn("⚠️ Some database objects already exist, continuing..."); - } else { - throw error; - } - } - - // 3. Run Payload migrations using CLI - await runPayloadMigrations(); - - // 4. Initialize Payload - console.log("🚀 Initializing Payload CMS..."); - const payload = await getPayloadClient(); - if (!payload) { - console.warn("⚠️ Did not initialize Payload"); - } else if (process.env.SEED_DB === "true") { - console.log("🌱 Running database seed..."); - await seed(); - console.log("✅ Database seeded"); - } - - console.log("✨ Database synchronization completed successfully!"); - process.exit(0); - } catch (error) { - console.error("❌ Error synchronizing database:", error); - process.exit(1); - } + console.log("🔄 Starting database synchronization..."); + + try { + if (!db) { + console.error("❌ Database connection not found"); + process.exit(1); + } + + // 1. Generate migrations if no SKIP_MIGRATIONS + if (process.env.SKIP_MIGRATIONS !== "true") { + await generateMigrations(); + } + + // 2. Run Drizzle migrations + console.log("📦 Running Drizzle migrations..."); + try { + await migrate(db, { + migrationsFolder: path.join(process.cwd(), "src/migrations"), + }); + console.log("✅ Drizzle migrations completed"); + } catch (error) { + // Check if the error is about existing types or schemas + if ( + error instanceof Error && + (error.message.includes("already exists") || + error.message.includes("schema") || + error.message.includes("type")) + ) { + console.warn("⚠️ Some database objects already exist, continuing..."); + } else { + throw error; + } + } + + // 3. Run Payload migrations using CLI + await runPayloadMigrations(); + + // 4. Initialize Payload + console.log("🚀 Initializing Payload CMS..."); + const payload = await getPayloadClient(); + if (!payload) { + console.warn("⚠️ Did not initialize Payload"); + } else if (process.env.SEED_DB === "true") { + console.log("🌱 Running database seed..."); + await seed(); + console.log("✅ Database seeded"); + } + + console.log("✨ Database synchronization completed successfully!"); + process.exit(0); + } catch (error) { + console.error("❌ Error synchronizing database:", error); + process.exit(1); + } } void syncDatabase(); diff --git a/scripts/generator.ts b/scripts/generator.ts index 51f3cdff..0c4102ca 100644 --- a/scripts/generator.ts +++ b/scripts/generator.ts @@ -2,136 +2,136 @@ import fs from "fs/promises"; import path from "path"; interface BlockConfig { - // The name of the block (e.g., "login-01") - name: string; - // Description of the block - description?: string; - // Type of registry item (default: "registry:block") - type?: string; - // Source directory containing the block files - sourceDir: string; - // Registry dependencies (e.g., ["button", "card", "input"]) - registryDependencies?: string[]; - // NPM dependencies - dependencies?: string[]; - // Categories for the block - categories?: string[]; - // File mappings from source to target - files: { - // Source path relative to sourceDir - source: string; - // Target path in the registry - target?: string; - // Type of file (default: "registry:component") - type?: string; - }[]; + // The name of the block (e.g., "login-01") + name: string; + // Description of the block + description?: string; + // Type of registry item (default: "registry:block") + type?: string; + // Source directory containing the block files + sourceDir: string; + // Registry dependencies (e.g., ["button", "card", "input"]) + registryDependencies?: string[]; + // NPM dependencies + dependencies?: string[]; + // Categories for the block + categories?: string[]; + // File mappings from source to target + files: { + // Source path relative to sourceDir + source: string; + // Target path in the registry + target?: string; + // Type of file (default: "registry:component") + type?: string; + }[]; } interface GeneratorOptions { - // Directory containing block directories - blocksDir: string; - // Output directory for JSON files - outputDir: string; - // Optional base configuration for all blocks - baseConfig?: Partial; - // Optional function to transform content before writing - contentTransform?: (content: string) => string; + // Directory containing block directories + blocksDir: string; + // Output directory for JSON files + outputDir: string; + // Optional base configuration for all blocks + baseConfig?: Partial; + // Optional function to transform content before writing + contentTransform?: (content: string) => string; } async function readFileContent(filePath: string): Promise { - try { - return await fs.readFile(filePath, "utf-8"); - } catch (error) { - throw new Error(`Failed to read file ${filePath}: ${error}`); - } + try { + return await fs.readFile(filePath, "utf-8"); + } catch (error) { + throw new Error(`Failed to read file ${filePath}: ${error}`); + } } async function writeJsonFile(filePath: string, content: any): Promise { - try { - await fs.mkdir(path.dirname(filePath), { recursive: true }); - await fs.writeFile(filePath, JSON.stringify(content, null, 2), "utf-8"); - } catch (error) { - throw new Error(`Failed to write file ${filePath}: ${error}`); - } + try { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, JSON.stringify(content, null, 2), "utf-8"); + } catch (error) { + throw new Error(`Failed to write file ${filePath}: ${error}`); + } } export async function generateBlockJson( - blockConfig: BlockConfig, - options: GeneratorOptions + blockConfig: BlockConfig, + options: GeneratorOptions ): Promise { - const { - name, - description, - type = "registry:block", - sourceDir, - registryDependencies = [], - dependencies = [], - categories = [], - files: fileConfigs, - } = blockConfig; - - const files = await Promise.all( - fileConfigs.map(async (file) => { - const sourcePath = path.join(sourceDir, file.source); - let content = await readFileContent(sourcePath); - - if (options.contentTransform) { - content = options.contentTransform(content); - } - - return { - path: file.source, - content, - type: file.type || "registry:component", - target: file.target || "", - }; - }) - ); - - const blockJson = { - name, - type, - ...(description && { description }), - ...(dependencies.length > 0 && { dependencies }), - ...(registryDependencies.length > 0 && { registryDependencies }), - ...(categories.length > 0 && { categories }), - files, - }; - - const outputPath = path.join(options.outputDir, "styles", "default", `${name}.json`); - - await writeJsonFile(outputPath, blockJson); + const { + name, + description, + type = "registry:block", + sourceDir, + registryDependencies = [], + dependencies = [], + categories = [], + files: fileConfigs, + } = blockConfig; + + const files = await Promise.all( + fileConfigs.map(async (file) => { + const sourcePath = path.join(sourceDir, file.source); + let content = await readFileContent(sourcePath); + + if (options.contentTransform) { + content = options.contentTransform(content); + } + + return { + path: file.source, + content, + type: file.type || "registry:component", + target: file.target || "", + }; + }) + ); + + const blockJson = { + name, + type, + ...(description && { description }), + ...(dependencies.length > 0 && { dependencies }), + ...(registryDependencies.length > 0 && { registryDependencies }), + ...(categories.length > 0 && { categories }), + files, + }; + + const outputPath = path.join(options.outputDir, "styles", "default", `${name}.json`); + + await writeJsonFile(outputPath, blockJson); } export async function generateAllBlocks(options: GeneratorOptions): Promise { - const blockDirs = await fs.readdir(options.blocksDir); - - for (const blockDir of blockDirs) { - const blockPath = path.join(options.blocksDir, blockDir); - const stat = await fs.stat(blockPath); - console.log("Processing block:", blockPath); - - if (!stat.isDirectory()) continue; - - // Look for a block.config.json in the block directory - const configPath = path.join(blockPath, "block.config.json"); - let blockConfig: BlockConfig; - - try { - const configContent = await readFileContent(configPath); - console.log("Found config:", configPath); - blockConfig = { - ...options.baseConfig, - ...JSON.parse(configContent), - sourceDir: blockPath, - }; - } catch (error) { - console.error(`Error processing config for ${blockDir}:`, error); - continue; - } - - await generateBlockJson(blockConfig, options); - } + const blockDirs = await fs.readdir(options.blocksDir); + + for (const blockDir of blockDirs) { + const blockPath = path.join(options.blocksDir, blockDir); + const stat = await fs.stat(blockPath); + console.log("Processing block:", blockPath); + + if (!stat.isDirectory()) continue; + + // Look for a block.config.json in the block directory + const configPath = path.join(blockPath, "block.config.json"); + let blockConfig: BlockConfig; + + try { + const configContent = await readFileContent(configPath); + console.log("Found config:", configPath); + blockConfig = { + ...options.baseConfig, + ...JSON.parse(configContent), + sourceDir: blockPath, + }; + } catch (error) { + console.error(`Error processing config for ${blockDir}:`, error); + continue; + } + + await generateBlockJson(blockConfig, options); + } } // Example usage: diff --git a/scripts/git-add-upstream.ts b/scripts/git-add-upstream.ts index c2f26985..c9e2ae1a 100644 --- a/scripts/git-add-upstream.ts +++ b/scripts/git-add-upstream.ts @@ -11,130 +11,130 @@ const upstreamUrl = "https://github.com/shipkit-io/bones.git"; * @returns The command output */ function git(command: string): string { - try { - return execSync(`git ${command}`, { encoding: "utf-8" }); - } catch (error) { - console.error(`Git command failed: git ${command}`); - throw error; - } + try { + return execSync(`git ${command}`, { encoding: "utf-8" }); + } catch (error) { + console.error(`Git command failed: git ${command}`); + throw error; + } } async function copyFiles(sourcePath: string, targetPath: string) { - try { - // Create target directory if it doesn't exist - await mkdir(targetPath, { recursive: true }); - - // Read source directory - const entries = await readdir(sourcePath, { withFileTypes: true }); - - for (const entry of entries) { - const sourceFilePath = join(sourcePath, entry.name); - const targetFilePath = join(targetPath, entry.name); - - // Skip excluded directories - if (EXCLUDED_DIRS.includes(entry.name)) { - console.info(`Skipping excluded directory: ${entry.name}`); - continue; - } - - if (entry.isDirectory()) { - // Recursively copy subdirectories - await copyFiles(sourceFilePath, targetFilePath); - } else { - // Copy individual files - await copyFile(sourceFilePath, targetFilePath); - console.info(`Copied: ${relative(process.cwd(), targetFilePath)}`); - } - } - } catch (error) { - console.error("Error copying files:", error); - process.exit(1); - } + try { + // Create target directory if it doesn't exist + await mkdir(targetPath, { recursive: true }); + + // Read source directory + const entries = await readdir(sourcePath, { withFileTypes: true }); + + for (const entry of entries) { + const sourceFilePath = join(sourcePath, entry.name); + const targetFilePath = join(targetPath, entry.name); + + // Skip excluded directories + if (EXCLUDED_DIRS.includes(entry.name)) { + console.info(`Skipping excluded directory: ${entry.name}`); + continue; + } + + if (entry.isDirectory()) { + // Recursively copy subdirectories + await copyFiles(sourceFilePath, targetFilePath); + } else { + // Copy individual files + await copyFile(sourceFilePath, targetFilePath); + console.info(`Copied: ${relative(process.cwd(), targetFilePath)}`); + } + } + } catch (error) { + console.error("Error copying files:", error); + process.exit(1); + } } async function main() { - const sourceDir = process.cwd(); - const tempDir = join(sourceDir, "temp"); - - console.info("Starting upstream sync process..."); - - try { - // Make sure we're on main branch first - console.info("\nSwitching to main branch..."); - git("checkout main"); - - // Delete existing temp branch if it exists - console.info("\nCleaning up existing temp branch..."); - try { - git("branch -D temp"); - } catch { - // Ignore error if branch doesn't exist - } - - // Force cleanup any other temp state - try { - git("clean -fd"); // Clean untracked files and directories - git("reset --hard"); // Reset any uncommitted changes - } catch { - // Ignore cleanup errors - } - - // Step 1: Save current files to temp directory - console.info("\nSaving current files to temp directory..."); - await copyFiles(sourceDir, tempDir); - - // Step 2: Clean working directory (except temp) - console.info("\nCleaning working directory..."); - const entries = await readdir(sourceDir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.name === "temp" || entry.name === ".git" || EXCLUDED_DIRS.includes(entry.name)) { - continue; - } - await rm(join(sourceDir, entry.name), { recursive: true, force: true }); - } - - // Step 3: Setup upstream remote - console.info("\nSetting up upstream remote..."); - try { - git("remote remove up"); - } catch { - // Ignore error if remote doesn't exist - } - git(`remote add up ${upstreamUrl}`); - git("fetch up"); - - // Step 4: Create temp branch from upstream - console.info("\nCreating temp branch from upstream..."); - git("checkout up/main"); - git("switch -c temp"); - - // Step 5: Copy files back from temp - console.info("\nCopying files back from temp..."); - await copyFiles(tempDir, sourceDir); - await rm(tempDir, { recursive: true, force: true }); - - // Step 6: Commit the changes - console.info("\nCommitting changes..."); - git("add ."); - git('commit -m "merge upstream changes"'); - - // Step 7: Checkout main and merge changes - console.info("\nChecking out main and merging changes..."); - git("checkout main"); - git("merge temp --allow-unrelated-histories"); - - console.info("\nProcess completed successfully!"); - console.info("Please resolve any merge conflicts, then run:"); - console.info("git add ."); - console.info('git commit -m "added upstream"'); - console.info("git push"); - } catch (error) { - console.error("Failed to sync with upstream:", error); - if (error instanceof Error) { - console.error("Error details:", error.message); - } - process.exit(1); - } + const sourceDir = process.cwd(); + const tempDir = join(sourceDir, "temp"); + + console.info("Starting upstream sync process..."); + + try { + // Make sure we're on main branch first + console.info("\nSwitching to main branch..."); + git("checkout main"); + + // Delete existing temp branch if it exists + console.info("\nCleaning up existing temp branch..."); + try { + git("branch -D temp"); + } catch { + // Ignore error if branch doesn't exist + } + + // Force cleanup any other temp state + try { + git("clean -fd"); // Clean untracked files and directories + git("reset --hard"); // Reset any uncommitted changes + } catch { + // Ignore cleanup errors + } + + // Step 1: Save current files to temp directory + console.info("\nSaving current files to temp directory..."); + await copyFiles(sourceDir, tempDir); + + // Step 2: Clean working directory (except temp) + console.info("\nCleaning working directory..."); + const entries = await readdir(sourceDir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.name === "temp" || entry.name === ".git" || EXCLUDED_DIRS.includes(entry.name)) { + continue; + } + await rm(join(sourceDir, entry.name), { recursive: true, force: true }); + } + + // Step 3: Setup upstream remote + console.info("\nSetting up upstream remote..."); + try { + git("remote remove up"); + } catch { + // Ignore error if remote doesn't exist + } + git(`remote add up ${upstreamUrl}`); + git("fetch up"); + + // Step 4: Create temp branch from upstream + console.info("\nCreating temp branch from upstream..."); + git("checkout up/main"); + git("switch -c temp"); + + // Step 5: Copy files back from temp + console.info("\nCopying files back from temp..."); + await copyFiles(tempDir, sourceDir); + await rm(tempDir, { recursive: true, force: true }); + + // Step 6: Commit the changes + console.info("\nCommitting changes..."); + git("add ."); + git('commit -m "merge upstream changes"'); + + // Step 7: Checkout main and merge changes + console.info("\nChecking out main and merging changes..."); + git("checkout main"); + git("merge temp --allow-unrelated-histories"); + + console.info("\nProcess completed successfully!"); + console.info("Please resolve any merge conflicts, then run:"); + console.info("git add ."); + console.info('git commit -m "added upstream"'); + console.info("git push"); + } catch (error) { + console.error("Failed to sync with upstream:", error); + if (error instanceof Error) { + console.error("Error details:", error.message); + } + process.exit(1); + } } // Run the script diff --git a/scripts/git-checkpoint.ts b/scripts/git-checkpoint.ts index 08c287cd..2a9aef78 100755 --- a/scripts/git-checkpoint.ts +++ b/scripts/git-checkpoint.ts @@ -11,30 +11,30 @@ import { format } from "date-fns"; */ try { - // Get current branch name - const currentBranch = execSync("git branch --show-current").toString().trim(); - console.log(`📝 Current branch: ${currentBranch}`); + // Get current branch name + const currentBranch = execSync("git branch --show-current").toString().trim(); + console.log(`📝 Current branch: ${currentBranch}`); - // Format date for branch name (YYYY-MM-DD-HHMMSS) - const dateString = format(new Date(), "yyyy-MM-dd-HHmmss"); + // Format date for branch name (YYYY-MM-DD-HHMMSS) + const dateString = format(new Date(), "yyyy-MM-dd-HHmmss"); - // Create checkpoint branch name - const checkpointBranch = `checkpoint-${currentBranch}-${dateString}`; - console.log(`🔖 Creating checkpoint branch: ${checkpointBranch}`); + // Create checkpoint branch name + const checkpointBranch = `checkpoint-${currentBranch}-${dateString}`; + console.log(`🔖 Creating checkpoint branch: ${checkpointBranch}`); - // Create and checkout the checkpoint branch - execSync(`git checkout -b ${checkpointBranch}`); + // Create and checkout the checkpoint branch + execSync(`git checkout -b ${checkpointBranch}`); - // Push the checkpoint branch to the remote - console.log(`📤 Pushing ${checkpointBranch} to remote...`); - execSync(`git push -u origin ${checkpointBranch}`); + // Push the checkpoint branch to the remote + console.log(`📤 Pushing ${checkpointBranch} to remote...`); + execSync(`git push -u origin ${checkpointBranch}`); - // Return to the original branch - console.log(`↩️ Returning to branch: ${currentBranch}`); - execSync(`git checkout ${currentBranch}`); + // Return to the original branch + console.log(`↩️ Returning to branch: ${currentBranch}`); + execSync(`git checkout ${currentBranch}`); - console.log("✅ Checkpoint created successfully"); + console.log("✅ Checkpoint created successfully"); } catch (error) { - console.error("❌ Error creating checkpoint:", error); - process.exit(1); + console.error("❌ Error creating checkpoint:", error); + process.exit(1); } diff --git a/scripts/git-sync-upstream.ts b/scripts/git-sync-upstream.ts index 893be208..c2e34f85 100644 --- a/scripts/git-sync-upstream.ts +++ b/scripts/git-sync-upstream.ts @@ -1,11 +1,11 @@ import { execSync } from "node:child_process"; import { Command } from "commander"; import { - createPullRequest, - deleteBranch, - generateBranchName, - runCommand, - verifyCurrentBranch, + createPullRequest, + deleteBranch, + generateBranchName, + runCommand, + verifyCurrentBranch, } from "./utils/git"; // Configuration @@ -16,12 +16,12 @@ const CURRENT_BRANCH = "main"; // Bones is the root template. An existing `upstream` remote always wins; set // UPSTREAM_REPO_URL to force a different upstream. const UPSTREAM_REPOS: string[] = process.env.UPSTREAM_REPO_URL - ? [process.env.UPSTREAM_REPO_URL] - : ["https://github.com/shipkit-io/bones.git"]; + ? [process.env.UPSTREAM_REPO_URL] + : ["https://github.com/shipkit-io/bones.git"]; interface SyncOptions { - direct: boolean; - labels?: string[]; + direct: boolean; + labels?: string[]; } /** @@ -29,29 +29,29 @@ interface SyncOptions { * Uses git ls-remote which is lightweight and doesn't clone */ function canAccessRepo(url: string): boolean { - try { - execSync(`git ls-remote ${url}`, { stdio: "ignore" }); - return true; - } catch { - return false; - } + try { + execSync(`git ls-remote ${url}`, { stdio: "ignore" }); + return true; + } catch { + return false; + } } /** * Gets the first accessible upstream URL from the default list */ function getUpstreamUrl(): string { - for (const url of UPSTREAM_REPOS) { - console.info(`Checking access to ${url}...`); - if (canAccessRepo(url)) { - console.info(`✅ Using upstream: ${url}`); - return url; - } - console.info(`⚠️ No access to ${url}, trying next...`); - } - throw new Error( - "No accessible upstream repository found. Please check your GitHub authentication." - ); + for (const url of UPSTREAM_REPOS) { + console.info(`Checking access to ${url}...`); + if (canAccessRepo(url)) { + console.info(`✅ Using upstream: ${url}`); + return url; + } + console.info(`⚠️ No access to ${url}, trying next...`); + } + throw new Error( + "No accessible upstream repository found. Please check your GitHub authentication." + ); } /** @@ -60,94 +60,94 @@ function getUpstreamUrl(): string { * UPSTREAM_REPO_URL is set, which forces that URL. */ function ensureUpstreamRemote(): void { - const forcedUrl = process.env.UPSTREAM_REPO_URL; - - try { - const existingUrl = runCommand(`git remote get-url ${UPSTREAM_REMOTE}`).trim(); - - if (forcedUrl && existingUrl !== forcedUrl) { - console.info( - `Updating upstream remote '${UPSTREAM_REMOTE}' from '${existingUrl}' to '${forcedUrl}'` - ); - runCommand(`git remote set-url ${UPSTREAM_REMOTE} ${forcedUrl}`); - return; - } - - console.info(`Using existing remote '${UPSTREAM_REMOTE}' -> ${existingUrl}`); - } catch { - const upstreamUrl = getUpstreamUrl(); - console.info(`Adding upstream remote '${UPSTREAM_REMOTE}' -> ${upstreamUrl}`); - runCommand(`git remote add ${UPSTREAM_REMOTE} ${upstreamUrl}`); - } + const forcedUrl = process.env.UPSTREAM_REPO_URL; + + try { + const existingUrl = runCommand(`git remote get-url ${UPSTREAM_REMOTE}`).trim(); + + if (forcedUrl && existingUrl !== forcedUrl) { + console.info( + `Updating upstream remote '${UPSTREAM_REMOTE}' from '${existingUrl}' to '${forcedUrl}'` + ); + runCommand(`git remote set-url ${UPSTREAM_REMOTE} ${forcedUrl}`); + return; + } + + console.info(`Using existing remote '${UPSTREAM_REMOTE}' -> ${existingUrl}`); + } catch { + const upstreamUrl = getUpstreamUrl(); + console.info(`Adding upstream remote '${UPSTREAM_REMOTE}' -> ${upstreamUrl}`); + runCommand(`git remote add ${UPSTREAM_REMOTE} ${upstreamUrl}`); + } } async function syncUpstream(options: SyncOptions): Promise { - console.info("Syncing from upstream..."); - - try { - // Ensure upstream remote exists (add if missing) - ensureUpstreamRemote(); - - // Fetch the latest changes from upstream - runCommand(`git fetch ${UPSTREAM_REMOTE}`); - - if (!options.direct) { - const tempBranch = generateBranchName("sync-upstream"); - - // Clean up any existing temp branch with same name (shouldn't exist due to timestamp) - deleteBranch(tempBranch); - - // Create new temp branch from current branch - runCommand(`git checkout -b ${tempBranch}`); - - try { - // Merge upstream changes into temp branch - console.info( - `Merging changes from ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH} into ${tempBranch}...` - ); - runCommand(`git merge ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}`); - - // Create and push PR - runCommand(`git push -u origin ${tempBranch}`); - - createPullRequest({ - title: "chore: sync upstream changes", - body: "Automated PR to sync changes from upstream repository", - base: CURRENT_BRANCH, - head: tempBranch, - labels: options.labels, - }); - } finally { - // Always switch back to original branch - runCommand(`git checkout ${CURRENT_BRANCH}`); - } - } else { - // Verify we're on the correct branch for direct merge - verifyCurrentBranch(CURRENT_BRANCH); - - // Merge upstream changes directly - console.info(`Merging changes from ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}...`); - runCommand(`git merge ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}`); - } - - console.info("Update from upstream completed successfully."); - } catch (error) { - console.error("Failed to sync upstream:", error instanceof Error ? error.message : error); - process.exit(1); - } + console.info("Syncing from upstream..."); + + try { + // Ensure upstream remote exists (add if missing) + ensureUpstreamRemote(); + + // Fetch the latest changes from upstream + runCommand(`git fetch ${UPSTREAM_REMOTE}`); + + if (!options.direct) { + const tempBranch = generateBranchName("sync-upstream"); + + // Clean up any existing temp branch with same name (shouldn't exist due to timestamp) + deleteBranch(tempBranch); + + // Create new temp branch from current branch + runCommand(`git checkout -b ${tempBranch}`); + + try { + // Merge upstream changes into temp branch + console.info( + `Merging changes from ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH} into ${tempBranch}...` + ); + runCommand(`git merge ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}`); + + // Create and push PR + runCommand(`git push -u origin ${tempBranch}`); + + createPullRequest({ + title: "chore: sync upstream changes", + body: "Automated PR to sync changes from upstream repository", + base: CURRENT_BRANCH, + head: tempBranch, + labels: options.labels, + }); + } finally { + // Always switch back to original branch + runCommand(`git checkout ${CURRENT_BRANCH}`); + } + } else { + // Verify we're on the correct branch for direct merge + verifyCurrentBranch(CURRENT_BRANCH); + + // Merge upstream changes directly + console.info(`Merging changes from ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}...`); + runCommand(`git merge ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}`); + } + + console.info("Update from upstream completed successfully."); + } catch (error) { + console.error("Failed to sync upstream:", error instanceof Error ? error.message : error); + process.exit(1); + } } // CLI configuration const program = new Command() - .name("sync-upstream") - .description("Sync changes from upstream repository") - .option("-d, --direct", "Sync directly to current branch instead of creating PR", false) - .option("-l, --labels ", "Labels to add to the PR") - .action((options) => { - syncUpstream({ - direct: options.direct, - labels: options.labels, - }); - }); + .name("sync-upstream") + .description("Sync changes from upstream repository") + .option("-d, --direct", "Sync directly to current branch instead of creating PR", false) + .option("-l, --labels ", "Labels to add to the PR") + .action((options) => { + syncUpstream({ + direct: options.direct, + labels: options.labels, + }); + }); program.parse(process.argv); diff --git a/scripts/lighthouse/analyze.ts b/scripts/lighthouse/analyze.ts index 0cf78665..aa31e598 100644 --- a/scripts/lighthouse/analyze.ts +++ b/scripts/lighthouse/analyze.ts @@ -8,91 +8,91 @@ import fs from "node:fs/promises"; import path from "node:path"; interface NormalizedIssue { - url: string; - type: - | "image-missing-dimensions" - | "legacy-img-tag" - | "font-preload-missing" - | "html-lang-missing" - | "canonical-missing" - | "large-image-unoptimized" - | "unused-css" - | "tap-targets-small" - | "a11y-alt-missing" - | "seo-missing-meta"; - details?: Record; + url: string; + type: + | "image-missing-dimensions" + | "legacy-img-tag" + | "font-preload-missing" + | "html-lang-missing" + | "canonical-missing" + | "large-image-unoptimized" + | "unused-css" + | "tap-targets-small" + | "a11y-alt-missing" + | "seo-missing-meta"; + details?: Record; } interface LighthouseReportFile { - finalUrl?: string; - audits?: Record; - categories?: Record; + finalUrl?: string; + audits?: Record; + categories?: Record; } async function findReports(dir: string): Promise { - const entries = await fs.readdir(dir, { withFileTypes: true }); - const files: string[] = []; - for (const entry of entries) { - if (entry.isDirectory()) { - const candidate = path.join(dir, entry.name, "lighthouse.json"); - try { - await fs.access(candidate); - files.push(candidate); - } catch {} - } - } - return files; + const entries = await fs.readdir(dir, { withFileTypes: true }); + const files: string[] = []; + for (const entry of entries) { + if (entry.isDirectory()) { + const candidate = path.join(dir, entry.name, "lighthouse.json"); + try { + await fs.access(candidate); + files.push(candidate); + } catch {} + } + } + return files; } function pushIf(condition: boolean, issues: NormalizedIssue[], issue: NormalizedIssue): void { - if (condition) issues.push(issue); + if (condition) issues.push(issue); } async function analyzeReport(filePath: string): Promise { - const text = await fs.readFile(filePath, "utf8"); - const report: LighthouseReportFile = JSON.parse(text); - const url = report.finalUrl ?? ""; - const audits = report.audits ?? {}; - const issues: NormalizedIssue[] = []; + const text = await fs.readFile(filePath, "utf8"); + const report: LighthouseReportFile = JSON.parse(text); + const url = report.finalUrl ?? ""; + const audits = report.audits ?? {}; + const issues: NormalizedIssue[] = []; - const usesNextImage = false; // Determined later by codemods; keep conservative here + const usesNextImage = false; // Determined later by codemods; keep conservative here - const imageSizeAudit = audits["image-size-responsive"]; - const imageAspectAudit = audits["unsized-images"]; - const tapTargets = audits["tap-targets"]; - // const usesHttp2 = audits["uses-http2"]; // currently unused - const fontDisplay = audits["font-display"]; - const lang = audits["html-has-lang"]; - const canonical = audits["document-title"] && audits["meta-description"]; // heuristic for missing meta - const altText = audits["image-alt"]; + const imageSizeAudit = audits["image-size-responsive"]; + const imageAspectAudit = audits["unsized-images"]; + const tapTargets = audits["tap-targets"]; + // const usesHttp2 = audits["uses-http2"]; // currently unused + const fontDisplay = audits["font-display"]; + const lang = audits["html-has-lang"]; + const canonical = audits["document-title"] && audits["meta-description"]; // heuristic for missing meta + const altText = audits["image-alt"]; - pushIf(imageAspectAudit?.score === 0, issues, { url, type: "image-missing-dimensions" }); - pushIf(imageSizeAudit?.score === 0, issues, { url, type: "large-image-unoptimized" }); - pushIf(!usesNextImage, issues, { url, type: "legacy-img-tag" }); - pushIf(fontDisplay?.score === 0, issues, { url, type: "font-preload-missing" }); - pushIf(lang?.score === 0, issues, { url, type: "html-lang-missing" }); - pushIf(!canonical, issues, { url, type: "seo-missing-meta" }); - pushIf(tapTargets?.score === 0, issues, { url, type: "tap-targets-small" }); - pushIf(altText?.score === 0, issues, { url, type: "a11y-alt-missing" }); + pushIf(imageAspectAudit?.score === 0, issues, { url, type: "image-missing-dimensions" }); + pushIf(imageSizeAudit?.score === 0, issues, { url, type: "large-image-unoptimized" }); + pushIf(!usesNextImage, issues, { url, type: "legacy-img-tag" }); + pushIf(fontDisplay?.score === 0, issues, { url, type: "font-preload-missing" }); + pushIf(lang?.score === 0, issues, { url, type: "html-lang-missing" }); + pushIf(!canonical, issues, { url, type: "seo-missing-meta" }); + pushIf(tapTargets?.score === 0, issues, { url, type: "tap-targets-small" }); + pushIf(altText?.score === 0, issues, { url, type: "a11y-alt-missing" }); - return issues; + return issues; } async function main(): Promise { - const baseDir = process.env.LH_OUT_DIR ?? ".lighthouse"; - const reports = await findReports(baseDir); - const allIssues: NormalizedIssue[] = []; - for (const file of reports) { - const issues = await analyzeReport(file); - allIssues.push(...issues); - } - const out = path.join(baseDir, "normalized.json"); - await fs.writeFile(out, JSON.stringify(allIssues, null, 2), "utf8"); + const baseDir = process.env.LH_OUT_DIR ?? ".lighthouse"; + const reports = await findReports(baseDir); + const allIssues: NormalizedIssue[] = []; + for (const file of reports) { + const issues = await analyzeReport(file); + allIssues.push(...issues); + } + const out = path.join(baseDir, "normalized.json"); + await fs.writeFile(out, JSON.stringify(allIssues, null, 2), "utf8"); - console.log(`[analyze] Wrote normalized issues → ${out} (${allIssues.length})`); + console.log(`[analyze] Wrote normalized issues → ${out} (${allIssues.length})`); } main().catch((err) => { - console.error("[analyze] Failed:", err); - process.exit(1); + console.error("[analyze] Failed:", err); + process.exit(1); }); diff --git a/scripts/lighthouse/fixers.ts b/scripts/lighthouse/fixers.ts index 8f7f4213..9b6f02bd 100644 --- a/scripts/lighthouse/fixers.ts +++ b/scripts/lighthouse/fixers.ts @@ -9,39 +9,39 @@ import fs from "node:fs/promises"; import path from "node:path"; export interface FixTask { - name: string; - run: () => Promise<{ changedFiles: string[]; notes?: string[] }>; + name: string; + run: () => Promise<{ changedFiles: string[]; notes?: string[] }>; } function repoPath(...paths: string[]): string { - return path.join(process.cwd(), ...paths); + return path.join(process.cwd(), ...paths); } export const fixCanonicalInSiteConfig: FixTask = { - name: "ensure-canonical", - async run() { - const file = repoPath("src", "config", "site-config.ts"); - try { - const content = await fs.readFile(file, "utf8"); - if (content.includes("alternates") && content.includes("canonical")) { - return { changedFiles: [], notes: ["canonical already set in site-config.ts"] }; - } - // Conservative: do not auto-edit blindly; just log a note for now - return { changedFiles: [], notes: ["canonical appears configured; no change"] }; - } catch { - return { changedFiles: [], notes: ["site-config.ts not found; skipping canonical"] }; - } - }, + name: "ensure-canonical", + async run() { + const file = repoPath("src", "config", "site-config.ts"); + try { + const content = await fs.readFile(file, "utf8"); + if (content.includes("alternates") && content.includes("canonical")) { + return { changedFiles: [], notes: ["canonical already set in site-config.ts"] }; + } + // Conservative: do not auto-edit blindly; just log a note for now + return { changedFiles: [], notes: ["canonical appears configured; no change"] }; + } catch { + return { changedFiles: [], notes: ["site-config.ts not found; skipping canonical"] }; + } + }, }; export async function runFixers(): Promise<{ changedFiles: string[]; notes: string[] }> { - const tasks: FixTask[] = [fixCanonicalInSiteConfig]; - const changedFiles: string[] = []; - const notes: string[] = []; - for (const task of tasks) { - const res = await task.run(); - changedFiles.push(...res.changedFiles); - if (res.notes) notes.push(...res.notes); - } - return { changedFiles, notes }; + const tasks: FixTask[] = [fixCanonicalInSiteConfig]; + const changedFiles: string[] = []; + const notes: string[] = []; + for (const task of tasks) { + const res = await task.run(); + changedFiles.push(...res.changedFiles); + if (res.notes) notes.push(...res.notes); + } + return { changedFiles, notes }; } diff --git a/scripts/lighthouse/loop.ts b/scripts/lighthouse/loop.ts index 5c743f65..29441626 100644 --- a/scripts/lighthouse/loop.ts +++ b/scripts/lighthouse/loop.ts @@ -10,112 +10,112 @@ import path from "node:path"; import { runFixers } from "./fixers"; interface NormalizedIssue { - url: string; - type: string; + url: string; + type: string; } interface Thresholds { - performance: number; - accessibility: number; - seo: number; - bestPractices: number; + performance: number; + accessibility: number; + seo: number; + bestPractices: number; } function exec( - cmd: string, - args: string[], - env?: Record + cmd: string, + args: string[], + env?: Record ): Promise<{ code: number }> { - return new Promise((resolve, reject) => { - const child = spawn(cmd, args, { stdio: "inherit", env: { ...process.env, ...env } }); - child.on("close", (code) => resolve({ code: code ?? 1 })); - child.on("error", reject); - }); + return new Promise((resolve, reject) => { + const child = spawn(cmd, args, { stdio: "inherit", env: { ...process.env, ...env } }); + child.on("close", (code) => resolve({ code: code ?? 1 })); + child.on("error", reject); + }); } async function readLatestSummary(baseDir: string): Promise> { - const dir = baseDir; - const entries = await fs.readdir(dir); - const indexes = entries.filter((f) => f.startsWith("index-") && f.endsWith(".json")); - if (indexes.length === 0) return {}; - const latestEntry = indexes.sort().at(-1); - if (!latestEntry) return {}; - const latest = latestEntry; - const json = JSON.parse(await fs.readFile(path.join(dir, latest), "utf8")); - // For single URL run, take first categories - const first = Array.isArray(json) ? json[0] : undefined; - return first?.categories ?? {}; + const dir = baseDir; + const entries = await fs.readdir(dir); + const indexes = entries.filter((f) => f.startsWith("index-") && f.endsWith(".json")); + if (indexes.length === 0) return {}; + const latestEntry = indexes.sort().at(-1); + if (!latestEntry) return {}; + const latest = latestEntry; + const json = JSON.parse(await fs.readFile(path.join(dir, latest), "utf8")); + // For single URL run, take first categories + const first = Array.isArray(json) ? json[0] : undefined; + return first?.categories ?? {}; } async function readIssues(baseDir: string): Promise { - const file = path.join(baseDir, "normalized.json"); - try { - const text = await fs.readFile(file, "utf8"); - return JSON.parse(text); - } catch { - return []; - } + const file = path.join(baseDir, "normalized.json"); + try { + const text = await fs.readFile(file, "utf8"); + return JSON.parse(text); + } catch { + return []; + } } function categoriesMeetThresholds(categories: Record, t: Thresholds): boolean { - const performance = categories.performance ?? categories.performance; - const accessibility = categories.accessibility ?? categories.accessibility; - const seo = categories.seo ?? categories.seo; - const bestPractices = categories["best-practices"] ?? categories.bestPractices; - return ( - (performance ?? 0) >= t.performance && - (accessibility ?? 0) >= t.accessibility && - (seo ?? 0) >= t.seo && - (bestPractices ?? 0) >= t.bestPractices - ); + const performance = categories.performance ?? categories.performance; + const accessibility = categories.accessibility ?? categories.accessibility; + const seo = categories.seo ?? categories.seo; + const bestPractices = categories["best-practices"] ?? categories.bestPractices; + return ( + (performance ?? 0) >= t.performance && + (accessibility ?? 0) >= t.accessibility && + (seo ?? 0) >= t.seo && + (bestPractices ?? 0) >= t.bestPractices + ); } async function main(): Promise { - const urls = process.argv.slice(2).filter((a) => !a.startsWith("-")); - const LH_OUT_DIR = process.env.LH_OUT_DIR ?? ".lighthouse"; - const thresholds: Thresholds = { - performance: Number(process.env.LH_T_PERF ?? 90), - accessibility: Number(process.env.LH_T_A11Y ?? 95), - seo: Number(process.env.LH_T_SEO ?? 95), - bestPractices: Number(process.env.LH_T_BP ?? 95), - }; - const maxIters = Number(process.env.LH_MAX_ITERS ?? 2); - - for (let i = 0; i < maxIters; i++) { - console.log(`\n[loop] Iteration ${i + 1}/${maxIters}`); - - const runArgs = ["run", "scripts/lighthouse/run.ts", ...urls]; - const runRes = await exec("bun", runArgs, { LH_OUT_DIR }); - if (runRes.code !== 0) throw new Error("lighthouse run failed"); - - const analyzeRes = await exec("bun", ["run", "scripts/lighthouse/analyze.ts"], { LH_OUT_DIR }); - if (analyzeRes.code !== 0) throw new Error("analyze failed"); - - const categories = await readLatestSummary(LH_OUT_DIR); - - console.log("[loop] Categories:", categories); - if (categoriesMeetThresholds(categories, thresholds)) { - console.log("[loop] Thresholds met. Exiting."); - return; - } - - const issues = await readIssues(LH_OUT_DIR); - - console.log(`[loop] ${issues.length} normalized issues`); - const { changedFiles, notes } = await runFixers(); - if (notes.length) { - console.log(`[loop] Fixer notes:\n - ${notes.join("\n - ")}`); - } - if (changedFiles.length === 0) { - console.log("[loop] No changes from fixers; stopping."); - break; - } - - console.log(`[loop] Applied changes to ${changedFiles.length} files. Re-running audits...`); - } + const urls = process.argv.slice(2).filter((a) => !a.startsWith("-")); + const LH_OUT_DIR = process.env.LH_OUT_DIR ?? ".lighthouse"; + const thresholds: Thresholds = { + performance: Number(process.env.LH_T_PERF ?? 90), + accessibility: Number(process.env.LH_T_A11Y ?? 95), + seo: Number(process.env.LH_T_SEO ?? 95), + bestPractices: Number(process.env.LH_T_BP ?? 95), + }; + const maxIters = Number(process.env.LH_MAX_ITERS ?? 2); + + for (let i = 0; i < maxIters; i++) { + console.log(`\n[loop] Iteration ${i + 1}/${maxIters}`); + + const runArgs = ["run", "scripts/lighthouse/run.ts", ...urls]; + const runRes = await exec("bun", runArgs, { LH_OUT_DIR }); + if (runRes.code !== 0) throw new Error("lighthouse run failed"); + + const analyzeRes = await exec("bun", ["run", "scripts/lighthouse/analyze.ts"], { LH_OUT_DIR }); + if (analyzeRes.code !== 0) throw new Error("analyze failed"); + + const categories = await readLatestSummary(LH_OUT_DIR); + + console.log("[loop] Categories:", categories); + if (categoriesMeetThresholds(categories, thresholds)) { + console.log("[loop] Thresholds met. Exiting."); + return; + } + + const issues = await readIssues(LH_OUT_DIR); + + console.log(`[loop] ${issues.length} normalized issues`); + const { changedFiles, notes } = await runFixers(); + if (notes.length) { + console.log(`[loop] Fixer notes:\n - ${notes.join("\n - ")}`); + } + if (changedFiles.length === 0) { + console.log("[loop] No changes from fixers; stopping."); + break; + } + + console.log(`[loop] Applied changes to ${changedFiles.length} files. Re-running audits...`); + } } main().catch((err) => { - console.error("[loop] Failed:", err); - process.exit(1); + console.error("[loop] Failed:", err); + process.exit(1); }); diff --git a/scripts/lighthouse/run.ts b/scripts/lighthouse/run.ts index 49376b00..7574e150 100644 --- a/scripts/lighthouse/run.ts +++ b/scripts/lighthouse/run.ts @@ -14,104 +14,104 @@ import { launch } from "chrome-launcher"; import lighthouse from "lighthouse"; interface LighthouseRunResultSummary { - url: string; - outputDir: string; - categories: Record; - reportJsonPath: string; - reportHtmlPath: string; + url: string; + outputDir: string; + categories: Record; + reportJsonPath: string; + reportHtmlPath: string; } function getUrlsFromArgsOrEnv(): string[] { - const argvUrls = process.argv.slice(2).filter((arg) => !arg.startsWith("-")); - if (argvUrls.length > 0) return argvUrls; - const envList = - process.env.LH_URLS?.split(",") - .map((u) => u.trim()) - .filter(Boolean) ?? []; - if (envList.length > 0) return envList; - return ["http://localhost:3000/"]; + const argvUrls = process.argv.slice(2).filter((arg) => !arg.startsWith("-")); + if (argvUrls.length > 0) return argvUrls; + const envList = + process.env.LH_URLS?.split(",") + .map((u) => u.trim()) + .filter(Boolean) ?? []; + if (envList.length > 0) return envList; + return ["http://localhost:3000/"]; } function getPreset(): "desktop" | "mobile" { - const value = (process.env.LH_PRESET ?? "desktop").toLowerCase(); - return value === "mobile" ? "mobile" : "desktop"; + const value = (process.env.LH_PRESET ?? "desktop").toLowerCase(); + return value === "mobile" ? "mobile" : "desktop"; } function safeSlug(input: string): string { - return input - .replace(/^https?:\/\//, "") - .replace(/[^a-zA-Z0-9]+/g, "-") - .replace(/-+/g, "-") - .replace(/(^-|-$)/g, "") - .slice(0, 120); + return input + .replace(/^https?:\/\//, "") + .replace(/[^a-zA-Z0-9]+/g, "-") + .replace(/-+/g, "-") + .replace(/(^-|-$)/g, "") + .slice(0, 120); } async function ensureDir(dir: string): Promise { - await fs.mkdir(dir, { recursive: true }); + await fs.mkdir(dir, { recursive: true }); } async function runSingle( - url: string, - outBaseDir: string, - preset: "desktop" | "mobile" + url: string, + outBaseDir: string, + preset: "desktop" | "mobile" ): Promise { - const slug = safeSlug(url); - const outputDir = path.join(outBaseDir, `${slug}-${preset}`); - await ensureDir(outputDir); - - const chrome = await launch({ chromeFlags: ["--headless", "--no-sandbox"] }); - try { - const result = await lighthouse(url, { - port: chrome.port, - logLevel: "error", - output: ["json", "html"], - }); - - const lhr = result?.lhr; - const report = result?.report; - - // report[0] json, report[1] html - const jsonPath = path.join(outputDir, "lighthouse.json"); - const htmlPath = path.join(outputDir, "lighthouse.html"); - await fs.writeFile(jsonPath, String(report?.[0] ?? ""), "utf8"); - await fs.writeFile(htmlPath, String(report?.[1] ?? ""), "utf8"); - - const categories: Record = {}; - if (lhr?.categories) { - for (const [key, cat] of Object.entries(lhr.categories)) { - const score = (cat as any)?.score; - categories[key] = typeof score === "number" ? Math.round(score * 100) : null; - } - } - - return { url, outputDir, categories, reportJsonPath: jsonPath, reportHtmlPath: htmlPath }; - } finally { - await chrome.kill(); - } + const slug = safeSlug(url); + const outputDir = path.join(outBaseDir, `${slug}-${preset}`); + await ensureDir(outputDir); + + const chrome = await launch({ chromeFlags: ["--headless", "--no-sandbox"] }); + try { + const result = await lighthouse(url, { + port: chrome.port, + logLevel: "error", + output: ["json", "html"], + }); + + const lhr = result?.lhr; + const report = result?.report; + + // report[0] json, report[1] html + const jsonPath = path.join(outputDir, "lighthouse.json"); + const htmlPath = path.join(outputDir, "lighthouse.html"); + await fs.writeFile(jsonPath, String(report?.[0] ?? ""), "utf8"); + await fs.writeFile(htmlPath, String(report?.[1] ?? ""), "utf8"); + + const categories: Record = {}; + if (lhr?.categories) { + for (const [key, cat] of Object.entries(lhr.categories)) { + const score = (cat as any)?.score; + categories[key] = typeof score === "number" ? Math.round(score * 100) : null; + } + } + + return { url, outputDir, categories, reportJsonPath: jsonPath, reportHtmlPath: htmlPath }; + } finally { + await chrome.kill(); + } } async function main(): Promise { - const urls = getUrlsFromArgsOrEnv(); - const preset = getPreset(); - const outDir = process.env.LH_OUT_DIR ?? ".lighthouse"; - await ensureDir(outDir); + const urls = getUrlsFromArgsOrEnv(); + const preset = getPreset(); + const outDir = process.env.LH_OUT_DIR ?? ".lighthouse"; + await ensureDir(outDir); - const results: LighthouseRunResultSummary[] = []; - for (const url of urls) { - console.log(`[lighthouse] Running on ${url} (${preset})`); - const summary = await runSingle(url, outDir, preset); - results.push(summary); + const results: LighthouseRunResultSummary[] = []; + for (const url of urls) { + console.log(`[lighthouse] Running on ${url} (${preset})`); + const summary = await runSingle(url, outDir, preset); + results.push(summary); - console.log(`[lighthouse] Done ${url} →`, summary.categories); - } + console.log(`[lighthouse] Done ${url} →`, summary.categories); + } - const indexPath = path.join(outDir, `index-${Date.now()}.json`); - await fs.writeFile(indexPath, JSON.stringify(results, null, 2), "utf8"); + const indexPath = path.join(outDir, `index-${Date.now()}.json`); + await fs.writeFile(indexPath, JSON.stringify(results, null, 2), "utf8"); - console.log(`[lighthouse] Wrote summary index → ${indexPath}`); + console.log(`[lighthouse] Wrote summary index → ${indexPath}`); } main().catch((error) => { - console.error("[lighthouse] Failed:", error); - process.exit(1); + console.error("[lighthouse] Failed:", error); + process.exit(1); }); diff --git a/scripts/nextjs-list-pages.ts b/scripts/nextjs-list-pages.ts index 9c23e16e..6d28ab95 100644 --- a/scripts/nextjs-list-pages.ts +++ b/scripts/nextjs-list-pages.ts @@ -15,46 +15,46 @@ const serverDir = path.join(nextDir, "server"); // pages or app live under here let ssgRoutes = []; let dynamicSsgRoutes = []; try { - const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); - ssgRoutes = Object.keys(manifest.routes); - dynamicSsgRoutes = Object.keys(manifest.dynamicRoutes); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + ssgRoutes = Object.keys(manifest.routes); + dynamicSsgRoutes = Object.keys(manifest.dynamicRoutes); } catch (err) { - console.error("❌ Can’t read prerender‑manifest.json – did you run `next build`?"); - process.exit(1); + console.error("❌ Can’t read prerender‑manifest.json – did you run `next build`?"); + process.exit(1); } // 2️⃣ Walk the .next/server directory for emitted HTML files function walk(dir: string, acc: string[] = []): string[] { - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) walk(full, acc); - else if (entry.isFile() && entry.name.endsWith(".html")) acc.push(full); - } - return acc; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) walk(full, acc); + else if (entry.isFile() && entry.name.endsWith(".html")) acc.push(full); + } + return acc; } const htmlFiles = walk(serverDir) - // turn ".next/server/pages/about.html" → “/about” - .map((f: string) => { - const rel = f.replace(serverDir, "").replace(/\\/g, "/"); - return rel.replace(/\/index\.html$/, "/").replace(/\.html$/, "") || "/"; - }) - .sort(); + // turn ".next/server/pages/about.html" → “/about” + .map((f: string) => { + const rel = f.replace(serverDir, "").replace(/\\/g, "/"); + return rel.replace(/\/index\.html$/, "/").replace(/\.html$/, "") || "/"; + }) + .sort(); console.log("\n📄 Static HTML files emitted:"); htmlFiles.forEach((p) => { - console.log(" •", p); + console.log(" •", p); }); console.log("\n📦 SSG routes from prerender‑manifest.json:"); ssgRoutes.forEach((p) => { - console.log(" •", p); + console.log(" •", p); }); if (dynamicSsgRoutes.length) { - console.log("\n📦 Dynamic SSG routes (ISR/fallback):"); - dynamicSsgRoutes.forEach((p) => { - console.log(" •", p); - }); + console.log("\n📦 Dynamic SSG routes (ISR/fallback):"); + dynamicSsgRoutes.forEach((p) => { + console.log(" •", p); + }); } console.log("\n✅ Done."); diff --git a/scripts/patch-payload-imports.js b/scripts/patch-payload-imports.js index 026a49be..fc9d1aa4 100644 --- a/scripts/patch-payload-imports.js +++ b/scripts/patch-payload-imports.js @@ -23,37 +23,27 @@ const dirsToPath = [ ]; // File extensions that Node.js ESM cannot handle -const problematicExtensions = - "css|scss|svg|png|jpg|jpeg|gif|webp|ico|woff|woff2|eot|ttf|otf"; +const problematicExtensions = "css|scss|svg|png|jpg|jpeg|gif|webp|ico|woff|woff2|eot|ttf|otf"; // Patterns to match problematic imports (both formatted and minified) const problematicImportPatterns = [ // Style imports: import './something.scss'; - new RegExp( - `import\\s+['"][^'"]*\\.(${problematicExtensions})['"];?\\n?`, - "g", - ), + new RegExp(`import\\s+['"][^'"]*\\.(${problematicExtensions})['"];?\\n?`, "g"), // Minified style imports: import"./something.scss"; new RegExp(`import["'][^"']*\\.(${problematicExtensions})["'];?`, "g"), // Asset imports with variable: import something from './file.svg'; - new RegExp( - `import\\s+(\\w+)\\s+from\\s+['"][^'"]*\\.(${problematicExtensions})['"];?\\n?`, - "g", - ), + new RegExp(`import\\s+(\\w+)\\s+from\\s+['"][^'"]*\\.(${problematicExtensions})['"];?\\n?`, "g"), // Minified asset imports: import a from"./file.svg"; - new RegExp( - `import\\s*(\\w+)\\s*from["'][^"']*\\.(${problematicExtensions})["'];?`, - "g", - ), + new RegExp(`import\\s*(\\w+)\\s*from["'][^"']*\\.(${problematicExtensions})["'];?`, "g"), // Re-export: export { default as name } from './file.svg'; new RegExp( `export\\s*\\{\\s*default\\s+as\\s+(\\w+)\\s*\\}\\s*from\\s*['"][^'"]*\\.(${problematicExtensions})['"];?\\n?`, - "g", + "g" ), // Minified re-export: export{default as name}from"./file.svg"; new RegExp( `export\\s*\\{\\s*default\\s+as\\s+(\\w+)\\s*\\}\\s*from\\s*["'][^"']*\\.(${problematicExtensions})["'];?`, - "g", + "g" ), ]; @@ -122,14 +112,10 @@ for (const relDir of dirsToPath) { } } -console.log( - `[patch] Scanned ${filesScanned} files, patched ${patchedCount} file(s)`, -); +console.log(`[patch] Scanned ${filesScanned} files, patched ${patchedCount} file(s)`); if (patchedCount > 0) { - console.log( - "[patch] Successfully removed problematic imports from Payload packages", - ); + console.log("[patch] Successfully removed problematic imports from Payload packages"); } else { console.log("[patch] No problematic imports found to patch"); } diff --git a/scripts/post-deploy.ts b/scripts/post-deploy.ts index 13358153..20e866eb 100644 --- a/scripts/post-deploy.ts +++ b/scripts/post-deploy.ts @@ -1,19 +1,19 @@ import { execSync } from "child_process"; async function postDeploy() { - console.log("🚀 Running post-deployment tasks..."); + console.log("🚀 Running post-deployment tasks..."); - try { - // Sync database - console.log("🔄 Syncing database..."); - execSync("bun run db:sync", { stdio: "inherit" }); + try { + // Sync database + console.log("🔄 Syncing database..."); + execSync("bun run db:sync", { stdio: "inherit" }); - console.log("✨ Post-deployment tasks completed successfully!"); - process.exit(0); - } catch (error) { - console.error("❌ Error in post-deployment tasks:", error); - process.exit(1); - } + console.log("✨ Post-deployment tasks completed successfully!"); + process.exit(0); + } catch (error) { + console.error("❌ Error in post-deployment tasks:", error); + process.exit(1); + } } void postDeploy(); diff --git a/scripts/rebrand.ts b/scripts/rebrand.ts index 5759a8a5..efaf2fc5 100755 --- a/scripts/rebrand.ts +++ b/scripts/rebrand.ts @@ -24,17 +24,17 @@ import readline from "readline"; // Create readline interface for prompting const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, + input: process.stdin, + output: process.stdout, }); // Helper function to prompt for input const prompt = (question: string, defaultValue?: string): Promise => { - return new Promise((resolve) => { - rl.question(`${question}${defaultValue ? ` (default: ${defaultValue})` : ""}: `, (answer) => { - resolve(answer || defaultValue || ""); - }); - }); + return new Promise((resolve) => { + rl.question(`${question}${defaultValue ? ` (default: ${defaultValue})` : ""}: `, (answer) => { + resolve(answer || defaultValue || ""); + }); + }); }; // Parse command line arguments @@ -43,16 +43,16 @@ const argMap: Record = {}; const flags: Record = {}; for (let i = 0; i < args.length; i++) { - if (args[i] && args[i]!.startsWith("--")) { - const arg = args[i]!.substring(2); - // Check if it's a flag (no value) - if (i + 1 >= args.length || (args[i + 1] && args[i + 1]!.startsWith("--"))) { - flags[arg] = true; - } else if (args[i + 1]) { - argMap[arg] = args[i + 1]!; - i++; - } - } + if (args[i] && args[i]!.startsWith("--")) { + const arg = args[i]!.substring(2); + // Check if it's a flag (no value) + if (i + 1 >= args.length || (args[i + 1] && args[i + 1]!.startsWith("--"))) { + flags[arg] = true; + } else if (args[i + 1]) { + argMap[arg] = args[i + 1]!; + i++; + } + } } // Check for dry run flag @@ -60,105 +60,105 @@ const isDryRun = flags["dry-run"] || false; // Main function async function main() { - console.log("🚀 Rebranding Script"); - console.log("===================="); - console.log("This script will update your site configuration with new branding information."); - if (isDryRun) { - console.log("\n⚠️ DRY RUN MODE: No files will be modified"); - } - console.log(""); - - // Get essential branding information - const projectName = argMap.name || (await prompt("Project Name", "Shipkit")); - const projectSlug = - argMap.slug || (await prompt("Project Slug", projectName.toLowerCase().replace(/\s+/g, "-"))); - const domain = argMap.domain || (await prompt("Domain", `${projectSlug}.com`)); - - // Get creator information (optional) - const creatorName = argMap["creator-name"] || (await prompt("Your Name (optional)", "")); - - // Derive other values from the essential information - const githubOrg = `${projectSlug}-org`; - const githubRepo = projectSlug; - const creatorUsername = creatorName ? creatorName.toLowerCase().replace(/\s+/g, "") : projectSlug; - const creatorEmail = `hello@${domain}`; - const creatorDomain = creatorName ? `${creatorUsername}.com` : domain; - const creatorTwitter = creatorUsername; - const databaseName = projectSlug; - const vercelProjectName = `${projectSlug}-app`; - - // Product names - consolidated tiers: Bones (free) and Brains (paid) - const bonesName = "Bones"; - const brainsName = "Brains"; - - // Update site-config.ts - console.log("\nUpdating site configuration..."); - - const siteConfigPath = path.join(process.cwd(), "src", "config", "site-config.ts"); - let siteConfig = fs.readFileSync(siteConfigPath, "utf8"); - - // We need to identify the export declaration and handle the branding info correctly - // Find where the actual siteConfig object starts - const siteConfigStartPattern = /export const siteConfig: SiteConfig = \{/; - const siteConfigMatch = siteConfigStartPattern.exec(siteConfig); - - if (!siteConfigMatch) { - console.error("Could not find siteConfig export in site-config.ts"); - process.exit(1); - } - - const siteConfigStart = siteConfigMatch.index; - const beforeConfig = siteConfig.substring(0, siteConfigStart + siteConfigMatch[0].length); - let afterConfig = siteConfig.substring(siteConfigStart + siteConfigMatch[0].length); - - // Handle replacement of individual keys in the object - // Name - afterConfig = afterConfig.replace(/\n\s*name: "([^"]+)"/, `\n\tname: "${projectName}"`); - - // URL - afterConfig = afterConfig.replace(/\n\s*url: "([^"]+)"/, `\n\turl: "https://${domain}"`); - - // OG Image - afterConfig = afterConfig.replace( - /\n\s*ogImage: "([^"]+)"/, - `\n\togImage: "https://${domain}/og"` - ); - - // Description - keep original but update if needed - - // Branding section - const brandingPattern = /\n\s*branding: \{[\s\S]*?\n\s*\},/; - const newBranding = ` + console.log("🚀 Rebranding Script"); + console.log("===================="); + console.log("This script will update your site configuration with new branding information."); + if (isDryRun) { + console.log("\n⚠️ DRY RUN MODE: No files will be modified"); + } + console.log(""); + + // Get essential branding information + const projectName = argMap.name || (await prompt("Project Name", "Shipkit")); + const projectSlug = + argMap.slug || (await prompt("Project Slug", projectName.toLowerCase().replace(/\s+/g, "-"))); + const domain = argMap.domain || (await prompt("Domain", `${projectSlug}.com`)); + + // Get creator information (optional) + const creatorName = argMap["creator-name"] || (await prompt("Your Name (optional)", "")); + + // Derive other values from the essential information + const githubOrg = `${projectSlug}-org`; + const githubRepo = projectSlug; + const creatorUsername = creatorName ? creatorName.toLowerCase().replace(/\s+/g, "") : projectSlug; + const creatorEmail = `hello@${domain}`; + const creatorDomain = creatorName ? `${creatorUsername}.com` : domain; + const creatorTwitter = creatorUsername; + const databaseName = projectSlug; + const vercelProjectName = `${projectSlug}-app`; + + // Product names - consolidated tiers: Bones (free) and Brains (paid) + const bonesName = "Bones"; + const brainsName = "Brains"; + + // Update site-config.ts + console.log("\nUpdating site configuration..."); + + const siteConfigPath = path.join(process.cwd(), "src", "config", "site-config.ts"); + let siteConfig = fs.readFileSync(siteConfigPath, "utf8"); + + // We need to identify the export declaration and handle the branding info correctly + // Find where the actual siteConfig object starts + const siteConfigStartPattern = /export const siteConfig: SiteConfig = \{/; + const siteConfigMatch = siteConfigStartPattern.exec(siteConfig); + + if (!siteConfigMatch) { + console.error("Could not find siteConfig export in site-config.ts"); + process.exit(1); + } + + const siteConfigStart = siteConfigMatch.index; + const beforeConfig = siteConfig.substring(0, siteConfigStart + siteConfigMatch[0].length); + let afterConfig = siteConfig.substring(siteConfigStart + siteConfigMatch[0].length); + + // Handle replacement of individual keys in the object + // Name + afterConfig = afterConfig.replace(/\n\s*name: "([^"]+)"/, `\n\tname: "${projectName}"`); + + // URL + afterConfig = afterConfig.replace(/\n\s*url: "([^"]+)"/, `\n\turl: "https://${domain}"`); + + // OG Image + afterConfig = afterConfig.replace( + /\n\s*ogImage: "([^"]+)"/, + `\n\togImage: "https://${domain}/og"` + ); + + // Description - keep original but update if needed + + // Branding section + const brandingPattern = /\n\s*branding: \{[\s\S]*?\n\s*\},/; + const newBranding = ` branding: {\n\t\tprojectName: "${projectName}",\n\t\tprojectSlug: "${projectSlug}",\n\t\tproductNames: {\n\t\t\tbones: "${bonesName}",\n\t\t\tbrains: "${brainsName}",\n\t\t\tmain: "${projectName}",\n\t\t},\n\t\tdomain: "${domain}",\n\t\tprotocol: "web+${projectSlug}",\n\t\tgithubOrg: "${githubOrg}",\n\t\tgithubRepo: "${githubRepo}",\n\t\tvercelProjectName: "${vercelProjectName}",\n\t\tdatabaseName: "${databaseName}",\n },`; - afterConfig = afterConfig.replace(brandingPattern, newBranding); + afterConfig = afterConfig.replace(brandingPattern, newBranding); - // Repository section - const repoPattern = /\n\s*repo: \{[\s\S]*?\n\s*\},/; - const newRepo = ` + // Repository section + const repoPattern = /\n\s*repo: \{[\s\S]*?\n\s*\},/; + const newRepo = ` repo: {\n\t\towner: "${githubOrg}",\n\t\tname: "${githubRepo}",\n\t\turl: "https://github.com/${githubOrg}/${githubRepo}",\n\t\tformat: {\n\t\t\tclone: () => acktickhttps://github.com/${githubOrg}/${githubRepo}.gitacktick,\n\t\t\tssh: () => acktickgit@github.com:${githubOrg}/${githubRepo}.gitacktick,\n\t\t\t\n\t\t} },`; - afterConfig = afterConfig.replace(repoPattern, newRepo); + afterConfig = afterConfig.replace(repoPattern, newRepo); - // Email section - const emailPattern = /\n\s*email: \{[\s\S]*?\n\s*\},/; - const newEmail = ` + // Email section + const emailPattern = /\n\s*email: \{[\s\S]*?\n\s*\},/; + const newEmail = ` email: {\n\t\tsupport: "support@${domain}",\n\t\tteam: "team@${domain}",\n\t\tnoreply: "noreply@${domain}",\n\t\tdomain: "${domain}",\n\t\tlegal: "legal@${domain}",\n\t\tprivacy: "privacy@${domain}",\n\t\tformat: (type) => siteConfig.email[type], },`; - afterConfig = afterConfig.replace(emailPattern, newEmail); + afterConfig = afterConfig.replace(emailPattern, newEmail); - // Creator section - const creatorPattern = /\n\s*creator: \{[\s\S]*?\n\s*\},/; - const newCreator = ` + // Creator section + const creatorPattern = /\n\s*creator: \{[\s\S]*?\n\s*\},/; + const newCreator = ` creator: {\n\t\tname: "${creatorUsername}",\n\t\temail: "${creatorEmail}",\n\t\turl: "https://${creatorDomain}",\n\t\ttwitter: "@${creatorTwitter}",\n\t\ttwitter_handle: "${creatorTwitter}",\n\t\tdomain: "${creatorDomain}",\n\t\tfullName: "${creatorName || `${projectName} Team`}",\n\t\trole: "Developer",\n\t\tavatar: "https://avatars.githubusercontent.com/u/1311301?v=4",\n\t\tlocation: "San Francisco, CA",\n\t\tbio: "Creator and developer.",\n },`; - afterConfig = afterConfig.replace(creatorPattern, newCreator); + afterConfig = afterConfig.replace(creatorPattern, newCreator); - // Metadata keywords - const keywordsPattern = /\n\s*keywords: \[\s*["\s\S]*?\n\s*\]/; - const newKeywords = ` + // Metadata keywords + const keywordsPattern = /\n\s*keywords: \[\s*["\s\S]*?\n\s*\]/; + const newKeywords = ` keywords: [ "Next.js", "React", @@ -169,82 +169,82 @@ async function main() { "UI Components", ],`; - afterConfig = afterConfig.replace(keywordsPattern, newKeywords); - - // Put the file back together - siteConfig = beforeConfig + afterConfig; - - // Read .env.example and package.json - const envExamplePath = path.join(process.cwd(), ".env.example"); - let envExample = fs.readFileSync(envExamplePath, "utf8"); - - envExample = envExample.replace( - /DATABASE_URL="postgresql:\/\/postgres:password@localhost:5432\/([^"]+)"/, - `DATABASE_URL="postgresql://postgres:password@localhost:5432/${databaseName}"` - ); - - const packageJsonPath = path.join(process.cwd(), "package.json"); - let packageJson = fs.readFileSync(packageJsonPath, "utf8"); - - packageJson = packageJson.replace(/"name":\s*"([^"]+)"/, `"name": "${projectSlug}"`); - - // If dry run, just show what would be changed - if (isDryRun) { - console.log("\n📝 Changes that would be made:"); - console.log("\n1. src/config/site-config.ts:"); - console.log(` - Project name: Shipkit -> ${projectName}`); - console.log(` - Domain: shipkit.io -> ${domain}`); - console.log(` - GitHub: lacymorrow/shipkit -> ${githubOrg}/${githubRepo}`); - console.log(` - Creator: Lacy Morrow -> ${creatorName || `${projectName} Team`}`); - - console.log("\n2. .env.example:"); - console.log(` - Database name: shipkit -> ${databaseName}`); - - console.log("\n3. package.json:"); - console.log(` - Package name: ship-kit -> ${projectSlug}`); - - console.log("\n⚠️ No files were modified (dry run)"); - } else { - // Write updated files - fs.writeFileSync(siteConfigPath, siteConfig); - console.log("✅ Updated src/config/site-config.ts"); - - fs.writeFileSync(envExamplePath, envExample); - console.log("✅ Updated .env.example"); - - fs.writeFileSync(packageJsonPath, packageJson); - console.log("✅ Updated package.json"); - - // Run formatter - console.log("\nFormatting files..."); - try { - execSync('bunx eslint --fix src/config/site-config.ts || echo "Formatting skipped"', { - stdio: "inherit", - }); - } catch (error) { - console.error("Error formatting files:", error); - } - } - - console.log("\n✅ Rebranding complete!"); - console.log( - `Your project has been ${isDryRun ? "prepared to be" : ""} rebranded to "${projectName}".` - ); - - if (!isDryRun) { - console.log("\nNext steps:"); - console.log("1. Review the changes in src/config/site-config.ts"); - console.log("2. Update your .env file with the new database name"); - console.log("3. Restart your development server"); - } else { - console.log("\nTo apply these changes, run the script without the --dry-run flag."); - } - - rl.close(); + afterConfig = afterConfig.replace(keywordsPattern, newKeywords); + + // Put the file back together + siteConfig = beforeConfig + afterConfig; + + // Read .env.example and package.json + const envExamplePath = path.join(process.cwd(), ".env.example"); + let envExample = fs.readFileSync(envExamplePath, "utf8"); + + envExample = envExample.replace( + /DATABASE_URL="postgresql:\/\/postgres:password@localhost:5432\/([^"]+)"/, + `DATABASE_URL="postgresql://postgres:password@localhost:5432/${databaseName}"` + ); + + const packageJsonPath = path.join(process.cwd(), "package.json"); + let packageJson = fs.readFileSync(packageJsonPath, "utf8"); + + packageJson = packageJson.replace(/"name":\s*"([^"]+)"/, `"name": "${projectSlug}"`); + + // If dry run, just show what would be changed + if (isDryRun) { + console.log("\n📝 Changes that would be made:"); + console.log("\n1. src/config/site-config.ts:"); + console.log(` - Project name: Shipkit -> ${projectName}`); + console.log(` - Domain: shipkit.io -> ${domain}`); + console.log(` - GitHub: lacymorrow/shipkit -> ${githubOrg}/${githubRepo}`); + console.log(` - Creator: Lacy Morrow -> ${creatorName || `${projectName} Team`}`); + + console.log("\n2. .env.example:"); + console.log(` - Database name: shipkit -> ${databaseName}`); + + console.log("\n3. package.json:"); + console.log(` - Package name: ship-kit -> ${projectSlug}`); + + console.log("\n⚠️ No files were modified (dry run)"); + } else { + // Write updated files + fs.writeFileSync(siteConfigPath, siteConfig); + console.log("✅ Updated src/config/site-config.ts"); + + fs.writeFileSync(envExamplePath, envExample); + console.log("✅ Updated .env.example"); + + fs.writeFileSync(packageJsonPath, packageJson); + console.log("✅ Updated package.json"); + + // Run formatter + console.log("\nFormatting files..."); + try { + execSync('bunx eslint --fix src/config/site-config.ts || echo "Formatting skipped"', { + stdio: "inherit", + }); + } catch (error) { + console.error("Error formatting files:", error); + } + } + + console.log("\n✅ Rebranding complete!"); + console.log( + `Your project has been ${isDryRun ? "prepared to be" : ""} rebranded to "${projectName}".` + ); + + if (!isDryRun) { + console.log("\nNext steps:"); + console.log("1. Review the changes in src/config/site-config.ts"); + console.log("2. Update your .env file with the new database name"); + console.log("3. Restart your development server"); + } else { + console.log("\nTo apply these changes, run the script without the --dry-run flag."); + } + + rl.close(); } main().catch((error) => { - console.error("Error:", error); - rl.close(); - process.exit(1); + console.error("Error:", error); + rl.close(); + process.exit(1); }); diff --git a/scripts/setup-upstream.ts b/scripts/setup-upstream.ts index 4a8bbb0a..1724b09c 100644 --- a/scripts/setup-upstream.ts +++ b/scripts/setup-upstream.ts @@ -18,100 +18,99 @@ const UPSTREAM_REMOTE = "upstream"; // lacymorrow/shipkit) get their upstream remote set by create-shipkit-app, or set // UPSTREAM_REPO_URL to override. const UPSTREAM_REPOS: string[] = process.env.UPSTREAM_REPO_URL - ? [process.env.UPSTREAM_REPO_URL] - : ["https://github.com/shipkit-io/bones.git"]; + ? [process.env.UPSTREAM_REPO_URL] + : ["https://github.com/shipkit-io/bones.git"]; /** * Run a command silently and return success status */ function runSilent(command: string): boolean { - try { - execSync(command, { stdio: "ignore" }); - return true; - } catch { - return false; - } + try { + execSync(command, { stdio: "ignore" }); + return true; + } catch { + return false; + } } /** * Run a command and return output */ function runCommand(command: string): string { - try { - return execSync(command, { encoding: "utf-8" }).trim(); - } catch { - return ""; - } + try { + return execSync(command, { encoding: "utf-8" }).trim(); + } catch { + return ""; + } } /** * Check if we're in a git repository */ function isGitRepo(): boolean { - return runSilent("git rev-parse --git-dir"); + return runSilent("git rev-parse --git-dir"); } /** * Check if the upstream remote already exists */ function hasUpstreamRemote(): boolean { - return runSilent(`git remote get-url ${UPSTREAM_REMOTE}`); + return runSilent(`git remote get-url ${UPSTREAM_REMOTE}`); } /** * Check if a remote repository is accessible */ function canAccessRepo(url: string): boolean { - return runSilent(`git ls-remote ${url}`); + return runSilent(`git ls-remote ${url}`); } /** * Get the first accessible upstream URL */ function getAccessibleUpstreamUrl(): string | null { - for (const url of UPSTREAM_REPOS) { - if (canAccessRepo(url)) { - return url; - } - } - return null; + for (const url of UPSTREAM_REPOS) { + if (canAccessRepo(url)) { + return url; + } + } + return null; } /** * Main setup function */ function setup(): void { - // Skip if not in a git repository - if (!isGitRepo()) { - return; - } - - // Skip if upstream already exists - if (hasUpstreamRemote()) { - const existingUrl = runCommand(`git remote get-url ${UPSTREAM_REMOTE}`); - // Only log if running interactively (not during npm install) - if (process.stdout.isTTY) { - console.info(`✓ Upstream remote already configured: ${existingUrl}`); - } - return; - } - - // Find accessible upstream - const upstreamUrl = getAccessibleUpstreamUrl(); - - if (!upstreamUrl) { - // Silently skip if no upstream is accessible - // (user might not have network access during install) - return; - } - - // Add the upstream remote - if (runSilent(`git remote add ${UPSTREAM_REMOTE} ${upstreamUrl}`)) { - console.info(`✓ Added upstream remote: ${upstreamUrl}`); - console.info(" Run 'bun run upstream:pull' to sync changes from upstream"); - } + // Skip if not in a git repository + if (!isGitRepo()) { + return; + } + + // Skip if upstream already exists + if (hasUpstreamRemote()) { + const existingUrl = runCommand(`git remote get-url ${UPSTREAM_REMOTE}`); + // Only log if running interactively (not during npm install) + if (process.stdout.isTTY) { + console.info(`✓ Upstream remote already configured: ${existingUrl}`); + } + return; + } + + // Find accessible upstream + const upstreamUrl = getAccessibleUpstreamUrl(); + + if (!upstreamUrl) { + // Silently skip if no upstream is accessible + // (user might not have network access during install) + return; + } + + // Add the upstream remote + if (runSilent(`git remote add ${UPSTREAM_REMOTE} ${upstreamUrl}`)) { + console.info(`✓ Added upstream remote: ${upstreamUrl}`); + console.info(" Run 'bun run upstream:pull' to sync changes from upstream"); + } } // Run setup setup(); - diff --git a/scripts/smoke-test.ts b/scripts/smoke-test.ts index 468c3eba..310f1398 100644 --- a/scripts/smoke-test.ts +++ b/scripts/smoke-test.ts @@ -22,8 +22,8 @@ import http from "node:http"; // Allow opt-out for environments where smoke testing isn't desired if (process.env.SKIP_SMOKE_TEST === "true") { - console.log("⏭️ Smoke test skipped (SKIP_SMOKE_TEST=true)"); - process.exit(0); + console.log("⏭️ Smoke test skipped (SKIP_SMOKE_TEST=true)"); + process.exit(0); } const PORT = Number(process.env.SMOKE_TEST_PORT) || 3847; @@ -33,91 +33,91 @@ const DEFAULT_ROUTES = ["/"]; const routes = process.argv.slice(2).length > 0 ? process.argv.slice(2) : DEFAULT_ROUTES; function waitForServer(port: number, timeoutMs: number): Promise { - const start = Date.now(); - return new Promise((resolve, reject) => { - const check = () => { - const req = http.get(`http://localhost:${port}/`, (res) => { - res.resume(); - resolve(); - }); - req.on("error", () => { - if (Date.now() - start > timeoutMs) { - reject(new Error(`Server did not start within ${timeoutMs}ms`)); - } else { - setTimeout(check, 300); - } - }); - req.end(); - }; - check(); - }); + const start = Date.now(); + return new Promise((resolve, reject) => { + const check = () => { + const req = http.get(`http://localhost:${port}/`, (res) => { + res.resume(); + resolve(); + }); + req.on("error", () => { + if (Date.now() - start > timeoutMs) { + reject(new Error(`Server did not start within ${timeoutMs}ms`)); + } else { + setTimeout(check, 300); + } + }); + req.end(); + }; + check(); + }); } function checkRoute(port: number, route: string): Promise<{ route: string; status: number }> { - return new Promise((resolve, reject) => { - const req = http.get(`http://localhost:${port}${route}`, (res) => { - res.resume(); - resolve({ route, status: res.statusCode ?? 0 }); - }); - req.on("error", (err) => reject(err)); - req.setTimeout(10_000, () => { - req.destroy(); - reject(new Error(`Request to ${route} timed out`)); - }); - req.end(); - }); + return new Promise((resolve, reject) => { + const req = http.get(`http://localhost:${port}${route}`, (res) => { + res.resume(); + resolve({ route, status: res.statusCode ?? 0 }); + }); + req.on("error", (err) => reject(err)); + req.setTimeout(10_000, () => { + req.destroy(); + reject(new Error(`Request to ${route} timed out`)); + }); + req.end(); + }); } async function main() { - console.log(`\n🔥 Smoke test: starting server on port ${PORT}...`); - - const server: ChildProcess = spawn("npx", ["next", "start", "-p", String(PORT)], { - stdio: "pipe", - env: { ...process.env, NODE_ENV: "production" }, - }); - - // Forward server stderr for debugging - server.stderr?.on("data", (data: Buffer) => { - const msg = data.toString().trim(); - if (msg) console.error(` [server] ${msg}`); - }); - - const cleanup = () => { - server.kill("SIGTERM"); - // Force kill after 3s if it doesn't exit - setTimeout(() => server.kill("SIGKILL"), 3000); - }; - - try { - await waitForServer(PORT, TIMEOUT_MS); - console.log(`✅ Server is up on port ${PORT}`); - - const results = await Promise.all(routes.map((r) => checkRoute(PORT, r))); - let failed = false; - - for (const { route, status } of results) { - if (status >= 500) { - console.error(`❌ ${route} → ${status}`); - failed = true; - } else { - console.log(`✅ ${route} → ${status}`); - } - } - - cleanup(); - - if (failed) { - console.error("\n💥 Smoke test FAILED — server returned 5xx on one or more routes.\n"); - process.exit(1); - } - - console.log("\n🎉 Smoke test passed.\n"); - process.exit(0); - } catch (err) { - cleanup(); - console.error(`\n💥 Smoke test error: ${err}`); - process.exit(1); - } + console.log(`\n🔥 Smoke test: starting server on port ${PORT}...`); + + const server: ChildProcess = spawn("npx", ["next", "start", "-p", String(PORT)], { + stdio: "pipe", + env: { ...process.env, NODE_ENV: "production" }, + }); + + // Forward server stderr for debugging + server.stderr?.on("data", (data: Buffer) => { + const msg = data.toString().trim(); + if (msg) console.error(` [server] ${msg}`); + }); + + const cleanup = () => { + server.kill("SIGTERM"); + // Force kill after 3s if it doesn't exit + setTimeout(() => server.kill("SIGKILL"), 3000); + }; + + try { + await waitForServer(PORT, TIMEOUT_MS); + console.log(`✅ Server is up on port ${PORT}`); + + const results = await Promise.all(routes.map((r) => checkRoute(PORT, r))); + let failed = false; + + for (const { route, status } of results) { + if (status >= 500) { + console.error(`❌ ${route} → ${status}`); + failed = true; + } else { + console.log(`✅ ${route} → ${status}`); + } + } + + cleanup(); + + if (failed) { + console.error("\n💥 Smoke test FAILED — server returned 5xx on one or more routes.\n"); + process.exit(1); + } + + console.log("\n🎉 Smoke test passed.\n"); + process.exit(0); + } catch (err) { + cleanup(); + console.error(`\n💥 Smoke test error: ${err}`); + process.exit(1); + } } main(); diff --git a/scripts/utils/git.ts b/scripts/utils/git.ts index d3076cb1..2cea8847 100644 --- a/scripts/utils/git.ts +++ b/scripts/utils/git.ts @@ -5,28 +5,28 @@ import { format } from "date-fns"; * Escape a string for shell command usage */ function escapeShellArg(arg: string): string { - // Wrap the entire argument in single quotes and escape any existing single quotes - return `'${arg.replace(/'/g, "'\\''")}'`; + // Wrap the entire argument in single quotes and escape any existing single quotes + return `'${arg.replace(/'/g, "'\\''")}'`; } /** * Git command execution utilities */ export function runCommand(command: string): string { - try { - // If this is a git add command, escape the file path - const finalCommand = command.startsWith("git add ") - ? `git add ${escapeShellArg(command.slice("git add ".length))}` - : command; + try { + // If this is a git add command, escape the file path + const finalCommand = command.startsWith("git add ") + ? `git add ${escapeShellArg(command.slice("git add ".length))}` + : command; - return execSync(finalCommand, { encoding: "utf8", stdio: "pipe" }); - } catch (error) { - console.error(`Error executing command: ${command}`); - if (error instanceof Error) { - console.error((error as { stderr?: string }).stderr || error.message); - } - throw error; - } + return execSync(finalCommand, { encoding: "utf8", stdio: "pipe" }); + } catch (error) { + console.error(`Error executing command: ${command}`); + if (error instanceof Error) { + console.error((error as { stderr?: string }).stderr || error.message); + } + throw error; + } } /** @@ -35,8 +35,8 @@ export function runCommand(command: string): string { * @returns Formatted branch name with timestamp */ export function generateBranchName(prefix: string): string { - const timestamp = format(new Date(), "yyyyMMdd-HHmmss"); - return `${prefix}-${timestamp}`; + const timestamp = format(new Date(), "yyyyMMdd-HHmmss"); + return `${prefix}-${timestamp}`; } /** @@ -45,15 +45,15 @@ export function generateBranchName(prefix: string): string { * @param remote - Remote name (default: 'origin') */ export function deleteBranch(branchName: string, remote = "origin"): void { - try { - // Delete local branch if it exists - runCommand(`git branch -D ${branchName} 2>/dev/null || true`); - // Delete remote branch if it exists - runCommand(`git push ${remote} --delete ${branchName} 2>/dev/null || true`); - } catch (_error) { - // Ignore errors since the branch might not exist - console.info(`No existing branch '${branchName}' to clean up`); - } + try { + // Delete local branch if it exists + runCommand(`git branch -D ${branchName} 2>/dev/null || true`); + // Delete remote branch if it exists + runCommand(`git push ${remote} --delete ${branchName} 2>/dev/null || true`); + } catch (_error) { + // Ignore errors since the branch might not exist + console.info(`No existing branch '${branchName}' to clean up`); + } } /** @@ -61,74 +61,74 @@ export function deleteBranch(branchName: string, remote = "origin"): void { * @param options - Pull request options */ export function createPullRequest(options: { - title: string; - body: string; - base: string; - head: string; - labels?: string[]; + title: string; + body: string; + base: string; + head: string; + labels?: string[]; }): void { - try { - // Check if gh CLI is installed - let hasGhCli = false; - try { - runCommand("gh --version"); - hasGhCli = true; - } catch (_error) { - // Let this error fall through to the manual instructions - hasGhCli = false; - } + try { + // Check if gh CLI is installed + let hasGhCli = false; + try { + runCommand("gh --version"); + hasGhCli = true; + } catch (_error) { + // Let this error fall through to the manual instructions + hasGhCli = false; + } - // If GitHub CLI is not installed, show manual instructions - if (!hasGhCli) { - console.info("\n=================================="); - console.info("✨ PULL REQUEST CREATION INSTRUCTIONS"); - console.info("=================================="); - console.info("\nGitHub CLI not found. You have two options:"); - console.info("\n1. Install GitHub CLI (recommended):"); - console.info(" brew install gh # macOS"); - console.info(" winget install GitHub.cli # Windows"); - console.info(" Then run: gh auth login"); - console.info("\n2. Create PR manually:"); - console.info(" Open this URL in your browser:"); - console.info(` ${getRepoUrl()}/compare/${options.base}...${options.head}`); - console.info("\n Then fill in these details:"); - console.info(` • Title: ${options.title}`); - console.info(` • Description: ${options.body}`); - if (options.labels?.length) { - console.info(` • Labels: ${options.labels.join(", ")}`); - } - console.info("\n==================================\n"); - return; - } + // If GitHub CLI is not installed, show manual instructions + if (!hasGhCli) { + console.info("\n=================================="); + console.info("✨ PULL REQUEST CREATION INSTRUCTIONS"); + console.info("=================================="); + console.info("\nGitHub CLI not found. You have two options:"); + console.info("\n1. Install GitHub CLI (recommended):"); + console.info(" brew install gh # macOS"); + console.info(" winget install GitHub.cli # Windows"); + console.info(" Then run: gh auth login"); + console.info("\n2. Create PR manually:"); + console.info(" Open this URL in your browser:"); + console.info(` ${getRepoUrl()}/compare/${options.base}...${options.head}`); + console.info("\n Then fill in these details:"); + console.info(` • Title: ${options.title}`); + console.info(` • Description: ${options.body}`); + if (options.labels?.length) { + console.info(` • Labels: ${options.labels.join(", ")}`); + } + console.info("\n==================================\n"); + return; + } - // GitHub CLI is installed - create PR - const labelArgs = options.labels?.length - ? options.labels.map((label) => `--label "${label}"`).join(" ") - : ""; + // GitHub CLI is installed - create PR + const labelArgs = options.labels?.length + ? options.labels.map((label) => `--label "${label}"`).join(" ") + : ""; - runCommand( - `gh pr create --title "${options.title}" --body "${options.body}" ` + - `--base ${options.base} --head ${options.head} ${labelArgs}` - ); - console.info("✨ Pull request created successfully"); - } catch (error) { - if (error instanceof Error) { - console.debug("Error details:", error.message); - } - console.warn("\n=================================="); - console.warn("❌ PULL REQUEST CREATION FAILED"); - console.warn("=================================="); - console.warn("\nPlease create the PR manually:"); - console.warn("\n1. Open this URL in your browser:"); - console.warn(` ${getRepoUrl()}/compare/${options.base}...${options.head}`); - console.warn("\n2. Fill in these details:"); - console.warn(` • Title: ${options.title}`); - console.warn(` • Description: ${options.body}`); - if (options.labels?.length) { - console.warn(` • Labels: ${options.labels.join(", ")}`); - } - console.warn("\n==================================\n"); - } + runCommand( + `gh pr create --title "${options.title}" --body "${options.body}" ` + + `--base ${options.base} --head ${options.head} ${labelArgs}` + ); + console.info("✨ Pull request created successfully"); + } catch (error) { + if (error instanceof Error) { + console.debug("Error details:", error.message); + } + console.warn("\n=================================="); + console.warn("❌ PULL REQUEST CREATION FAILED"); + console.warn("=================================="); + console.warn("\nPlease create the PR manually:"); + console.warn("\n1. Open this URL in your browser:"); + console.warn(` ${getRepoUrl()}/compare/${options.base}...${options.head}`); + console.warn("\n2. Fill in these details:"); + console.warn(` • Title: ${options.title}`); + console.warn(` • Description: ${options.body}`); + if (options.labels?.length) { + console.warn(` • Labels: ${options.labels.join(", ")}`); + } + console.warn("\n==================================\n"); + } } /** @@ -136,21 +136,21 @@ export function createPullRequest(options: { * @returns Repository URL or empty string if not found */ function getRepoUrl(): string { - try { - // Get the remote URL - const remoteUrl = runCommand("git remote get-url origin").trim(); + try { + // Get the remote URL + const remoteUrl = runCommand("git remote get-url origin").trim(); - // Convert SSH URL to HTTPS URL if necessary - if (remoteUrl.startsWith("git@github.com:")) { - return `https://github.com/${remoteUrl.slice(15).replace(".git", "")}`; - } + // Convert SSH URL to HTTPS URL if necessary + if (remoteUrl.startsWith("git@github.com:")) { + return `https://github.com/${remoteUrl.slice(15).replace(".git", "")}`; + } - // Clean up HTTPS URL if necessary - return remoteUrl.replace(".git", "").replace(/\n/g, ""); - } catch (error) { - console.error("Failed to get repository URL:", error); - return ""; - } + // Clean up HTTPS URL if necessary + return remoteUrl.replace(".git", "").replace(/\n/g, ""); + } catch (error) { + console.error("Failed to get repository URL:", error); + return ""; + } } /** @@ -159,12 +159,12 @@ function getRepoUrl(): string { * @throws Error if not on expected branch */ export function verifyCurrentBranch(expectedBranch: string): void { - const currentBranch = runCommand("git rev-parse --abbrev-ref HEAD").trim(); - if (currentBranch !== expectedBranch) { - throw new Error( - `Not on ${expectedBranch} branch. Please switch to ${expectedBranch} before running this script.` - ); - } + const currentBranch = runCommand("git rev-parse --abbrev-ref HEAD").trim(); + if (currentBranch !== expectedBranch) { + throw new Error( + `Not on ${expectedBranch} branch. Please switch to ${expectedBranch} before running this script.` + ); + } } /** @@ -173,12 +173,12 @@ export function verifyCurrentBranch(expectedBranch: string): void { * @throws Error if remote doesn't exist */ export function verifyRemote(remote: string): void { - try { - runCommand(`git remote get-url ${remote}`); - } catch (_error) { - throw new Error( - `Remote '${remote}' not found. Please add it using:\n` + - `git remote add ${remote} ` - ); - } + try { + runCommand(`git remote get-url ${remote}`); + } catch (_error) { + throw new Error( + `Remote '${remote}' not found. Please add it using:\n` + + `git remote add ${remote} ` + ); + } } diff --git a/src/app/(app)/(authentication)/_components/authentication-card.tsx b/src/app/(app)/(authentication)/_components/authentication-card.tsx index 4e17a45e..7eea353c 100644 --- a/src/app/(app)/(authentication)/_components/authentication-card.tsx +++ b/src/app/(app)/(authentication)/_components/authentication-card.tsx @@ -2,15 +2,15 @@ import { Card } from "@/components/ui/card"; import { cn } from "@/lib/utils"; export function AuthenticationCard({ - children, - className, + children, + className, }: { - children: React.ReactNode; - className?: string; + children: React.ReactNode; + className?: string; }) { - return ( - - {children} - - ); + return ( + + {children} + + ); } diff --git a/src/app/(app)/(authentication)/_components/login-form.tsx b/src/app/(app)/(authentication)/_components/login-form.tsx index 936bc2ec..14f5a8a6 100644 --- a/src/app/(app)/(authentication)/_components/login-form.tsx +++ b/src/app/(app)/(authentication)/_components/login-form.tsx @@ -9,65 +9,69 @@ import { authProvidersArray } from "@/server/auth.providers"; import Link from "next/link"; interface AuthFormProps extends ComponentPropsWithoutRef<"div"> { - mode: "sign-in" | "sign-up"; - children: ReactNode; - title?: string; - description?: string; + mode: "sign-in" | "sign-up"; + children: ReactNode; + title?: string; + description?: string; } export function AuthForm({ - mode = "sign-in", - className, - children, - title, - description, - ...props + mode = "sign-in", + className, + children, + title, + description, + ...props }: AuthFormProps) { - const isSignIn = mode === "sign-in"; - const cardTitle = typeof title === "string" ? title : (isSignIn ? "Welcome to Shipkit" : "Create an account"); - const cardDescription = typeof description === "string" ? description : isSignIn - ? "Login to get started" - : "Sign up to get started"; - const alternateLink = isSignIn - ? { text: "Don't have an account?", href: routes.auth.signUp, label: "Sign up" } - : { text: "Already have an account?", href: routes.auth.signIn, label: "Sign in" }; + const isSignIn = mode === "sign-in"; + const cardTitle = + typeof title === "string" ? title : isSignIn ? "Welcome to Shipkit" : "Create an account"; + const cardDescription = + typeof description === "string" + ? description + : isSignIn + ? "Login to get started" + : "Sign up to get started"; + const alternateLink = isSignIn + ? { text: "Don't have an account?", href: routes.auth.signUp, label: "Sign up" } + : { text: "Already have an account?", href: routes.auth.signIn, label: "Sign in" }; - return ( -
- - - {cardTitle} - {cardDescription} - - -
- }> - - - {/* Only show email sign-in if credentials provider is enabled */} - {authProvidersArray.includes("credentials") && ( - <> -
- - Or continue with email - -
- {children} - - )} -
- {alternateLink.text}{" "} - - {alternateLink.label} - -
-
-
-
-
- By clicking continue, you agree to our Terms of Service and{" "} - Privacy Policy. -
-
- ); + return ( +
+ + + {cardTitle} + {cardDescription} + + +
+ }> + + + {/* Only show email sign-in if credentials provider is enabled */} + {authProvidersArray.includes("credentials") && ( + <> +
+ + Or continue with email + +
+ {children} + + )} +
+ {alternateLink.text}{" "} + + {alternateLink.label} + +
+
+
+
+
+ By clicking continue, you agree to our Terms of Service and{" "} + Privacy Policy. +
+
+ ); } diff --git a/src/app/(app)/(authentication)/_components/oauth-buttons.tsx b/src/app/(app)/(authentication)/_components/oauth-buttons.tsx index 2b993100..7255b47a 100644 --- a/src/app/(app)/(authentication)/_components/oauth-buttons.tsx +++ b/src/app/(app)/(authentication)/_components/oauth-buttons.tsx @@ -12,79 +12,76 @@ import { cva } from "class-variance-authority"; import { useSearchParams } from "next/navigation"; const oauthButtonVariants = cva("flex items-center justify-center gap-sm", { - variants: { - variant: { - default: "w-full", - icons: "w-auto p-2", - }, - }, - defaultVariants: { - variant: "default", - }, + variants: { + variant: { + default: "w-full", + icons: "w-auto p-2", + }, + }, + defaultVariants: { + variant: "default", + }, }); interface OAuthButtonsProps { - variant?: "default" | "icons"; - className?: string; + variant?: "default" | "icons"; + className?: string; } interface Provider { - id: string; - name: string; + id: string; + name: string; } export function OAuthButtons({ variant = "default", className }: OAuthButtonsProps) { - // Redirect back to the page that the user was on before signing in - const searchParams = useSearchParams(); - const nextUrl = searchParams?.get(SEARCH_PARAM_KEYS.nextUrl); - const options = nextUrl ? { redirectTo: nextUrl } : {}; + // Redirect back to the page that the user was on before signing in + const searchParams = useSearchParams(); + const nextUrl = searchParams?.get(SEARCH_PARAM_KEYS.nextUrl); + const options = nextUrl ? { redirectTo: nextUrl } : {}; - const handleSignIn = async (providerId: string) => { - await signInWithOAuthAction({ providerId, options }); - }; + const handleSignIn = async (providerId: string) => { + await signInWithOAuthAction({ providerId, options }); + }; - return ( -
- {authProviders.map((provider: Provider) => { - const { name, id } = provider; + return ( +
+ {authProviders.map((provider: Provider) => { + const { name, id } = provider; - if (!name || String(name).toLowerCase() === "credentials" || id === "resend") { - return null; - } + if (!name || String(name).toLowerCase() === "credentials" || id === "resend") { + return null; + } - const button = ( - - ); + const button = ( + + ); - return ( -
handleSignIn(id)} - > - {variant === "icons" ? ( - - {button} - -

Sign in with {name}

-
-
- ) : ( - button - )} -
- ); - })} -
- ); + return ( +
handleSignIn(id)}> + {variant === "icons" ? ( + + {button} + +

Sign in with {name}

+
+
+ ) : ( + button + )} +
+ ); + })} +
+ ); } diff --git a/src/app/(app)/(authentication)/error/_components/auth-error-content.tsx b/src/app/(app)/(authentication)/error/_components/auth-error-content.tsx index c0acc349..e3b686b8 100644 --- a/src/app/(app)/(authentication)/error/_components/auth-error-content.tsx +++ b/src/app/(app)/(authentication)/error/_components/auth-error-content.tsx @@ -6,39 +6,39 @@ import Link from "next/link"; import { useSearchParams } from "next/navigation"; enum Error { - Configuration = "Configuration", + Configuration = "Configuration", } const errorMap = { - [Error.Configuration]: ( -

- There was a problem when trying to authenticate. Please contact us if this error persists. - Unique error code: Configuration -

- ), + [Error.Configuration]: ( +

+ There was a problem when trying to authenticate. Please contact us if this error persists. + Unique error code: Configuration +

+ ), }; export function AuthErrorContent() { - const searchParams = useSearchParams(); - const error = searchParams.get("error") as Error; + const searchParams = useSearchParams(); + const error = searchParams.get("error") as Error; - return ( -
+ ); } diff --git a/src/app/(app)/(authentication)/error/page.tsx b/src/app/(app)/(authentication)/error/page.tsx index a3215a5b..09b536f3 100644 --- a/src/app/(app)/(authentication)/error/page.tsx +++ b/src/app/(app)/(authentication)/error/page.tsx @@ -8,9 +8,9 @@ import { AuthErrorContent } from "./_components/auth-error-content"; * site-wide); this page owns its own now. */ export default function AuthErrorPage() { - return ( - - - - ); + return ( + + + + ); } diff --git a/src/app/(app)/(authentication)/forgot-password/_components/forgot-password-form.tsx b/src/app/(app)/(authentication)/forgot-password/_components/forgot-password-form.tsx index ffc25f0b..c511facf 100644 --- a/src/app/(app)/(authentication)/forgot-password/_components/forgot-password-form.tsx +++ b/src/app/(app)/(authentication)/forgot-password/_components/forgot-password-form.tsx @@ -6,12 +6,12 @@ import type { z } from "zod"; import { Button } from "@/components/ui/button"; import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { forgotPasswordSchema } from "@/lib/schemas/auth"; @@ -20,45 +20,45 @@ import { forgotPasswordAction } from "@/server/actions/auth"; import { toast } from "sonner"; export function ForgotPasswordForm() { - const form = useForm>({ - resolver: zodResolver(forgotPasswordSchema), - defaultValues: getSchemaDefaults(forgotPasswordSchema), - }); + const form = useForm>({ + resolver: zodResolver(forgotPasswordSchema), + defaultValues: getSchemaDefaults(forgotPasswordSchema), + }); - async function onSubmit(values: z.infer) { - await forgotPasswordAction(values) - .then(() => { - toast.success("Email sent", { - description: "Please check your email for a link to reset your password.", - }); - }) - .catch(() => { - toast.error("Error sending password reset email", { - description: "Please try again.", - }); - }); - } + async function onSubmit(values: z.infer) { + await forgotPasswordAction(values) + .then(() => { + toast.success("Email sent", { + description: "Please check your email for a link to reset your password.", + }); + }) + .catch(() => { + toast.error("Error sending password reset email", { + description: "Please try again.", + }); + }); + } - return ( -
- void form.handleSubmit(onSubmit)} className="flex flex-col gap-sm"> - ( - - Email - - - - - - )} - /> - - - - ); + return ( +
+ void form.handleSubmit(onSubmit)} className="flex flex-col gap-sm"> + ( + + Email + + + + + + )} + /> + + + + ); } diff --git a/src/app/(app)/(authentication)/forgot-password/page.tsx b/src/app/(app)/(authentication)/forgot-password/page.tsx index 08e601b1..91b0e15f 100644 --- a/src/app/(app)/(authentication)/forgot-password/page.tsx +++ b/src/app/(app)/(authentication)/forgot-password/page.tsx @@ -3,15 +3,15 @@ import { CardContent, CardDescription, CardHeader, CardTitle } from "@/component import { AuthenticationCard } from "../_components/authentication-card"; export default function ForgotPasswordPage() { - return ( - - - Forgot Password - Enter your email below to reset your password - - - - - - ); + return ( + + + Forgot Password + Enter your email below to reset your password + + + + + + ); } diff --git a/src/app/(app)/(authentication)/layout.tsx b/src/app/(app)/(authentication)/layout.tsx index 780e7980..af7454d8 100644 --- a/src/app/(app)/(authentication)/layout.tsx +++ b/src/app/(app)/(authentication)/layout.tsx @@ -9,9 +9,5 @@ export const metadata: Metadata = { }; export default function Layout({ children }: { children: React.ReactNode }) { - return ( -
- {children} -
- ); + return
{children}
; } diff --git a/src/app/(app)/(authentication)/sign-in/_components/sign-in-form.tsx b/src/app/(app)/(authentication)/sign-in/_components/sign-in-form.tsx index 4539c95d..038f3da8 100644 --- a/src/app/(app)/(authentication)/sign-in/_components/sign-in-form.tsx +++ b/src/app/(app)/(authentication)/sign-in/_components/sign-in-form.tsx @@ -7,12 +7,12 @@ import type { z } from "zod"; import { Button } from "@/components/ui/button"; import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { routes } from "@/config/routes"; @@ -24,113 +24,113 @@ import Link from "next/link"; import { useSearchParams } from "next/navigation"; export const SignInForm = () => { - const searchParams = useSearchParams(); - const nextUrl = searchParams.get(SEARCH_PARAM_KEYS.nextUrl); - const { toast } = useToast(); - const form = useForm>({ - resolver: zodResolver(signInSchema), - defaultValues: { - ...getSchemaDefaults(signInSchema), - redirectTo: routes.home, - redirect: true, - mode: "onChange", - }, - }); + const searchParams = useSearchParams(); + const nextUrl = searchParams.get(SEARCH_PARAM_KEYS.nextUrl); + const { toast } = useToast(); + const form = useForm>({ + resolver: zodResolver(signInSchema), + defaultValues: { + ...getSchemaDefaults(signInSchema), + redirectTo: routes.home, + redirect: true, + mode: "onChange", + }, + }); - async function onSubmit(values: z.infer) { - try { - const formData = new FormData(); - for (const [key, value] of Object.entries(values)) { - if (value !== undefined && value !== null) { - formData.append(key, value.toString()); - } - } + async function onSubmit(values: z.infer) { + try { + const formData = new FormData(); + for (const [key, value] of Object.entries(values)) { + if (value !== undefined && value !== null) { + formData.append(key, value.toString()); + } + } - // Redirect back to the page that the user was on before signing in - if (nextUrl) { - formData.append(SEARCH_PARAM_KEYS.nextUrl, nextUrl); - } + // Redirect back to the page that the user was on before signing in + if (nextUrl) { + formData.append(SEARCH_PARAM_KEYS.nextUrl, nextUrl); + } - await signInWithCredentialsAction(values, formData); + await signInWithCredentialsAction(values, formData); - toast({ - title: "Success", - description: "Signed in successfully.", - }); - } catch (error) { - if (error instanceof Error) { - const errorMessage = error.message.toLowerCase(); - if ( - errorMessage.includes("invalid credentials") || - errorMessage.includes("user not found") - ) { - toast({ - title: "Error", - description: "Invalid email or password. Please try again.", - variant: "destructive", - }); - } else { - toast({ - title: "Error", - description: error.message, - variant: "destructive", - }); - } - } else { - toast({ - title: "Error", - description: "Something went wrong. Please try again.", - variant: "destructive", - }); - } - } - } + toast({ + title: "Success", + description: "Signed in successfully.", + }); + } catch (error) { + if (error instanceof Error) { + const errorMessage = error.message.toLowerCase(); + if ( + errorMessage.includes("invalid credentials") || + errorMessage.includes("user not found") + ) { + toast({ + title: "Error", + description: "Invalid email or password. Please try again.", + variant: "destructive", + }); + } else { + toast({ + title: "Error", + description: error.message, + variant: "destructive", + }); + } + } else { + toast({ + title: "Error", + description: "Something went wrong. Please try again.", + variant: "destructive", + }); + } + } + } - return ( -
- - ( - - Email - - - - - - )} - /> - ( - -
- Password - - Forgot password? - -
- - - - -
- )} - /> - - - - ); + return ( +
+ + ( + + Email + + + + + + )} + /> + ( + +
+ Password + + Forgot password? + +
+ + + + +
+ )} + /> + + + + ); }; diff --git a/src/app/(app)/(authentication)/sign-in/page.tsx b/src/app/(app)/(authentication)/sign-in/page.tsx index 651aace6..ec9768f1 100644 --- a/src/app/(app)/(authentication)/sign-in/page.tsx +++ b/src/app/(app)/(authentication)/sign-in/page.tsx @@ -6,17 +6,17 @@ import { AuthForm } from "../_components/login-form"; import { SignInForm } from "./_components/sign-in-form"; export default function SignInPage() { - return ( -
- -
- -
- {siteConfig.name} - - - - -
- ); + return ( +
+ +
+ +
+ {siteConfig.name} + + + + +
+ ); } diff --git a/src/app/(app)/(authentication)/sign-out/page.tsx b/src/app/(app)/(authentication)/sign-out/page.tsx index ba8bc1b8..a539b6aa 100644 --- a/src/app/(app)/(authentication)/sign-out/page.tsx +++ b/src/app/(app)/(authentication)/sign-out/page.tsx @@ -3,5 +3,5 @@ import { SEARCH_PARAM_KEYS } from "@/config/search-param-keys"; import { redirect } from "next/navigation"; export default function SignOutPage() { - redirect(`${routes.auth.signOutIn}?${SEARCH_PARAM_KEYS.nextUrl}=${routes.home}`); + redirect(`${routes.auth.signOutIn}?${SEARCH_PARAM_KEYS.nextUrl}=${routes.home}`); } diff --git a/src/app/(app)/(authentication)/sign-up/_components/sign-up-card.tsx b/src/app/(app)/(authentication)/sign-up/_components/sign-up-card.tsx index 1f53b2d0..defe94a0 100644 --- a/src/app/(app)/(authentication)/sign-up/_components/sign-up-card.tsx +++ b/src/app/(app)/(authentication)/sign-up/_components/sign-up-card.tsx @@ -6,26 +6,26 @@ import Link from "next/link"; import { AuthenticationCard } from "../../_components/authentication-card"; export function SignUpCard() { - return ( - <> - - - Sign Up - Enter your information to create an account - - -
- - -
-
- Already have an account?{" "} - - Sign in - -
-
-
- - ); + return ( + <> + + + Sign Up + Enter your information to create an account + + +
+ + +
+
+ Already have an account?{" "} + + Sign in + +
+
+
+ + ); } diff --git a/src/app/(app)/(authentication)/sign-up/_components/sign-up-form.tsx b/src/app/(app)/(authentication)/sign-up/_components/sign-up-form.tsx index ac3e7586..1e8f87fa 100644 --- a/src/app/(app)/(authentication)/sign-up/_components/sign-up-form.tsx +++ b/src/app/(app)/(authentication)/sign-up/_components/sign-up-form.tsx @@ -7,12 +7,12 @@ import type { z } from "zod"; import { Button } from "@/components/ui/button"; import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { routes } from "@/config/routes"; @@ -21,92 +21,92 @@ import { getSchemaDefaults } from "@/lib/utils/get-schema-defaults"; import { signUpWithCredentialsAction } from "@/server/actions/auth"; export const SignUpForm = () => { - const { toast } = useToast(); - const form = useForm>({ - resolver: zodResolver(signUpSchema), - defaultValues: { - ...getSchemaDefaults(signUpSchema), - redirectTo: routes.home, - redirect: true, - }, - }); + const { toast } = useToast(); + const form = useForm>({ + resolver: zodResolver(signUpSchema), + defaultValues: { + ...getSchemaDefaults(signUpSchema), + redirectTo: routes.home, + redirect: true, + }, + }); - async function onSubmit(values: z.infer) { - try { - const formData = new FormData(); - for (const [key, value] of Object.entries(values)) { - if (value !== undefined && value !== null) { - formData.append(key, value.toString()); - } - } + async function onSubmit(values: z.infer) { + try { + const formData = new FormData(); + for (const [key, value] of Object.entries(values)) { + if (value !== undefined && value !== null) { + formData.append(key, value.toString()); + } + } - await signUpWithCredentialsAction(values, formData); + await signUpWithCredentialsAction(values, formData); - toast({ - title: "Success", - description: "Account created successfully.", - }); - } catch (error) { - if (error instanceof Error) { - if (error.message.includes("User already exists")) { - form.setError("email", { - type: "manual", - message: "An account with this email already exists.", - }); - } else { - toast({ - title: "Error", - description: error.message, - variant: "destructive", - }); - } - } else { - toast({ - title: "Error", - description: "Something went wrong. Please try again.", - variant: "destructive", - }); - } - } - } + toast({ + title: "Success", + description: "Account created successfully.", + }); + } catch (error) { + if (error instanceof Error) { + if (error.message.includes("User already exists")) { + form.setError("email", { + type: "manual", + message: "An account with this email already exists.", + }); + } else { + toast({ + title: "Error", + description: error.message, + variant: "destructive", + }); + } + } else { + toast({ + title: "Error", + description: "Something went wrong. Please try again.", + variant: "destructive", + }); + } + } + } - return ( -
- - ( - - Email - - - - - - )} - /> - ( - - Password - - - - - - )} - /> - - - - ); + return ( +
+ + ( + + Email + + + + + + )} + /> + ( + + Password + + + + + + )} + /> + + + + ); }; diff --git a/src/app/(app)/(authentication)/sign-up/_components/sign-up.tsx b/src/app/(app)/(authentication)/sign-up/_components/sign-up.tsx index 170f4625..ae956ac1 100644 --- a/src/app/(app)/(authentication)/sign-up/_components/sign-up.tsx +++ b/src/app/(app)/(authentication)/sign-up/_components/sign-up.tsx @@ -6,99 +6,99 @@ import { IconBrandGithub, IconBrandGoogle, IconBrandOnlyfans } from "@tabler/ico import type React from "react"; export function SignupFormDemo() { - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - }; + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + }; - return ( -
-

- Welcome to Aceternity -

-

- Login to aceternity if you can because we don't have a login flow yet -

+ return ( +
+

+ Welcome to Aceternity +

+

+ Login to aceternity if you can because we don't have a login flow yet +

-
-
- - - - - - - - -
- - - - - - - - - - - - + +
+ + + + + + + + +
+ + + + + + + + + + + + - + -
+
-
- - - -
- -
- ); +
+ + + +
+ +
+ ); } const BottomGradient = () => { - return ( - <> - - - - ); + return ( + <> + + + + ); }; const LabelInputContainer = ({ - children, - className, + children, + className, }: { - children: React.ReactNode; - className?: string; + children: React.ReactNode; + className?: string; }) => { - return
{children}
; + return
{children}
; }; diff --git a/src/app/(app)/(authentication)/sign-up/page.tsx b/src/app/(app)/(authentication)/sign-up/page.tsx index 5481031b..61e15ac2 100644 --- a/src/app/(app)/(authentication)/sign-up/page.tsx +++ b/src/app/(app)/(authentication)/sign-up/page.tsx @@ -6,17 +6,17 @@ import { AuthForm } from "../_components/login-form"; import { SignUpForm } from "./_components/sign-up-form"; export default function SignUpPage() { - return ( -
- -
- -
- {siteConfig.name} - - - - -
- ); + return ( +
+ +
+ +
+ {siteConfig.name} + + + + +
+ ); } diff --git a/src/app/(app)/(demo)/layout.tsx b/src/app/(app)/(demo)/layout.tsx index ac285cf9..f174362f 100644 --- a/src/app/(app)/(demo)/layout.tsx +++ b/src/app/(app)/(demo)/layout.tsx @@ -2,9 +2,9 @@ import type { Metadata } from "next"; import type React from "react"; export const metadata: Metadata = { - robots: { index: false, follow: false }, + robots: { index: false, follow: false }, }; export default function DemoLayout({ children }: { children: React.ReactNode }) { - return <>{children}; + return <>{children}; } diff --git a/src/app/(app)/(demo)/trpc/_components/post.tsx b/src/app/(app)/(demo)/trpc/_components/post.tsx index 1832a3fb..21bcdfcd 100644 --- a/src/app/(app)/(demo)/trpc/_components/post.tsx +++ b/src/app/(app)/(demo)/trpc/_components/post.tsx @@ -5,46 +5,46 @@ import { useState } from "react"; import { api } from "@/lib/trpc/react"; export function LatestPost() { - const [latestPost] = api.post.getLatest.useSuspenseQuery(); + const [latestPost] = api.post.getLatest.useSuspenseQuery(); - const utils = api.useUtils(); - const [name, setName] = useState(""); - const createPost = api.post.create.useMutation({ - onSuccess: async () => { - await utils.post.invalidate(); - setName(""); - }, - }); + const utils = api.useUtils(); + const [name, setName] = useState(""); + const createPost = api.post.create.useMutation({ + onSuccess: async () => { + await utils.post.invalidate(); + setName(""); + }, + }); - return ( -
- {latestPost ? ( -

Your most recent post: {latestPost.name}

- ) : ( -

You have no posts yet.

- )} -
{ - e.preventDefault(); - createPost.mutate({ name }); - }} - className="flex flex-col gap-2" - > - setName(e.target.value)} - className="w-full rounded-full px-4 py-2" - /> - -
-
- ); + return ( +
+ {latestPost ? ( +

Your most recent post: {latestPost.name}

+ ) : ( +

You have no posts yet.

+ )} +
{ + e.preventDefault(); + createPost.mutate({ name }); + }} + className="flex flex-col gap-2" + > + setName(e.target.value)} + className="w-full rounded-full px-4 py-2" + /> + +
+
+ ); } diff --git a/src/app/(app)/(demo)/trpc/api/[trpc]/route.ts b/src/app/(app)/(demo)/trpc/api/[trpc]/route.ts index 204af50a..5f533f74 100644 --- a/src/app/(app)/(demo)/trpc/api/[trpc]/route.ts +++ b/src/app/(app)/(demo)/trpc/api/[trpc]/route.ts @@ -10,23 +10,23 @@ import { createTRPCContext } from "@/lib/trpc/api/trpc"; * handling a HTTP request (e.g. when you make requests from Client Components). */ const createContext = async (req: NextRequest) => { - return createTRPCContext({ - headers: req.headers, - }); + return createTRPCContext({ + headers: req.headers, + }); }; const handler = (req: NextRequest) => - fetchRequestHandler({ - endpoint: "/api/trpc", - req, - router: appRouter, - createContext: () => createContext(req), - onError: - env.NODE_ENV === "development" - ? ({ path, error }) => { - console.error(`❌ tRPC failed on ${path ?? ""}: ${error.message}`); - } - : undefined, - }); + fetchRequestHandler({ + endpoint: "/api/trpc", + req, + router: appRouter, + createContext: () => createContext(req), + onError: + env.NODE_ENV === "development" + ? ({ path, error }) => { + console.error(`❌ tRPC failed on ${path ?? ""}: ${error.message}`); + } + : undefined, + }); export { handler as GET, handler as POST }; diff --git a/src/app/(app)/(demo)/trpc/page.tsx b/src/app/(app)/(demo)/trpc/page.tsx index 1344d42f..47b94e48 100644 --- a/src/app/(app)/(demo)/trpc/page.tsx +++ b/src/app/(app)/(demo)/trpc/page.tsx @@ -7,70 +7,70 @@ import { auth } from "@/server/auth"; import { LatestPost } from "./_components/post"; export default async function Home() { - const hello = await api.post.hello({ text: "from tRPC" }); - const session = await auth(); + const hello = await api.post.hello({ text: "from tRPC" }); + const session = await auth(); - void api.post.getLatest.prefetch(); + void api.post.getLatest.prefetch(); - const SignInOutButton = () => { - const urlPath = session ? routes.auth.signOut : routes.auth.signIn; - const nextUrl = routes.demo.trpc; - const url = `${urlPath}?${SEARCH_PARAM_KEYS.nextUrl}=${nextUrl}`; - return ( - - {session ? "Sign out" : "Sign in"} - - ); - }; + const SignInOutButton = () => { + const urlPath = session ? routes.auth.signOut : routes.auth.signIn; + const nextUrl = routes.demo.trpc; + const url = `${urlPath}?${SEARCH_PARAM_KEYS.nextUrl}=${nextUrl}`; + return ( + + {session ? "Sign out" : "Sign in"} + + ); + }; - return ( - -
-
-

- Create T3 App -

-
- -

First Steps →

-
- Just the basics - Everything you need to know to set up your database and - authentication. -
- - -

Documentation →

-
- Learn more about Create T3 App, the libraries it uses, and how to deploy it. -
- -
-
-

- {hello ? hello.greeting : "Loading tRPC query..."} -

+ return ( + +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your database and + authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how to deploy it. +
+ +
+
+

+ {hello ? hello.greeting : "Loading tRPC query..."} +

-
-

- {session && Logged in as {session.user?.name}} -

-
-
- - {session?.user && } -
-
-
- ); +
+

+ {session && Logged in as {session.user?.name}} +

+
+
+ + {session?.user && } +
+
+
+ ); } diff --git a/src/app/(app)/(legal)/eula/page.mdx b/src/app/(app)/(legal)/eula/page.mdx index ca0f0f2d..06eb0c16 100644 --- a/src/app/(app)/(legal)/eula/page.mdx +++ b/src/app/(app)/(legal)/eula/page.mdx @@ -47,6 +47,7 @@ The Integration may be subject to usage limits based on your Shipkit subscriptio ### 5.2 Modification Restrictions You may not: + - Modify, reverse engineer, decompile, or disassemble the Integration - Create derivative works based on the Integration - Remove any proprietary notices or labels on the Integration diff --git a/src/app/(app)/(legal)/legal/page.mdx b/src/app/(app)/(legal)/legal/page.mdx index 800f1de3..597528a5 100644 --- a/src/app/(app)/(legal)/legal/page.mdx +++ b/src/app/(app)/(legal)/legal/page.mdx @@ -16,14 +16,19 @@ All services on this website are provided by {siteConfig.creator.fullName}. Email: {siteConfig.email.format("support")} ## Liability and Copyright Notices + ### Limitation of Liability + The content of this online offering has been created meticulously and according to our current state knowledge, but is for informational purposes only and does not create any legally binding effect, unless it concerns legally required information (e.g., imprint, privacy policy, terms of service, or mandatory consumer notices). We reserve the right to modify, partially or completely delete the content, provided the contractual obligations remain unaffected. All offers are non-binding. ### Links to Third-Party Websites + Contents from third-party websites that we directly or indirectly link to are beyond our area of responsibility and we do not endorse them. The provider of the linked websites is solely responsible for all content and particularly for damages resulting from the use of information accessible on these linked websites. ### Copyrights and Brand Rights + All content displayed on this website, such as text, photographs, graphics, brands, and trademarks are protected by the respective protective rights (copyright, brand rights). The use, reproduction, etc., are subject to our rights or the rights of the respective copyright holders or rights administrators. ### Notices of Legal Violations + If you discover any legal infringements within our web presence, please inform us. We will promptly remove any illegal content and links upon becoming aware of them. diff --git a/src/app/(app)/about/page.tsx b/src/app/(app)/about/page.tsx index 16ed95fa..b2bf4634 100644 --- a/src/app/(app)/about/page.tsx +++ b/src/app/(app)/about/page.tsx @@ -15,23 +15,22 @@ export default function AboutPage() {

- {siteConfig.branding.projectName} is a modern development platform - built for teams and individuals who want to ship production-ready apps - without the boilerplate. We handle the infrastructure so you can focus - on what makes your product unique. + {siteConfig.branding.projectName} is a modern development platform built for teams and + individuals who want to ship production-ready apps without the boilerplate. We handle the + infrastructure so you can focus on what makes your product unique.

- Built with Next.js, TypeScript, and the tools developers already know - and love — Tailwind CSS, shadcn/ui, Drizzle ORM, and more. + Built with Next.js, TypeScript, and the tools developers already know and love — + Tailwind CSS, shadcn/ui, Drizzle ORM, and more.

Our Mission

- We believe shipping software should be fast, repeatable, and enjoyable. - Our goal is to eliminate the weeks of setup that come before you can - write your first line of business logic. + We believe shipping software should be fast, repeatable, and enjoyable. Our goal is to + eliminate the weeks of setup that come before you can write your first line of business + logic.

@@ -53,10 +52,7 @@ export default function AboutPage() {

Get in Touch

Have questions or want to learn more?{" "} - + Contact us {" "} or find us on{" "} diff --git a/src/app/(app)/api/health/contact/route.ts b/src/app/(app)/api/health/contact/route.ts index d3baf59d..ef07f46b 100644 --- a/src/app/(app)/api/health/contact/route.ts +++ b/src/app/(app)/api/health/contact/route.ts @@ -24,17 +24,17 @@ export const dynamic = "force-dynamic"; const RESEND_DOMAINS_URL = "https://api.resend.com/domains"; interface HealthCheck { - name: string; - ok: boolean; - detail: string; + name: string; + ok: boolean; + detail: string; } const isAuthorized = (request: Request): boolean => { - const expected = process.env.HEALTHCHECK_TOKEN; - if (!expected) return true; // no token configured -> open - const provided = - request.headers.get("x-healthcheck-token") ?? new URL(request.url).searchParams.get("token"); - return provided === expected; + const expected = process.env.HEALTHCHECK_TOKEN; + if (!expected) return true; // no token configured -> open + const provided = + request.headers.get("x-healthcheck-token") ?? new URL(request.url).searchParams.get("token"); + return provided === expected; }; /** @@ -42,75 +42,75 @@ const isAuthorized = (request: Request): boolean => { * lightweight authenticated read (listing domains). No email is sent. */ const checkResend = async (): Promise => { - const key = process.env.RESEND_API_KEY; - if (!key) { - return { name: "resend_api_key", ok: false, detail: "RESEND_API_KEY not set" }; - } + const key = process.env.RESEND_API_KEY; + if (!key) { + return { name: "resend_api_key", ok: false, detail: "RESEND_API_KEY not set" }; + } - try { - const response = await fetch(RESEND_DOMAINS_URL, { - headers: { Authorization: `Bearer ${key}` }, - cache: "no-store", - }); - if (!response.ok) { - // Sending-only keys can't list domains, but the rejection proves the - // key authenticated — Resend names it "restricted_api_key". That key - // still sends mail, so the form is healthy. - const body = (await response.json().catch(() => null)) as { - name?: string; - message?: string; - } | null; - if (body?.name === "restricted_api_key" || body?.message?.includes("restricted")) { - return { name: "resend_api_key", ok: true, detail: "valid (sending-only key)" }; - } - if (response.status === 400 || response.status === 401 || response.status === 403) { - return { - name: "resend_api_key", - ok: false, - detail: `Resend rejected key (HTTP ${response.status}): ${body?.message ?? "unknown"}`, - }; - } - return { - name: "resend_api_key", - ok: false, - detail: `Resend API unhealthy (HTTP ${response.status})`, - }; - } - return { name: "resend_api_key", ok: true, detail: "valid" }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return { name: "resend_api_key", ok: false, detail: `Resend request failed: ${message}` }; - } + try { + const response = await fetch(RESEND_DOMAINS_URL, { + headers: { Authorization: `Bearer ${key}` }, + cache: "no-store", + }); + if (!response.ok) { + // Sending-only keys can't list domains, but the rejection proves the + // key authenticated — Resend names it "restricted_api_key". That key + // still sends mail, so the form is healthy. + const body = (await response.json().catch(() => null)) as { + name?: string; + message?: string; + } | null; + if (body?.name === "restricted_api_key" || body?.message?.includes("restricted")) { + return { name: "resend_api_key", ok: true, detail: "valid (sending-only key)" }; + } + if (response.status === 400 || response.status === 401 || response.status === 403) { + return { + name: "resend_api_key", + ok: false, + detail: `Resend rejected key (HTTP ${response.status}): ${body?.message ?? "unknown"}`, + }; + } + return { + name: "resend_api_key", + ok: false, + detail: `Resend API unhealthy (HTTP ${response.status})`, + }; + } + return { name: "resend_api_key", ok: true, detail: "valid" }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return { name: "resend_api_key", ok: false, detail: `Resend request failed: ${message}` }; + } }; export async function GET(request: Request): Promise { - if (!isAuthorized(request)) { - return Response.json({ ok: false, error: "Unauthorized" }, { status: 401 }); - } + if (!isAuthorized(request)) { + return Response.json({ ok: false, error: "Unauthorized" }, { status: 401 }); + } - const checks: HealthCheck[] = [ - // The server action sends to siteConfig.email.support from .noreply — - // both must be non-empty for delivery to work. - { - name: "support_email", - ok: Boolean(siteConfig.email?.support), - detail: siteConfig.email?.support ? "set" : "siteConfig.email.support not set", - }, - { - name: "noreply_email", - ok: Boolean(siteConfig.email?.noreply), - detail: siteConfig.email?.noreply ? "set" : "siteConfig.email.noreply not set", - }, - await checkResend(), - ]; + const checks: HealthCheck[] = [ + // The server action sends to siteConfig.email.support from .noreply — + // both must be non-empty for delivery to work. + { + name: "support_email", + ok: Boolean(siteConfig.email?.support), + detail: siteConfig.email?.support ? "set" : "siteConfig.email.support not set", + }, + { + name: "noreply_email", + ok: Boolean(siteConfig.email?.noreply), + detail: siteConfig.email?.noreply ? "set" : "siteConfig.email.noreply not set", + }, + await checkResend(), + ]; - const ok = checks.every((check) => check.ok); - return Response.json( - { ok, checks }, - { - status: ok ? 200 : 503, - // Disable caching so monitors always see live state. - headers: { "Cache-Control": "no-store, max-age=0" }, - } - ); + const ok = checks.every((check) => check.ok); + return Response.json( + { ok, checks }, + { + status: ok ? 200 : 503, + // Disable caching so monitors always see live state. + headers: { "Cache-Control": "no-store, max-age=0" }, + } + ); } diff --git a/src/app/(app)/bones/cli-www/_actions/install.ts b/src/app/(app)/bones/cli-www/_actions/install.ts index 63cb868e..21b4e902 100644 --- a/src/app/(app)/bones/cli-www/_actions/install.ts +++ b/src/app/(app)/bones/cli-www/_actions/install.ts @@ -9,63 +9,60 @@ import { type InstallOptions } from "../_lib/types"; */ // eslint-disable-next-line @typescript-eslint/require-await export async function installComponent( - componentUrl: string, - options: InstallOptions = {}, + componentUrl: string, + options: InstallOptions = {} ): Promise> { - const encoder = new TextEncoder(); + const encoder = new TextEncoder(); - return new ReadableStream({ - // eslint-disable-next-line @typescript-eslint/require-await - async start(controller) { - try { - const args = ["shadcn@latest", "add"]; + return new ReadableStream({ + // eslint-disable-next-line @typescript-eslint/require-await + async start(controller) { + try { + const args = ["shadcn@latest", "add"]; - // Add component name - args.push(componentUrl); + // Add component name + args.push(componentUrl); - // Add options - if (options.overwrite) args.push("--overwrite"); - if (options.style) args.push("--style", options.style); - if (options.typescript) args.push("--typescript"); - if (options.path) args.push("--path", options.path); + // Add options + if (options.overwrite) args.push("--overwrite"); + if (options.style) args.push("--style", options.style); + if (options.typescript) args.push("--typescript"); + if (options.path) args.push("--path", options.path); - const process = spawn("npx", args, { - stdio: ["pipe", "pipe", "pipe"], - }); + const process = spawn("npx", args, { + stdio: ["pipe", "pipe", "pipe"], + }); - // If not overwriting, automatically answer "n" to prompts - if (!options.overwrite && process.stdin) { - process.stdin.write("n\n"); - process.stdin.end(); - } + // If not overwriting, automatically answer "n" to prompts + if (!options.overwrite && process.stdin) { + process.stdin.write("n\n"); + process.stdin.end(); + } - process.stdout?.on("data", (data: Buffer) => { - controller.enqueue(encoder.encode(data.toString())); - }); + process.stdout?.on("data", (data: Buffer) => { + controller.enqueue(encoder.encode(data.toString())); + }); - process.stderr?.on("data", (data: Buffer) => { - controller.enqueue(encoder.encode(data.toString())); - }); + process.stderr?.on("data", (data: Buffer) => { + controller.enqueue(encoder.encode(data.toString())); + }); - process.on("close", (code) => { - if (code !== 0) { - controller.enqueue( - encoder.encode(`\nProcess exited with code ${code}`), - ); - } - controller.close(); - }); + process.on("close", (code) => { + if (code !== 0) { + controller.enqueue(encoder.encode(`\nProcess exited with code ${code}`)); + } + controller.close(); + }); - process.on("error", (err) => { - controller.enqueue(encoder.encode(`\nError: ${err.message}`)); - controller.close(); - }); - } catch (error) { - const message = - error instanceof Error ? error.message : "Unknown error occurred"; - controller.enqueue(encoder.encode(`\nError: ${message}`)); - controller.close(); - } - }, - }); + process.on("error", (err) => { + controller.enqueue(encoder.encode(`\nError: ${err.message}`)); + controller.close(); + }); + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error occurred"; + controller.enqueue(encoder.encode(`\nError: ${message}`)); + controller.close(); + } + }, + }); } diff --git a/src/app/(app)/bones/cli-www/_actions/registry.ts b/src/app/(app)/bones/cli-www/_actions/registry.ts index 3f15eba1..dd71f8ab 100644 --- a/src/app/(app)/bones/cli-www/_actions/registry.ts +++ b/src/app/(app)/bones/cli-www/_actions/registry.ts @@ -9,13 +9,13 @@ import { z } from "zod"; const execAsync = promisify(exec); const ComponentsConfigSchema = z.object({ - aliases: z.object({ - components: z.string(), - ui: z.string(), - utils: z.string(), - lib: z.string(), - hooks: z.string(), - }), + aliases: z.object({ + components: z.string(), + ui: z.string(), + utils: z.string(), + lib: z.string(), + hooks: z.string(), + }), }); type ComponentsConfig = z.infer; @@ -24,89 +24,89 @@ type ComponentsConfig = z.infer; * Get the project root directory */ async function getProjectRoot(): Promise { - try { - const { stdout } = await execAsync("git rev-parse --show-toplevel"); - return stdout.trim(); - } catch { - throw new Error("Not in a git repository"); - } + try { + const { stdout } = await execAsync("git rev-parse --show-toplevel"); + return stdout.trim(); + } catch { + throw new Error("Not in a git repository"); + } } /** * Get project dependencies from package.json */ export async function getDependencies(): Promise<{ - dependencies: Record; - devDependencies: Record; + dependencies: Record; + devDependencies: Record; }> { - try { - const projectRoot = await getProjectRoot(); - const packageJsonPath = path.join(projectRoot, "package.json"); - const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { - dependencies: Record; - devDependencies: Record; - }; + try { + const projectRoot = await getProjectRoot(); + const packageJsonPath = path.join(projectRoot, "package.json"); + const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { + dependencies: Record; + devDependencies: Record; + }; - return { - dependencies: packageJson.dependencies || {}, - devDependencies: packageJson.devDependencies || {}, - }; - } catch (error) { - throw new Error("Failed to read package.json"); - } + return { + dependencies: packageJson.dependencies || {}, + devDependencies: packageJson.devDependencies || {}, + }; + } catch (error) { + throw new Error("Failed to read package.json"); + } } /** * Get the components configuration from components.json */ async function getComponentsConfig(): Promise { - try { - const projectRoot = await getProjectRoot(); - const configPath = path.join(projectRoot, "components.json"); - const config = JSON.parse(readFileSync(configPath, "utf-8")); - return ComponentsConfigSchema.parse(config); - } catch (error) { - throw new Error("Failed to read components.json"); - } + try { + const projectRoot = await getProjectRoot(); + const configPath = path.join(projectRoot, "components.json"); + const config = JSON.parse(readFileSync(configPath, "utf-8")); + return ComponentsConfigSchema.parse(config); + } catch (error) { + throw new Error("Failed to read components.json"); + } } /** * Get installed components from the components directory */ export async function getInstalledComponents(): Promise { - try { - const config = await getComponentsConfig(); - const projectRoot = await getProjectRoot(); + try { + const config = await getComponentsConfig(); + const projectRoot = await getProjectRoot(); - // Get the actual filesystem path from the ui alias - // The alias is typically "@/components/ui", we need to convert it to the actual path - const uiPath = config.aliases.ui.replace("@/", "src/"); - const componentsDir = path.join(projectRoot, uiPath); + // Get the actual filesystem path from the ui alias + // The alias is typically "@/components/ui", we need to convert it to the actual path + const uiPath = config.aliases.ui.replace("@/", "src/"); + const componentsDir = path.join(projectRoot, uiPath); - try { - // Check if directory exists - const stats = statSync(componentsDir); - if (!stats.isDirectory()) { - console.error("UI components path is not a directory:", componentsDir); - return []; - } + try { + // Check if directory exists + const stats = statSync(componentsDir); + if (!stats.isDirectory()) { + console.error("UI components path is not a directory:", componentsDir); + return []; + } - // Read the directory and filter for .tsx files - const components = readdirSync(componentsDir) - .filter((file) => file.endsWith(".tsx")) - .map((file) => file.replace(".tsx", "")); + // Read the directory and filter for .tsx files + const components = readdirSync(componentsDir) + .filter((file) => file.endsWith(".tsx")) + .map((file) => file.replace(".tsx", "")); - return components; - } catch (error: any) { - if (error.code === "ENOENT") { - console.error("UI components directory does not exist:", componentsDir); - } else { - console.error("Error reading UI components directory:", error); - } - return []; - } - } catch (error) { - console.error("Error getting installed components:", error); - return []; - } + return components; + } catch (error: any) { + if (error.code === "ENOENT") { + console.error("UI components directory does not exist:", componentsDir); + } else { + console.error("Error reading UI components directory:", error); + } + return []; + } + } catch (error) { + console.error("Error getting installed components:", error); + return []; + } } diff --git a/src/app/(app)/bones/cli-www/_components/add-registry-dialog.tsx b/src/app/(app)/bones/cli-www/_components/add-registry-dialog.tsx index 38232f11..9b4c014e 100644 --- a/src/app/(app)/bones/cli-www/_components/add-registry-dialog.tsx +++ b/src/app/(app)/bones/cli-www/_components/add-registry-dialog.tsx @@ -1,129 +1,129 @@ -'use client' +"use client"; -import { Button } from '@/components/ui/button' +import { Button } from "@/components/ui/button"; import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '@/components/ui/dialog' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { toast } from '@/hooks/use-toast' -import { ReloadIcon } from '@radix-ui/react-icons' -import { PlusCircle } from 'lucide-react' -import { type FormEvent, useState } from 'react' -import { validateRegistry } from '../_lib/registry-service' -import type { Registry } from '../_lib/types' + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { toast } from "@/hooks/use-toast"; +import { ReloadIcon } from "@radix-ui/react-icons"; +import { PlusCircle } from "lucide-react"; +import { type FormEvent, useState } from "react"; +import { validateRegistry } from "../_lib/registry-service"; +import type { Registry } from "../_lib/types"; interface AddRegistryDialogProps { - onAdd: (registry: Registry) => void + onAdd: (registry: Registry) => void; } export function AddRegistryDialog({ onAdd }: AddRegistryDialogProps) { - const [open, setOpen] = useState(false) - const [name, setName] = useState('') - const [url, setUrl] = useState('') - const [loading, setLoading] = useState(false) + const [open, setOpen] = useState(false); + const [name, setName] = useState(""); + const [url, setUrl] = useState(""); + const [loading, setLoading] = useState(false); - const handleSubmit = async (e: FormEvent) => { - e.preventDefault() - setLoading(true) + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setLoading(true); - try { - const registry = { - name, - url, - baseComponentUrl: url, - baseBlockUrl: url, - baseDocsUrl: url, - } + try { + const registry = { + name, + url, + baseComponentUrl: url, + baseBlockUrl: url, + baseDocsUrl: url, + }; - await validateRegistry(registry) - onAdd(registry) - setOpen(false) - setName('') - setUrl('') - toast({ - title: "Registry added", - description: `Successfully added registry "${name}"`, - }) - } catch (error) { - toast({ - title: "Failed to add registry", - description: error instanceof Error ? error.message : "Unknown error occurred", - variant: "destructive", - }) - } finally { - setLoading(false) - } - } + await validateRegistry(registry); + onAdd(registry); + setOpen(false); + setName(""); + setUrl(""); + toast({ + title: "Registry added", + description: `Successfully added registry "${name}"`, + }); + } catch (error) { + toast({ + title: "Failed to add registry", + description: error instanceof Error ? error.message : "Unknown error occurred", + variant: "destructive", + }); + } finally { + setLoading(false); + } + }; - return ( -

- - - - e.stopPropagation()}> - - Add Custom Registry - - Add a custom component registry to browse and install components from. - - -
-
-
- - setName(e.target.value)} - placeholder="My Custom Registry" - required - disabled={loading} - /> -
-
- - setUrl(e.target.value)} - placeholder="https://example.com/registry" - required - disabled={loading} - /> -
-
- - - -
-
-
- ) + return ( + + + + + e.stopPropagation()}> + + Add Custom Registry + + Add a custom component registry to browse and install components from. + + +
+
+
+ + setName(e.target.value)} + placeholder="My Custom Registry" + required + disabled={loading} + /> +
+
+ + setUrl(e.target.value)} + placeholder="https://example.com/registry" + required + disabled={loading} + /> +
+
+ + + +
+
+
+ ); } diff --git a/src/app/(app)/bones/cli-www/_components/browser-header.tsx b/src/app/(app)/bones/cli-www/_components/browser-header.tsx index ccbc8b31..a6b8106d 100644 --- a/src/app/(app)/bones/cli-www/_components/browser-header.tsx +++ b/src/app/(app)/bones/cli-www/_components/browser-header.tsx @@ -1,226 +1,218 @@ -'use client' +"use client"; -import { Button } from '@/components/ui/button' -import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command' -import { Label } from '@/components/ui/label' -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' -import { Separator } from '@/components/ui/separator' -import { Switch } from '@/components/ui/switch' -import { cn } from '@/lib/utils' -import { CaretSortIcon, CheckIcon } from '@radix-ui/react-icons' -import { Palette, Trash2 } from 'lucide-react' -import { useState } from 'react' -import type { Registry } from '../_lib/types' -import { AddRegistryDialog } from './add-registry-dialog' -import { getColor } from './colors' -import type { StyleMode } from './types' +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { Label } from "@/components/ui/label"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Separator } from "@/components/ui/separator"; +import { Switch } from "@/components/ui/switch"; +import { cn } from "@/lib/utils"; +import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; +import { Palette, Trash2 } from "lucide-react"; +import { useState } from "react"; +import type { Registry } from "../_lib/types"; +import { AddRegistryDialog } from "./add-registry-dialog"; +import { getColor } from "./colors"; +import type { StyleMode } from "./types"; export interface BrowserHeaderProps { - currentStyle: StyleMode - registries: Registry[] - currentRegistry?: Registry | null - onRegistryChange: (registry: Registry | null) => void - overwrite: boolean - onOverwriteChange: (value: boolean) => void - onAddRegistry: (registry: Registry) => Promise - onRemoveRegistry: (name: string) => void - onStyleChange?: () => void + currentStyle: StyleMode; + registries: Registry[]; + currentRegistry?: Registry | null; + onRegistryChange: (registry: Registry | null) => void; + overwrite: boolean; + onOverwriteChange: (value: boolean) => void; + onAddRegistry: (registry: Registry) => Promise; + onRemoveRegistry: (name: string) => void; + onStyleChange?: () => void; } export function BrowserHeader({ - currentStyle, - registries, - currentRegistry, - onRegistryChange, - overwrite, - onOverwriteChange, - onAddRegistry, - onRemoveRegistry, - onStyleChange, + currentStyle, + registries, + currentRegistry, + onRegistryChange, + overwrite, + onOverwriteChange, + onAddRegistry, + onRemoveRegistry, + onStyleChange, }: BrowserHeaderProps) { - const [open, setOpen] = useState(false) + const [open, setOpen] = useState(false); - const handleRegistryChange = (registry: Registry | null) => { - onRegistryChange(registry) - setOpen(false) - } + const handleRegistryChange = (registry: Registry | null) => { + onRegistryChange(registry); + setOpen(false); + }; - return ( -
-

Component Browser

+ return ( +
+

Component Browser

-
- - - - - - - - No registry found. - - - handleRegistryChange(null)} - className="flex justify-between" - > -
- -
- All Registries -
- - {registries - .filter((r) => !r.custom) - .map((registry) => ( - handleRegistryChange(registry)} - className="flex justify-between" - > -
- -
- {registry.name} -
- - ))} - - {registries.some(r => r.custom) && ( - - {registries - .filter((r) => r.custom) - .map((registry) => ( - handleRegistryChange(registry)} - className="flex justify-between" - > -
- -
- {registry.name} -
- - - ))} - - )} - - - - - - - - - - -
- - -
- - -
-
- ) +
+ + + + + + + + No registry found. + + + handleRegistryChange(null)} + className="flex justify-between" + > +
+ +
+ All Registries +
+ + {registries + .filter((r) => !r.custom) + .map((registry) => ( + handleRegistryChange(registry)} + className="flex justify-between" + > +
+ +
+ {registry.name} +
+ + ))} + + {registries.some((r) => r.custom) && ( + + {registries + .filter((r) => r.custom) + .map((registry) => ( + handleRegistryChange(registry)} + className="flex justify-between" + > +
+ +
+ {registry.name} +
+ + + ))} + + )} + + + + + + + + + + +
+ + +
+ + +
+
+ ); } - - diff --git a/src/app/(app)/bones/cli-www/_components/browser-sidebar.tsx b/src/app/(app)/bones/cli-www/_components/browser-sidebar.tsx index 4ae6d877..ceb47fcd 100644 --- a/src/app/(app)/bones/cli-www/_components/browser-sidebar.tsx +++ b/src/app/(app)/bones/cli-www/_components/browser-sidebar.tsx @@ -1,205 +1,236 @@ -'use client' +"use client"; -import { Badge } from '@/components/ui/badge' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { Separator } from '@/components/ui/separator' -import { cn } from '@/lib/utils' -import { Search } from 'lucide-react' -import { memo, useMemo } from 'react' -import type { RegistryFilters, RegistryItem } from '../_lib/types' -import { getColor } from './colors' -import type { StyleMode, InstallationProgress } from './types' -import { CustomInstallDialog } from './custom-install-dialog' +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; +import { Search } from "lucide-react"; +import { memo, useMemo } from "react"; +import type { RegistryFilters, RegistryItem } from "../_lib/types"; +import { getColor } from "./colors"; +import type { StyleMode, InstallationProgress } from "./types"; +import { CustomInstallDialog } from "./custom-install-dialog"; export interface BrowserSidebarProps { - currentStyle: StyleMode - searchTerm: string - setSearchTerm: (term: string) => void - filters: RegistryFilters - setFilters: (filters: RegistryFilters) => void - categories: string[] - types: string[] - filteredItems: RegistryItem[] - onCustomInstall: (command: string) => void - installationProgress: InstallationProgress + currentStyle: StyleMode; + searchTerm: string; + setSearchTerm: (term: string) => void; + filters: RegistryFilters; + setFilters: (filters: RegistryFilters) => void; + categories: string[]; + types: string[]; + filteredItems: RegistryItem[]; + onCustomInstall: (command: string) => void; + installationProgress: InstallationProgress; } -export const BrowserSidebar = memo(({ - currentStyle, - searchTerm, - setSearchTerm, - filters, - setFilters, - categories, - types, - filteredItems, - onCustomInstall, - installationProgress -}: BrowserSidebarProps) => { - // Memoize expensive computations - const componentCount = useMemo(() => - filteredItems.filter(item => item.type === 'registry:ui').length, - [filteredItems] - ) +export const BrowserSidebar = memo( + ({ + currentStyle, + searchTerm, + setSearchTerm, + filters, + setFilters, + categories, + types, + filteredItems, + onCustomInstall, + installationProgress, + }: BrowserSidebarProps) => { + // Memoize expensive computations + const componentCount = useMemo( + () => filteredItems.filter((item) => item.type === "registry:ui").length, + [filteredItems] + ); - const blockCount = useMemo(() => - filteredItems.filter(item => item.type === 'registry:block').length, - [filteredItems] - ) + const blockCount = useMemo( + () => filteredItems.filter((item) => item.type === "registry:block").length, + [filteredItems] + ); - const categoryCount = useMemo(() => - categories.reduce((acc, category) => { - acc[category] = filteredItems.filter(item => item.categories?.includes(category)).length - return acc - }, {} as Record), - [categories, filteredItems] - ) + const categoryCount = useMemo( + () => + categories.reduce( + (acc, category) => { + acc[category] = filteredItems.filter((item) => + item.categories?.includes(category) + ).length; + return acc; + }, + {} as Record + ), + [categories, filteredItems] + ); - return ( -
-
-
-
- - - {filteredItems.length} results - -
-
- - { - setSearchTerm(e.target.value) - // Reset filters when searching to show all results - setFilters({ - ...filters, - type: 'all', - category: 'all' - }) - }} - /> -
-
+ return ( +
+
+
+
+ + + {filteredItems.length} results + +
+
+ + { + setSearchTerm(e.target.value); + // Reset filters when searching to show all results + setFilters({ + ...filters, + type: "all", + category: "all", + }); + }} + /> +
+
- - + + -
- - -
+
+ + +
-
- - -
+
+ + +
- - -
-
- Total Components - {filteredItems.length} -
-
- Types - {types.length} -
-
- Categories - {categories.length} -
-
-
-
- ) -}) -BrowserSidebar.displayName = 'BrowserSidebar' + +
+
+ Total Components + + {filteredItems.length} + +
+
+ Types + + {types.length} + +
+
+ Categories + + {categories.length} + +
+
+
+
+ ); + } +); +BrowserSidebar.displayName = "BrowserSidebar"; diff --git a/src/app/(app)/bones/cli-www/_components/colors.ts b/src/app/(app)/bones/cli-www/_components/colors.ts index 5caf9b2b..ae91a3ec 100644 --- a/src/app/(app)/bones/cli-www/_components/colors.ts +++ b/src/app/(app)/bones/cli-www/_components/colors.ts @@ -5,24 +5,24 @@ const HUE_OFFSET = 0.7; // This gives us a nice starting point in the blue-green // Function to generate a distinct color based on a string function generateColor(str: string): string { - // Create a hash of the string - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = str.charCodeAt(i) + ((hash << 5) - hash); - } + // Create a hash of the string + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } - // Convert hash to float between 0 and 1 - const normalizedHash = Math.abs(hash) / 2 ** 32; + // Convert hash to float between 0 and 1 + const normalizedHash = Math.abs(hash) / 2 ** 32; - // Use golden ratio to create well-distributed hues, with offset - const hue = (normalizedHash + GOLDEN_RATIO + HUE_OFFSET) % 1; + // Use golden ratio to create well-distributed hues, with offset + const hue = (normalizedHash + GOLDEN_RATIO + HUE_OFFSET) % 1; - // Convert to degrees and ensure good saturation and lightness for visibility - const h = Math.floor(hue * 360); - const s = 70 + (hash % 15); // 70-85% - slightly more saturated - const l = 60 + (hash % 8); // 60-68% - slightly brighter + // Convert to degrees and ensure good saturation and lightness for visibility + const h = Math.floor(hue * 360); + const s = 70 + (hash % 15); // 70-85% - slightly more saturated + const l = 60 + (hash % 8); // 60-68% - slightly brighter - return Color.hsl(h, s, l).hex(); + return Color.hsl(h, s, l).hex(); } // Cache to store generated colors @@ -30,12 +30,12 @@ const colorCache: Record = {}; // Function to get or generate a color for a given key export function getColor(key: string): string { - if (!key) return "#666666"; // Default color for empty keys + if (!key) return "#666666"; // Default color for empty keys - if (!colorCache[key]) { - colorCache[key] = generateColor(key); - } - return colorCache[key]; + if (!colorCache[key]) { + colorCache[key] = generateColor(key); + } + return colorCache[key]; } // Export empty objects to maintain compatibility with existing code diff --git a/src/app/(app)/bones/cli-www/_components/component-browser.tsx b/src/app/(app)/bones/cli-www/_components/component-browser.tsx index 05b50450..33da5141 100644 --- a/src/app/(app)/bones/cli-www/_components/component-browser.tsx +++ b/src/app/(app)/bones/cli-www/_components/component-browser.tsx @@ -1,531 +1,576 @@ -'use client' - -import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import { toast } from '@/hooks/use-toast' -import { cn } from '@/lib/utils' -import { CheckIcon, Cross2Icon, ReloadIcon } from '@radix-ui/react-icons' -import { motion } from 'framer-motion' -import { CopyIcon, Download } from 'lucide-react' -import { type MouseEvent, type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { installComponent } from '../_actions/install' -import { getInstalledComponents } from '../_actions/registry' -import { useRegistry } from '../_hooks/use-registry' -import { addCustomRegistry, getInstallCommand, removeCustomRegistry } from '../_lib/registry-service' -import type { Registry, RegistryItem } from '../_lib/types' -import { BrowserHeader } from './browser-header' -import { BrowserSidebar } from './browser-sidebar' -import { getColor } from './colors' -import { ComponentDetails } from './component-details' -import { buttonStyles, componentCardStyles, containerStyles } from './styles' -import { Terminal } from './terminal' -import type { InstallationProgress, StyleMode } from './types' -import { memo } from 'react' +"use client"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { toast } from "@/hooks/use-toast"; +import { cn } from "@/lib/utils"; +import { CheckIcon, Cross2Icon, ReloadIcon } from "@radix-ui/react-icons"; +import { motion } from "framer-motion"; +import { CopyIcon, Download } from "lucide-react"; +import { + type MouseEvent, + type ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { installComponent } from "../_actions/install"; +import { getInstalledComponents } from "../_actions/registry"; +import { useRegistry } from "../_hooks/use-registry"; +import { + addCustomRegistry, + getInstallCommand, + removeCustomRegistry, +} from "../_lib/registry-service"; +import type { Registry, RegistryItem } from "../_lib/types"; +import { BrowserHeader } from "./browser-header"; +import { BrowserSidebar } from "./browser-sidebar"; +import { getColor } from "./colors"; +import { ComponentDetails } from "./component-details"; +import { buttonStyles, componentCardStyles, containerStyles } from "./styles"; +import { Terminal } from "./terminal"; +import type { InstallationProgress, StyleMode } from "./types"; +import { memo } from "react"; interface ComponentCardProps { - component: RegistryItem - currentStyle: StyleMode - currentRegistry?: Registry - onOpenSidebar: (component: RegistryItem) => void - showPreview?: boolean - showAllVariants?: boolean - onInstall: (component: RegistryItem) => void - isInstalled: boolean + component: RegistryItem; + currentStyle: StyleMode; + currentRegistry?: Registry; + onOpenSidebar: (component: RegistryItem) => void; + showPreview?: boolean; + showAllVariants?: boolean; + onInstall: (component: RegistryItem) => void; + isInstalled: boolean; } interface ActionButtonProps { - icon: ReactNode - tooltip: string - onClick: (e: MouseEvent) => void - currentStyle: StyleMode + icon: ReactNode; + tooltip: string; + onClick: (e: MouseEvent) => void; + currentStyle: StyleMode; } interface ComponentBrowserProps { - currentStyle?: StyleMode + currentStyle?: StyleMode; } const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text) - toast({ - title: "Copied to clipboard", - description: "The content has been copied to your clipboard.", - }) -} + navigator.clipboard.writeText(text); + toast({ + title: "Copied to clipboard", + description: "The content has been copied to your clipboard.", + }); +}; const ActionButton = ({ icon, tooltip, onClick, currentStyle }: ActionButtonProps) => ( - - - - - - -

{tooltip}

-
-
-
-) - -const ComponentCard = memo(({ component, currentStyle, currentRegistry, onOpenSidebar, showPreview, onInstall, isInstalled }: ComponentCardProps) => { - const registryColor = useMemo(() => getColor(component.registry || 'default'), [component.registry]) - const categoryColor = useMemo(() => getColor(component.categories?.[0] || 'default'), [component.categories]) - - const handleCopyCommand = useCallback((e: MouseEvent) => { - e.stopPropagation() - const installCommand = getInstallCommand(component, currentRegistry) - navigator.clipboard.writeText(installCommand) - toast({ - title: "Copied to clipboard", - description: "Install command has been copied to your clipboard.", - }) - }, [component, currentRegistry]) - - const handleInstallClick = useCallback((e: MouseEvent) => { - e.stopPropagation() - onInstall(component) - }, [component, onInstall]) - - return ( - onOpenSidebar(component)} - > -
- {/*
*/} - {isInstalled && ( - - - -
- - -

Installed

-
- - - )} - -
- {component.name} -
- } - tooltip="Copy install command" - onClick={handleCopyCommand} - currentStyle={currentStyle} - /> - {showPreview && ( - } - tooltip="Install component" - onClick={handleInstallClick} - currentStyle={currentStyle} - /> - )} -
- -
- -
- -

{component.description}

-
- - ) -}) -ComponentCard.displayName = 'ComponentCard' - -export function ComponentBrowser({ currentStyle: initialStyle = 'modern' }: ComponentBrowserProps) { - const [currentStyle, setCurrentStyle] = useState(initialStyle) - const [selectedComponent, setSelectedComponent] = useState(null) - const [isSidebarOpen, setIsSidebarOpen] = useState(false) - const [installationProgress, setInstallationProgress] = useState({ - status: 'idle' - }) - const [installedComponents, setInstalledComponents] = useState([]) - const [showInstallation, setShowInstallation] = useState(false) - const [overwrite, setOverwrite] = useState(false) - const sidebarRef = useRef(null) - - const { - registries, - currentRegistry, - items, - loading, - error, - filters, - searchQuery, - setCurrentRegistry: setCurrentRegistryBase, - setFilters, - setSearchQuery, - getCategories, - getTypes, - setRegistries, - filteredItems - } = useRegistry() - - // Wrap setCurrentRegistry to handle null case - const setCurrentRegistry = (registry: Registry | null) => { - setCurrentRegistryBase(registry || undefined) - } - - useEffect(() => { - const checkInstallations = async () => { - const components = await getInstalledComponents() - setInstalledComponents(components) - } - checkInstallations() - }, []) - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (sidebarRef.current && !sidebarRef.current.contains(event.target as Node)) { - setSelectedComponent(null) - setIsSidebarOpen(false) - } - } - - document.addEventListener('mousedown', handleClickOutside as any) - return () => { - document.removeEventListener('mousedown', handleClickOutside as any) - } - }, []) - - const toggleStyle = useCallback(() => { - setCurrentStyle(prev => { - if (prev === 'brutalist') return 'modern' - if (prev === 'modern') return 'minimalist' - return 'brutalist' - }) - }, []) - - const openSidebar = useCallback((component: RegistryItem) => { - setSelectedComponent(component) - setIsSidebarOpen(true) - }, []) - - const closeSidebar = useCallback(() => { - setSelectedComponent(null) - setIsSidebarOpen(false) - setInstallationProgress({ status: 'idle' }) - }, []) - - const handleInstall = useCallback((component: RegistryItem) => { - // Find the registry for this component - const registryName = component.registry - if (!registryName) { - toast({ - title: "Error", - description: "Component has no registry specified", - variant: "destructive", - }) - return - } - - const registry = registries.find(r => r.name === registryName) - if (!registry) { - toast({ - title: "Error", - description: "Could not find registry for component", - variant: "destructive", - }) - return - } - - const installCommand = getInstallCommand(component, registry) - const componentUrl = installCommand.split('"')[1] // Extract URL from command - if (!componentUrl) { - toast({ - title: "Error", - description: "Could not parse install command", - variant: "destructive", - }) - return - } - - setInstallationProgress({ status: 'installing' }) - installComponent(componentUrl, { overwrite }) - .then(async (stream) => { - let log = '' - const reader = stream.getReader() - while (true) { - const { done, value } = await reader.read() - if (done) break - log += new TextDecoder().decode(value) - setInstallationProgress({ - status: 'installing', - log - }) - } - setInstallationProgress({ - status: 'success', - log, - message: 'Component installed successfully!' - }) - const components = await getInstalledComponents() - setInstalledComponents(components) - }) - .catch((error) => { - setInstallationProgress({ - status: 'error', - message: error instanceof Error ? error.message : 'Failed to install component' - }) - }) - }, [registries, overwrite]) - - const handleCustomInstall = useCallback(async (command: string) => { - setInstallationProgress({ status: 'installing' }) - - // Extract the component URL or name from the command - const match = command.match(/"([^"]+)"/) || command.match(/add\s+(\S+)$/) - const componentUrl = match ? match[1] : null - - if (!componentUrl) { - setInstallationProgress({ - status: 'error', - message: 'Could not parse install command' - }) - return - } - - try { - const stream = await installComponent(componentUrl, { overwrite }) - let log = '' - const reader = stream.getReader() - while (true) { - const { done, value } = await reader.read() - if (done) break - log += new TextDecoder().decode(value) - setInstallationProgress({ - status: 'installing', - log - }) - } - setInstallationProgress({ - status: 'success', - log, - message: 'Component installed successfully!' - }) - const components = await getInstalledComponents() - setInstalledComponents(components) - } catch (error) { - setInstallationProgress({ - status: 'error', - message: error instanceof Error ? error.message : 'Failed to install component' - }) - } - }, [overwrite]) - - const hideInstallation = useCallback(() => { - setInstallationProgress({ status: 'idle' }) - }, []) - - const renderComponentGrid = useCallback((registry: string) => { - const allFilteredItems = filteredItems() - return ( -
- {allFilteredItems.map((component: RegistryItem) => ( - - ))} -
- ) - }, [currentStyle, currentRegistry, filteredItems, handleInstall, installedComponents, openSidebar]) - - if (error) { - return ( -
-
-

Error

-

{error.message}

-
-
- ) - } - - return ( -
- { - try { - const registryWithCustom = { ...registry, custom: true } - await addCustomRegistry(registryWithCustom) - setRegistries((prev: Registry[]) => [...prev, registryWithCustom]) - setCurrentRegistry(registryWithCustom) - } catch (error) { - toast({ - title: "Failed to add registry", - description: error instanceof Error ? error.message : "Unknown error occurred", - variant: "destructive", - }) - } - }} - onRemoveRegistry={(name: string) => { - try { - removeCustomRegistry(name) - const updatedRegistries = registries.filter((r) => r.name !== name) - setRegistries(updatedRegistries) - if (currentRegistry?.name === name) { - const defaultRegistry = updatedRegistries.find(r => !r.custom) || updatedRegistries[0] - if (defaultRegistry) { - setCurrentRegistry(defaultRegistry) - } - } - } catch (error) { - toast({ - title: "Failed to remove registry", - description: error instanceof Error ? error.message : "Unknown error occurred", - variant: "destructive", - }) - } - }} - onStyleChange={toggleStyle} - /> -
- -
- {loading ? ( -
-
-
- ) : ( -
- {renderComponentGrid('all')} -
- )} -
-
- - {/* Installation output overlay */} - {(installationProgress.status !== 'idle') && ( - - -
-
-
- {installationProgress.status === 'installing' ? ( -
- - Installing... -
- ) : installationProgress.status === 'success' ? ( -
- - Complete -
- ) : ( -
- - Error -
- )} -
- - Console Output - - -
-
- - {installationProgress.log ? ( - - ) : ( -
- -
- )} -
-
-
-
- )} - - {selectedComponent && ( - - )} -
- ) + + + + + + +

{tooltip}

+
+
+
+); + +const ComponentCard = memo( + ({ + component, + currentStyle, + currentRegistry, + onOpenSidebar, + showPreview, + onInstall, + isInstalled, + }: ComponentCardProps) => { + const registryColor = useMemo( + () => getColor(component.registry || "default"), + [component.registry] + ); + const categoryColor = useMemo( + () => getColor(component.categories?.[0] || "default"), + [component.categories] + ); + + const handleCopyCommand = useCallback( + (e: MouseEvent) => { + e.stopPropagation(); + const installCommand = getInstallCommand(component, currentRegistry); + navigator.clipboard.writeText(installCommand); + toast({ + title: "Copied to clipboard", + description: "Install command has been copied to your clipboard.", + }); + }, + [component, currentRegistry] + ); + + const handleInstallClick = useCallback( + (e: MouseEvent) => { + e.stopPropagation(); + onInstall(component); + }, + [component, onInstall] + ); + + return ( + onOpenSidebar(component)} + > +
+ {/*
*/} + {isInstalled && ( + + + +
+ + +

Installed

+
+ + + )} + +
+ {component.name} +
+ } + tooltip="Copy install command" + onClick={handleCopyCommand} + currentStyle={currentStyle} + /> + {showPreview && ( + } + tooltip="Install component" + onClick={handleInstallClick} + currentStyle={currentStyle} + /> + )} +
+
+ +
+ +

{component.description}

+
+ + ); + } +); +ComponentCard.displayName = "ComponentCard"; + +export function ComponentBrowser({ currentStyle: initialStyle = "modern" }: ComponentBrowserProps) { + const [currentStyle, setCurrentStyle] = useState(initialStyle); + const [selectedComponent, setSelectedComponent] = useState(null); + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + const [installationProgress, setInstallationProgress] = useState({ + status: "idle", + }); + const [installedComponents, setInstalledComponents] = useState([]); + const [showInstallation, setShowInstallation] = useState(false); + const [overwrite, setOverwrite] = useState(false); + const sidebarRef = useRef(null); + + const { + registries, + currentRegistry, + items, + loading, + error, + filters, + searchQuery, + setCurrentRegistry: setCurrentRegistryBase, + setFilters, + setSearchQuery, + getCategories, + getTypes, + setRegistries, + filteredItems, + } = useRegistry(); + + // Wrap setCurrentRegistry to handle null case + const setCurrentRegistry = (registry: Registry | null) => { + setCurrentRegistryBase(registry || undefined); + }; + + useEffect(() => { + const checkInstallations = async () => { + const components = await getInstalledComponents(); + setInstalledComponents(components); + }; + checkInstallations(); + }, []); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (sidebarRef.current && !sidebarRef.current.contains(event.target as Node)) { + setSelectedComponent(null); + setIsSidebarOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside as any); + return () => { + document.removeEventListener("mousedown", handleClickOutside as any); + }; + }, []); + + const toggleStyle = useCallback(() => { + setCurrentStyle((prev) => { + if (prev === "brutalist") return "modern"; + if (prev === "modern") return "minimalist"; + return "brutalist"; + }); + }, []); + + const openSidebar = useCallback((component: RegistryItem) => { + setSelectedComponent(component); + setIsSidebarOpen(true); + }, []); + + const closeSidebar = useCallback(() => { + setSelectedComponent(null); + setIsSidebarOpen(false); + setInstallationProgress({ status: "idle" }); + }, []); + + const handleInstall = useCallback( + (component: RegistryItem) => { + // Find the registry for this component + const registryName = component.registry; + if (!registryName) { + toast({ + title: "Error", + description: "Component has no registry specified", + variant: "destructive", + }); + return; + } + + const registry = registries.find((r) => r.name === registryName); + if (!registry) { + toast({ + title: "Error", + description: "Could not find registry for component", + variant: "destructive", + }); + return; + } + + const installCommand = getInstallCommand(component, registry); + const componentUrl = installCommand.split('"')[1]; // Extract URL from command + if (!componentUrl) { + toast({ + title: "Error", + description: "Could not parse install command", + variant: "destructive", + }); + return; + } + + setInstallationProgress({ status: "installing" }); + installComponent(componentUrl, { overwrite }) + .then(async (stream) => { + let log = ""; + const reader = stream.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + log += new TextDecoder().decode(value); + setInstallationProgress({ + status: "installing", + log, + }); + } + setInstallationProgress({ + status: "success", + log, + message: "Component installed successfully!", + }); + const components = await getInstalledComponents(); + setInstalledComponents(components); + }) + .catch((error) => { + setInstallationProgress({ + status: "error", + message: error instanceof Error ? error.message : "Failed to install component", + }); + }); + }, + [registries, overwrite] + ); + + const handleCustomInstall = useCallback( + async (command: string) => { + setInstallationProgress({ status: "installing" }); + + // Extract the component URL or name from the command + const match = command.match(/"([^"]+)"/) || command.match(/add\s+(\S+)$/); + const componentUrl = match ? match[1] : null; + + if (!componentUrl) { + setInstallationProgress({ + status: "error", + message: "Could not parse install command", + }); + return; + } + + try { + const stream = await installComponent(componentUrl, { overwrite }); + let log = ""; + const reader = stream.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + log += new TextDecoder().decode(value); + setInstallationProgress({ + status: "installing", + log, + }); + } + setInstallationProgress({ + status: "success", + log, + message: "Component installed successfully!", + }); + const components = await getInstalledComponents(); + setInstalledComponents(components); + } catch (error) { + setInstallationProgress({ + status: "error", + message: error instanceof Error ? error.message : "Failed to install component", + }); + } + }, + [overwrite] + ); + + const hideInstallation = useCallback(() => { + setInstallationProgress({ status: "idle" }); + }, []); + + const renderComponentGrid = useCallback( + (registry: string) => { + const allFilteredItems = filteredItems(); + return ( +
+ {allFilteredItems.map((component: RegistryItem) => ( + + ))} +
+ ); + }, + [currentStyle, currentRegistry, filteredItems, handleInstall, installedComponents, openSidebar] + ); + + if (error) { + return ( +
+
+

Error

+

{error.message}

+
+
+ ); + } + + return ( +
+ { + try { + const registryWithCustom = { ...registry, custom: true }; + await addCustomRegistry(registryWithCustom); + setRegistries((prev: Registry[]) => [...prev, registryWithCustom]); + setCurrentRegistry(registryWithCustom); + } catch (error) { + toast({ + title: "Failed to add registry", + description: error instanceof Error ? error.message : "Unknown error occurred", + variant: "destructive", + }); + } + }} + onRemoveRegistry={(name: string) => { + try { + removeCustomRegistry(name); + const updatedRegistries = registries.filter((r) => r.name !== name); + setRegistries(updatedRegistries); + if (currentRegistry?.name === name) { + const defaultRegistry = + updatedRegistries.find((r) => !r.custom) || updatedRegistries[0]; + if (defaultRegistry) { + setCurrentRegistry(defaultRegistry); + } + } + } catch (error) { + toast({ + title: "Failed to remove registry", + description: error instanceof Error ? error.message : "Unknown error occurred", + variant: "destructive", + }); + } + }} + onStyleChange={toggleStyle} + /> +
+ +
+ {loading ? ( +
+
+
+ ) : ( +
{renderComponentGrid("all")}
+ )} +
+
+ + {/* Installation output overlay */} + {installationProgress.status !== "idle" && ( + + +
+
+
+ {installationProgress.status === "installing" ? ( +
+ + Installing... +
+ ) : installationProgress.status === "success" ? ( +
+ + Complete +
+ ) : ( +
+ + Error +
+ )} +
+ + Console Output + + +
+
+ {installationProgress.log ? ( + + ) : ( +
+ +
+ )} +
+
+
+
+ )} + + {selectedComponent && ( + + )} +
+ ); } - diff --git a/src/app/(app)/bones/cli-www/_components/component-details.tsx b/src/app/(app)/bones/cli-www/_components/component-details.tsx index 75bcbdb9..9d0b6856 100644 --- a/src/app/(app)/bones/cli-www/_components/component-details.tsx +++ b/src/app/(app)/bones/cli-www/_components/component-details.tsx @@ -1,355 +1,360 @@ -'use client' +"use client"; -import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion' -import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' -import { toast } from '@/hooks/use-toast' -import { cn } from '@/lib/utils' -import { motion } from 'framer-motion' -import { BookOpen, Copy, FileJson, X } from 'lucide-react' -import type { KeyboardEvent, MouseEvent } from 'react' -import { useState } from 'react' -import { getDocumentationUrl, getInstallCommand } from '../_lib/registry-service' -import { getColor } from './colors' -import { ComponentStats } from './component-stats' -import { FileTree } from './file-tree' -import { Terminal } from './terminal' -import type { ComponentDetailsProps } from './types' +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { toast } from "@/hooks/use-toast"; +import { cn } from "@/lib/utils"; +import { motion } from "framer-motion"; +import { BookOpen, Copy, FileJson, X } from "lucide-react"; +import type { KeyboardEvent, MouseEvent } from "react"; +import { useState } from "react"; +import { getDocumentationUrl, getInstallCommand } from "../_lib/registry-service"; +import { getColor } from "./colors"; +import { ComponentStats } from "./component-stats"; +import { FileTree } from "./file-tree"; +import { Terminal } from "./terminal"; +import type { ComponentDetailsProps } from "./types"; const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text) - toast({ - title: "Copied to clipboard", - description: "The content has been copied to your clipboard.", - }) -} + navigator.clipboard.writeText(text); + toast({ + title: "Copied to clipboard", + description: "The content has been copied to your clipboard.", + }); +}; export function ComponentDetails({ - component, - currentStyle = 'modern', - onClose, - installationProgress, - onInstall, - onHideInstallation, - currentRegistry + component, + currentStyle = "modern", + onClose, + installationProgress, + onInstall, + onHideInstallation, + currentRegistry, }: ComponentDetailsProps) { - const [selectedFile, setSelectedFile] = useState() - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - onClose() - } - } - - const handleCopyInstall = () => { - const installCommand = getInstallCommand(component, currentRegistry) - copyToClipboard(installCommand) - } + const [selectedFile, setSelectedFile] = useState(); - const handleCopyJson = () => { - const json = JSON.stringify(component, null, 2) - copyToClipboard(json) - } + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + }; - const handleInstall = (e: MouseEvent) => { - e.preventDefault() - onInstall(component) - } + const handleCopyInstall = () => { + const installCommand = getInstallCommand(component, currentRegistry); + copyToClipboard(installCommand); + }; - return ( - <> - -
+ const handleCopyJson = () => { + const json = JSON.stringify(component, null, 2); + copyToClipboard(json); + }; -
- - - - - - -

Copy Install Command

-
-
-
+ const handleInstall = (e: MouseEvent) => { + e.preventDefault(); + onInstall(component); + }; - - - - - - -

Copy JSON

-
-
-
+ return ( + <> + +
- {getDocumentationUrl(component, currentRegistry) && ( - - )} +
+ + + + + + +

Copy Install Command

+
+
+
-
+ + + + + + +

Copy JSON

+
+
+
-
-

{component.description}

- - - Usage - Code - Files - Preview - -
- - - - Usage - - - - - Installation - -
-
-

Run the following command to add this component to your project:

-
-																	npx shadcn@latest add {component.name}
-																
-
-
- - {installationProgress.message && ( -

- {installationProgress.message} -

- )} - {installationProgress.log && ( -
- -
- )} -
-
-
-
- - Dependencies - - {component.dependencies?.length ? ( -
    - {component.dependencies.map(dep => ( -
  • {dep}
  • - ))} -
- ) : ( -

No dependencies

- )} -
-
-
-
-
-
- - - - Preview - - -
- Preview coming soon... -
-
-
-
- -
-
- - - - Code - - -
-												{JSON.stringify(component, null, 2)}
-											
-
-
-
- - - - Files - - - {component.files ? ( - ({ - path: typeof file === 'string' ? file : file.path, - type: 'file' - }))} - selectedFile={selectedFile} - onFileSelect={(file) => setSelectedFile(file.path)} - currentStyle={currentStyle} - /> - ) : ( -
- No files available -
- )} -
-
-
+ {getDocumentationUrl(component, currentRegistry) && ( + + )} +
-
- -
-
- - - ) +
+

{component.description}

+ + + Usage + Code + Files + Preview + +
+ + + + Usage + + + + + Installation + +
+
+

+ Run the following command to add this component to your project: +

+
+                                  npx shadcn@latest add {component.name}
+                                
+
+
+ + {installationProgress.message && ( +

+ {installationProgress.message} +

+ )} + {installationProgress.log && ( +
+ +
+ )} +
+
+
+
+ + Dependencies + + {component.dependencies?.length ? ( +
    + {component.dependencies.map((dep) => ( +
  • {dep}
  • + ))} +
+ ) : ( +

No dependencies

+ )} +
+
+
+
+
+
+ + + + Preview + + +
+ Preview coming soon... +
+
+
+
+ +
+
+ + + + Code + + +
+                        {JSON.stringify(component, null, 2)}
+                      
+
+
+
+ + + + Files + + + {component.files ? ( + ({ + path: typeof file === "string" ? file : file.path, + type: "file", + }))} + selectedFile={selectedFile} + onFileSelect={(file) => setSelectedFile(file.path)} + currentStyle={currentStyle} + /> + ) : ( +
No files available
+ )} +
+
+
+
+
+
+
+ + + ); } diff --git a/src/app/(app)/bones/cli-www/_components/component-stats.tsx b/src/app/(app)/bones/cli-www/_components/component-stats.tsx index a29f3b72..1b479b9b 100644 --- a/src/app/(app)/bones/cli-www/_components/component-stats.tsx +++ b/src/app/(app)/bones/cli-www/_components/component-stats.tsx @@ -1,215 +1,212 @@ -'use client' +"use client"; -import { Card } from '@/components/ui/card' -import { Progress } from '@/components/ui/progress' -import { cn } from '@/lib/utils' -import { useEffect, useState } from 'react' -import { getDependencies, getInstalledComponents } from '../_actions/registry' -import type { RegistryItem } from '../_lib/types' -import type { StyleMode } from './types' +import { Card } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { cn } from "@/lib/utils"; +import { useEffect, useState } from "react"; +import { getDependencies, getInstalledComponents } from "../_actions/registry"; +import type { RegistryItem } from "../_lib/types"; +import type { StyleMode } from "./types"; interface ComponentStatsProps { - component: RegistryItem - currentStyle: StyleMode + component: RegistryItem; + currentStyle: StyleMode; } interface DependencyStats { - total: number - installed: number - missing: string[] + total: number; + installed: number; + missing: string[]; } interface DependencyData { - dependencies: Record - devDependencies: Record + dependencies: Record; + devDependencies: Record; } interface Stats { - dependencies: DependencyStats - registryDependencies: DependencyStats - installedComponents: string[] + dependencies: DependencyStats; + registryDependencies: DependencyStats; + installedComponents: string[]; } export function ComponentStats({ component, currentStyle }: ComponentStatsProps) { - const [stats, setStats] = useState(null) - - useEffect(() => { - const loadStats = async () => { - if (!component) return - - try { - const [depsData, components] = await Promise.all([ - getDependencies(), - getInstalledComponents() - ]) - - const allDeps = { ...depsData.dependencies, ...depsData.devDependencies } - - // Check dependencies - const dependencies: DependencyStats = { - total: component.dependencies?.length || 0, - installed: 0, - missing: [] - } - - if (component.dependencies) { - for (const dep of component.dependencies) { - const parts = dep.split('@') - const name = parts[0] - if (name && name in allDeps) { - dependencies.installed++ - } else { - dependencies.missing.push(dep) - } - } - } - - // Check registry dependencies - const registryDependencies: DependencyStats = { - total: component.registryDependencies?.length || 0, - installed: 0, - missing: [] - } - - if (component.registryDependencies) { - for (const dep of component.registryDependencies) { - if (components.includes(dep)) { - registryDependencies.installed++ - } else { - registryDependencies.missing.push(dep) - } - } - } - - setStats({ - dependencies, - registryDependencies, - installedComponents: components - }) - } catch (err) { - console.error('Failed to load stats:', err) - } - } - - loadStats() - }, [component]) - - if (!component || !stats) { - return null - } - - return ( -
- {/* Dependencies */} - -

Dependencies

-
-
- - {stats.dependencies.installed} / {stats.dependencies.total} installed - - - {stats.dependencies.total - ? Math.round( - (stats.dependencies.installed / stats.dependencies.total) * 100 - ) - : 100} - % - -
- -
- {stats.dependencies.missing.length > 0 && ( -
-

Missing dependencies:

-
- {stats.dependencies.missing.map(dep => ( - - {dep} - - ))} -
-
- )} -
- - {/* Registry Dependencies */} - -

Registry Dependencies

-
-
- - {stats.registryDependencies.installed} / {stats.registryDependencies.total} installed - - - {stats.registryDependencies.total - ? Math.round( - (stats.registryDependencies.installed / stats.registryDependencies.total) * 100 - ) - : 100} - % - -
- -
- {stats.registryDependencies.missing.length > 0 && ( -
-

Missing components:

-
- {stats.registryDependencies.missing.map(dep => ( - - {dep} - - ))} -
-
- )} -
-
- ) + const [stats, setStats] = useState(null); + + useEffect(() => { + const loadStats = async () => { + if (!component) return; + + try { + const [depsData, components] = await Promise.all([ + getDependencies(), + getInstalledComponents(), + ]); + + const allDeps = { ...depsData.dependencies, ...depsData.devDependencies }; + + // Check dependencies + const dependencies: DependencyStats = { + total: component.dependencies?.length || 0, + installed: 0, + missing: [], + }; + + if (component.dependencies) { + for (const dep of component.dependencies) { + const parts = dep.split("@"); + const name = parts[0]; + if (name && name in allDeps) { + dependencies.installed++; + } else { + dependencies.missing.push(dep); + } + } + } + + // Check registry dependencies + const registryDependencies: DependencyStats = { + total: component.registryDependencies?.length || 0, + installed: 0, + missing: [], + }; + + if (component.registryDependencies) { + for (const dep of component.registryDependencies) { + if (components.includes(dep)) { + registryDependencies.installed++; + } else { + registryDependencies.missing.push(dep); + } + } + } + + setStats({ + dependencies, + registryDependencies, + installedComponents: components, + }); + } catch (err) { + console.error("Failed to load stats:", err); + } + }; + + loadStats(); + }, [component]); + + if (!component || !stats) { + return null; + } + + return ( +
+ {/* Dependencies */} + +

Dependencies

+
+
+ + {stats.dependencies.installed} / {stats.dependencies.total} installed + + + {stats.dependencies.total + ? Math.round((stats.dependencies.installed / stats.dependencies.total) * 100) + : 100} + % + +
+ +
+ {stats.dependencies.missing.length > 0 && ( +
+

Missing dependencies:

+
+ {stats.dependencies.missing.map((dep) => ( + + {dep} + + ))} +
+
+ )} +
+ + {/* Registry Dependencies */} + +

Registry Dependencies

+
+
+ + {stats.registryDependencies.installed} / {stats.registryDependencies.total} installed + + + {stats.registryDependencies.total + ? Math.round( + (stats.registryDependencies.installed / stats.registryDependencies.total) * 100 + ) + : 100} + % + +
+ +
+ {stats.registryDependencies.missing.length > 0 && ( +
+

Missing components:

+
+ {stats.registryDependencies.missing.map((dep) => ( + + {dep} + + ))} +
+
+ )} +
+
+ ); } - diff --git a/src/app/(app)/bones/cli-www/_components/custom-install-dialog.tsx b/src/app/(app)/bones/cli-www/_components/custom-install-dialog.tsx index 99ba324f..4ed09d3e 100644 --- a/src/app/(app)/bones/cli-www/_components/custom-install-dialog.tsx +++ b/src/app/(app)/bones/cli-www/_components/custom-install-dialog.tsx @@ -1,128 +1,127 @@ -'use client' +"use client"; -import { Button } from '@/components/ui/button' -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { toast } from '@/hooks/use-toast' -import { ReloadIcon } from '@radix-ui/react-icons' -import { Terminal as TerminalIcon } from 'lucide-react' -import { type FormEvent, memo, useCallback, useState } from 'react' -import { formatUrlToCommand, isValidCommand, isValidUrl } from '../_lib/registry-service' -import type { InstallationProgress } from './types' +import { Button } from "@/components/ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { toast } from "@/hooks/use-toast"; +import { ReloadIcon } from "@radix-ui/react-icons"; +import { Terminal as TerminalIcon } from "lucide-react"; +import { type FormEvent, memo, useCallback, useState } from "react"; +import { formatUrlToCommand, isValidCommand, isValidUrl } from "../_lib/registry-service"; +import type { InstallationProgress } from "./types"; interface CustomInstallDialogProps { - onInstall: (command: string) => void - installationProgress: InstallationProgress + onInstall: (command: string) => void; + installationProgress: InstallationProgress; } export const CustomInstallDialog = memo(({ onInstall }: CustomInstallDialogProps) => { - const [open, setOpen] = useState(false) - const [input, setInput] = useState('') - const [loading, setLoading] = useState(false) + const [open, setOpen] = useState(false); + const [input, setInput] = useState(""); + const [loading, setLoading] = useState(false); - const handleSubmit = useCallback(async (e: FormEvent) => { - e.preventDefault() - setLoading(true) + const handleSubmit = useCallback( + async (e: FormEvent) => { + e.preventDefault(); + setLoading(true); - try { - const trimmedInput = input.trim() - const finalCommand = isValidUrl(trimmedInput) - ? formatUrlToCommand(trimmedInput) - : isValidCommand(trimmedInput) - ? trimmedInput - : null + try { + const trimmedInput = input.trim(); + const finalCommand = isValidUrl(trimmedInput) + ? formatUrlToCommand(trimmedInput) + : isValidCommand(trimmedInput) + ? trimmedInput + : null; - if (!finalCommand) { - throw new Error('Please enter a valid URL or install command') - } + if (!finalCommand) { + throw new Error("Please enter a valid URL or install command"); + } - setOpen(false) - setInput('') - toast({ - title: "Command accepted", - description: "Starting installation...", - }) - await onInstall(finalCommand) - } catch (error) { - toast({ - title: "Invalid input", - description: error instanceof Error ? error.message : "Please check the format", - variant: "destructive", - }) - } finally { - setLoading(false) - } - }, [input, onInstall]) + setOpen(false); + setInput(""); + toast({ + title: "Command accepted", + description: "Starting installation...", + }); + await onInstall(finalCommand); + } catch (error) { + toast({ + title: "Invalid input", + description: error instanceof Error ? error.message : "Please check the format", + variant: "destructive", + }); + } finally { + setLoading(false); + } + }, + [input, onInstall] + ); - const handleOpenChange = useCallback((isOpen: boolean) => { - if (!loading || !isOpen) { - setOpen(isOpen) - } - }, [loading]) + const handleOpenChange = useCallback( + (isOpen: boolean) => { + if (!loading || !isOpen) { + setOpen(isOpen); + } + }, + [loading] + ); - return ( - - - - - e.stopPropagation()} - > -
-
-

Install Component

-

- Enter any component URL or install command. -

-
-
-
- - setInput(e.target.value)} - placeholder="npx shadcn@latest add ..." - required - disabled={loading} - /> -
-

Enter an install command or component URL...

-
-
-
- -
-
-
-
-
- ) -}) -CustomInstallDialog.displayName = 'CustomInstallDialog' + return ( + + + + + e.stopPropagation()}> +
+
+

Install Component

+

+ Enter any component URL or install command. +

+
+
+
+ + setInput(e.target.value)} + placeholder="npx shadcn@latest add ..." + required + disabled={loading} + /> +
+

Enter an install command or component URL...

+
+
+
+ +
+
+
+
+
+ ); +}); +CustomInstallDialog.displayName = "CustomInstallDialog"; diff --git a/src/app/(app)/bones/cli-www/_components/file-tree.tsx b/src/app/(app)/bones/cli-www/_components/file-tree.tsx index df31a374..51f6cac8 100644 --- a/src/app/(app)/bones/cli-www/_components/file-tree.tsx +++ b/src/app/(app)/bones/cli-www/_components/file-tree.tsx @@ -112,14 +112,14 @@ function TreeNode({ {node.type === "directory" ? ( <> {isExpanded ? ( - + ) : ( - + )} - + ) : ( - + )} {node.name}
@@ -142,12 +142,7 @@ function TreeNode({ ); } -export function FileTree({ - files, - onFileSelect, - selectedFile, - currentStyle, -}: FileTreeProps) { +export function FileTree({ files, onFileSelect, selectedFile, currentStyle }: FileTreeProps) { const tree = buildTree(files); return ( diff --git a/src/app/(app)/bones/cli-www/_components/preview.tsx b/src/app/(app)/bones/cli-www/_components/preview.tsx index ba1f6609..be9fe719 100644 --- a/src/app/(app)/bones/cli-www/_components/preview.tsx +++ b/src/app/(app)/bones/cli-www/_components/preview.tsx @@ -1,28 +1,30 @@ -'use client' +"use client"; -import { Card } from '@/components/ui/card' -import { cn } from '@/lib/utils' -import type { PreviewProps } from './types' +import { Card } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; +import type { PreviewProps } from "./types"; export function Preview({ component, currentStyle }: PreviewProps) { - return ( -
- -
-

Preview not available for {component.name}

-

Component type: {component.type}

-
-
+ return ( +
+ +
+

Preview not available for {component.name}

+

Component type: {component.type}

- ) +
+
+ ); } diff --git a/src/app/(app)/bones/cli-www/_components/styles.ts b/src/app/(app)/bones/cli-www/_components/styles.ts index 3a30776b..a2820828 100644 --- a/src/app/(app)/bones/cli-www/_components/styles.ts +++ b/src/app/(app)/bones/cli-www/_components/styles.ts @@ -1,62 +1,60 @@ import { cva } from "class-variance-authority"; export const containerStyles = cva( - "flex flex-col max-h-full w-full max-w-6xl mx-auto overflow-hidden text-base", - { - variants: { - style: { - brutalist: - "border-2 border-primary rounded-none shadow-[8px_8px_0px_0px_hsl(var(--primary)/.3)]", - modern: "border border-border rounded-lg shadow-md", - minimalist: "", - }, - }, - defaultVariants: { - style: "modern", - }, - }, + "flex flex-col max-h-full w-full max-w-6xl mx-auto overflow-hidden text-base", + { + variants: { + style: { + brutalist: + "border-2 border-primary rounded-none shadow-[8px_8px_0px_0px_hsl(var(--primary)/.3)]", + modern: "border border-border rounded-lg shadow-md", + minimalist: "", + }, + }, + defaultVariants: { + style: "modern", + }, + } ); export const headerStyles = cva("p-2 relative overflow-hidden", { - variants: { - style: { - brutalist: "border-b-4 border-primary", - modern: "", - minimalist: "", - }, - }, - defaultVariants: { - style: "modern", - }, + variants: { + style: { + brutalist: "border-b-4 border-primary", + modern: "", + minimalist: "", + }, + }, + defaultVariants: { + style: "modern", + }, }); export const buttonStyles = cva("transition-all", { - variants: { - style: { - brutalist: - "border-2 border-primary rounded-none hover:bg-primary hover:text-primary-foreground", - modern: - "border border-border rounded-md hover:bg-accent hover:text-accent-foreground transition-colors", - minimalist: - "border-none hover:bg-accent hover:text-accent-foreground transition-colors", - }, - }, - defaultVariants: { - style: "modern", - }, + variants: { + style: { + brutalist: + "border-2 border-primary rounded-none hover:bg-primary hover:text-primary-foreground", + modern: + "border border-border rounded-md hover:bg-accent hover:text-accent-foreground transition-colors", + minimalist: "border-none hover:bg-accent hover:text-accent-foreground transition-colors", + }, + }, + defaultVariants: { + style: "modern", + }, }); export const componentCardStyles = cva("cursor-pointer transition-all", { - variants: { - style: { - brutalist: - "hover:translate-x-1 hover:translate-y-1 hover:shadow-[4px_4px_0px_0px_hsl(var(--primary))] border-2 border-primary rounded-none", - modern: - "hover:shadow-lg border border-border rounded-lg transition-shadow duration-300", - minimalist: "hover:bg-accent border-b border-border", - }, - }, - defaultVariants: { - style: "modern", - }, + variants: { + style: { + brutalist: + "hover:translate-x-1 hover:translate-y-1 hover:shadow-[4px_4px_0px_0px_hsl(var(--primary))] border-2 border-primary rounded-none", + modern: "hover:shadow-lg border border-border rounded-lg transition-shadow duration-300", + minimalist: "hover:bg-accent border-b border-border", + }, + }, + defaultVariants: { + style: "modern", + }, }); diff --git a/src/app/(app)/bones/cli-www/_components/terminal.tsx b/src/app/(app)/bones/cli-www/_components/terminal.tsx index f0d0f145..f6d916a3 100644 --- a/src/app/(app)/bones/cli-www/_components/terminal.tsx +++ b/src/app/(app)/bones/cli-www/_components/terminal.tsx @@ -1,66 +1,68 @@ -'use client' +"use client"; -import { ScrollArea } from '@/components/ui/scroll-area' -import { cn } from '@/lib/utils' -import Convert from 'ansi-to-html' -import { useEffect, useMemo, useRef } from 'react' +import { ScrollArea } from "@/components/ui/scroll-area"; +import { cn } from "@/lib/utils"; +import Convert from "ansi-to-html"; +import { useEffect, useMemo, useRef } from "react"; interface TerminalProps { - output: string[] - className?: string + output: string[]; + className?: string; } // Clean up control sequences that ansi-to-html doesn't handle function cleanAnsi(text: string): string { - return text - // Remove cursor hide/show sequences - .replace(/\x1B\[\?25[hl]/g, '') - // Remove clear line and move cursor sequences - .replace(/\x1B\[2K/g, '') - .replace(/\x1B\[1G/g, '') - // Remove other control sequences - .replace(/\x1B\[\d*[ABCDEFGJKST]/g, '') + return ( + text + // Remove cursor hide/show sequences + .replace(/\x1B\[\?25[hl]/g, "") + // Remove clear line and move cursor sequences + .replace(/\x1B\[2K/g, "") + .replace(/\x1B\[1G/g, "") + // Remove other control sequences + .replace(/\x1B\[\d*[ABCDEFGJKST]/g, "") + ); } export function Terminal({ output, className }: TerminalProps) { - const scrollRef = useRef(null) - const convert = useMemo( - () => - new Convert({ - fg: '#D4D4D4', - bg: '#1E1E1E', - newline: false, - escapeXML: true, - stream: true, - }), - [] - ) + const scrollRef = useRef(null); + const convert = useMemo( + () => + new Convert({ + fg: "#D4D4D4", + bg: "#1E1E1E", + newline: false, + escapeXML: true, + stream: true, + }), + [] + ); - // Auto-scroll to bottom when output changes - useEffect(() => { - scrollRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }) - }, [output]) + // Auto-scroll to bottom when output changes + useEffect(() => { + scrollRef.current?.scrollIntoView({ behavior: "smooth", block: "end" }); + }, [output]); - // Process the output to handle newlines properly - const processedOutput = useMemo(() => { - return output.join('\n').split(/\r?\n/) - }, [output]) + // Process the output to handle newlines properly + const processedOutput = useMemo(() => { + return output.join("\n").split(/\r?\n/); + }, [output]); - return ( - -
- {processedOutput.map((line, i) => ( -
- ))} -
-
- - ) + return ( + +
+ {processedOutput.map((line, i) => ( +
+ ))} +
+
+ + ); } diff --git a/src/app/(app)/bones/cli-www/_components/types.ts b/src/app/(app)/bones/cli-www/_components/types.ts index d8416471..5d73a990 100644 --- a/src/app/(app)/bones/cli-www/_components/types.ts +++ b/src/app/(app)/bones/cli-www/_components/types.ts @@ -5,54 +5,54 @@ export type StyleMode = "brutalist" | "modern" | "minimalist"; export type PreviewMode = "none" | "single" | "all"; export interface ComponentCardProps { - component: RegistryItem; - currentStyle: StyleMode; - onOpenSidebar: (component: RegistryItem) => void; - showPreview?: boolean; - showAllVariants?: boolean; + component: RegistryItem; + currentStyle: StyleMode; + onOpenSidebar: (component: RegistryItem) => void; + showPreview?: boolean; + showAllVariants?: boolean; } export interface ActionButtonProps { - icon: ReactNode; - tooltip: string; - onClick: (e: MouseEvent) => void; - currentStyle: StyleMode; + icon: ReactNode; + tooltip: string; + onClick: (e: MouseEvent) => void; + currentStyle: StyleMode; } export interface BrowserHeaderProps { - currentStyle: StyleMode; - registries: Registry[]; - currentRegistry?: Registry; - onRegistryChange: (registry: Registry) => void; + currentStyle: StyleMode; + registries: Registry[]; + currentRegistry?: Registry; + onRegistryChange: (registry: Registry) => void; } export interface BrowserSidebarProps { - currentStyle: StyleMode; - searchTerm: string; - setSearchTerm: (term: string) => void; - filters: RegistryFilters; - setFilters: (filters: RegistryFilters) => void; - categories: string[]; - types: string[]; + currentStyle: StyleMode; + searchTerm: string; + setSearchTerm: (term: string) => void; + filters: RegistryFilters; + setFilters: (filters: RegistryFilters) => void; + categories: string[]; + types: string[]; } export interface InstallationProgress { - status: "idle" | "installing" | "success" | "error"; - message?: string; - log?: string; + status: "idle" | "installing" | "success" | "error"; + message?: string; + log?: string; } export interface PreviewProps { - component: RegistryItem; - currentStyle: StyleMode; + component: RegistryItem; + currentStyle: StyleMode; } export interface ComponentDetailsProps { - component: RegistryItem; - currentStyle?: StyleMode; - currentRegistry?: Registry; - onClose: () => void; - installationProgress: InstallationProgress; - onInstall: (component: RegistryItem) => void; - onHideInstallation: () => void; + component: RegistryItem; + currentStyle?: StyleMode; + currentRegistry?: Registry; + onClose: () => void; + installationProgress: InstallationProgress; + onInstall: (component: RegistryItem) => void; + onHideInstallation: () => void; } diff --git a/src/app/(app)/bones/cli-www/_hooks/use-registry.ts b/src/app/(app)/bones/cli-www/_hooks/use-registry.ts index df965c80..c8296ce6 100644 --- a/src/app/(app)/bones/cli-www/_hooks/use-registry.ts +++ b/src/app/(app)/bones/cli-www/_hooks/use-registry.ts @@ -5,160 +5,152 @@ import { fetchRegistryIndex, getRegistries } from "../_lib/registry-service"; import type { Registry, RegistryFilters, RegistryItem } from "../_lib/types"; export function useRegistry() { - const [registries, setRegistries] = useState([]); - const [currentRegistry, setCurrentRegistry] = useState( - undefined, - ); - const [items, setItems] = useState>({}); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [filters, setFilters] = useState({ - type: "all", - category: "all", - }); - const [searchQuery, setSearchQuery] = useState(""); + const [registries, setRegistries] = useState([]); + const [currentRegistry, setCurrentRegistry] = useState(undefined); + const [items, setItems] = useState>({}); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [filters, setFilters] = useState({ + type: "all", + category: "all", + }); + const [searchQuery, setSearchQuery] = useState(""); - useEffect(() => { - const loadRegistries = async () => { - try { - const registries = await getRegistries(); - setRegistries(registries); - } catch (error) { - setError( - error instanceof Error - ? error - : new Error("Failed to load registries"), - ); - } - }; - loadRegistries(); - }, []); + useEffect(() => { + const loadRegistries = async () => { + try { + const registries = await getRegistries(); + setRegistries(registries); + } catch (error) { + setError(error instanceof Error ? error : new Error("Failed to load registries")); + } + }; + loadRegistries(); + }, []); - useEffect(() => { - const loadItems = async () => { - setLoading(true); - setError(null); - try { - if (currentRegistry) { - const registryItems = await fetchRegistryIndex(currentRegistry.url); - if (Array.isArray(registryItems) && registryItems.length > 0) { - setItems({ - [currentRegistry.name]: registryItems.map((item) => ({ - ...item, - registry: currentRegistry.name, - componentUrl: `${currentRegistry.baseComponentUrl}/default/${item.name}.json`, - })), - }); - } else { - // If current registry is invalid, clear it and items - setCurrentRegistry(undefined); - setItems({}); - } - } else { - const allItems: Record = {}; + useEffect(() => { + const loadItems = async () => { + setLoading(true); + setError(null); + try { + if (currentRegistry) { + const registryItems = await fetchRegistryIndex(currentRegistry.url); + if (Array.isArray(registryItems) && registryItems.length > 0) { + setItems({ + [currentRegistry.name]: registryItems.map((item) => ({ + ...item, + registry: currentRegistry.name, + componentUrl: `${currentRegistry.baseComponentUrl}/default/${item.name}.json`, + })), + }); + } else { + // If current registry is invalid, clear it and items + setCurrentRegistry(undefined); + setItems({}); + } + } else { + const allItems: Record = {}; - for (const registry of registries) { - const items = await fetchRegistryIndex(registry.url); - if (Array.isArray(items) && items.length > 0) { - allItems[registry.name] = items.map((item) => ({ - ...item, - registry: registry.name, - componentUrl: `${registry.baseComponentUrl}/default/${item.name}.json`, - })); - } - } + for (const registry of registries) { + const items = await fetchRegistryIndex(registry.url); + if (Array.isArray(items) && items.length > 0) { + allItems[registry.name] = items.map((item) => ({ + ...item, + registry: registry.name, + componentUrl: `${registry.baseComponentUrl}/default/${item.name}.json`, + })); + } + } - if (Object.keys(allItems).length > 0) { - setItems(allItems); - } else { - setItems({}); - } - } - } catch (error) { - // Silently handle any remaining errors - setItems({}); - } finally { - setLoading(false); - } - }; - if (registries.length > 0) { - loadItems(); - } - }, [currentRegistry, registries]); + if (Object.keys(allItems).length > 0) { + setItems(allItems); + } else { + setItems({}); + } + } + } catch (error) { + // Silently handle any remaining errors + setItems({}); + } finally { + setLoading(false); + } + }; + if (registries.length > 0) { + loadItems(); + } + }, [currentRegistry, registries]); - const getCategories = () => { - const categories = new Set(); - for (const registryItems of Object.values(items)) { - for (const item of registryItems) { - if (item.categories) { - for (const category of item.categories) { - categories.add(category); - } - } - } - } - return Array.from(categories); - }; + const getCategories = () => { + const categories = new Set(); + for (const registryItems of Object.values(items)) { + for (const item of registryItems) { + if (item.categories) { + for (const category of item.categories) { + categories.add(category); + } + } + } + } + return Array.from(categories); + }; - const getTypes = () => { - const types = new Set(); - for (const registryItems of Object.values(items)) { - for (const item of registryItems) { - types.add(item.type); - } - } - return Array.from(types); - }; + const getTypes = () => { + const types = new Set(); + for (const registryItems of Object.values(items)) { + for (const item of registryItems) { + types.add(item.type); + } + } + return Array.from(types); + }; - const filteredItems = () => { - const allItems = Object.values(items).flat(); - const filtered = allItems.filter((item) => { - // Type filter - if (filters.type && filters.type !== "all") { - if (filters.type === "components" && item.type !== "registry:ui") { - return false; - } - if (filters.type === "blocks" && item.type !== "registry:block") { - return false; - } - } + const filteredItems = () => { + const allItems = Object.values(items).flat(); + const filtered = allItems.filter((item) => { + // Type filter + if (filters.type && filters.type !== "all") { + if (filters.type === "components" && item.type !== "registry:ui") { + return false; + } + if (filters.type === "blocks" && item.type !== "registry:block") { + return false; + } + } - // Category filter - if (filters.category && filters.category !== "all") { - if (!item.categories?.includes(filters.category)) { - return false; - } - } + // Category filter + if (filters.category && filters.category !== "all") { + if (!item.categories?.includes(filters.category)) { + return false; + } + } - // Search query - if (searchQuery) { - const searchString = `${item.name} ${item.description}`.toLowerCase(); - return searchString.includes(searchQuery.toLowerCase()); - } + // Search query + if (searchQuery) { + const searchString = `${item.name} ${item.description}`.toLowerCase(); + return searchString.includes(searchQuery.toLowerCase()); + } - return true; - }); + return true; + }); - // Sort by name - return filtered.sort((a: RegistryItem, b: RegistryItem) => - a.name.localeCompare(b.name), - ); - }; + // Sort by name + return filtered.sort((a: RegistryItem, b: RegistryItem) => a.name.localeCompare(b.name)); + }; - return { - registries, - currentRegistry, - items, - loading, - error, - filters, - searchQuery, - setCurrentRegistry, - setFilters, - setSearchQuery, - getCategories, - getTypes, - setRegistries, - filteredItems, - }; + return { + registries, + currentRegistry, + items, + loading, + error, + filters, + searchQuery, + setCurrentRegistry, + setFilters, + setSearchQuery, + getCategories, + getTypes, + setRegistries, + filteredItems, + }; } diff --git a/src/app/(app)/bones/cli-www/_lib/registry-service.ts b/src/app/(app)/bones/cli-www/_lib/registry-service.ts index b7718aab..4818be2e 100644 --- a/src/app/(app)/bones/cli-www/_lib/registry-service.ts +++ b/src/app/(app)/bones/cli-www/_lib/registry-service.ts @@ -7,41 +7,39 @@ import type { Registry, RegistryFilters, RegistryItem } from "./types"; * @see https://cli.bones.sh */ const BUILT_IN_REGISTRIES = [ - { - name: "ShipKit", - url: "https://shipkit.io/r/registry.json", - description: - "ShipKit integrations and blocks: auth, payments, email, analytics, CMS, storage and more", - baseComponentUrl: "https://shipkit.io/r", - baseBlockUrl: "https://shipkit.io/r", - baseDocsUrl: "https://shipkit.io/docs/features/registry", - }, - { - name: "shadcn/ui", - url: "https://ui.shadcn.com/r", - description: - "Official shadcn/ui component registry with customizable components and blocks", - baseComponentUrl: "https://ui.shadcn.com/registry/styles", - baseBlockUrl: "https://ui.shadcn.com/registry/blocks", - baseDocsUrl: "https://ui.shadcn.com/docs/components", - }, - { - name: "Magic UI", - url: "https://magicui.design/r/index.json", - description: - "Beautiful animated components and effects for modern web applications", - baseComponentUrl: "https://magicui.design/registry/styles", - baseBlockUrl: "https://magicui.design/registry/blocks", - baseDocsUrl: "https://magicui.design/docs/components", - }, - { - name: "Bones Registry", - url: "https://cli.bones.sh", - description: "Community-driven component registry", - baseComponentUrl: "https://cli.bones.sh", - baseBlockUrl: "https://cli.bones.sh", - baseDocsUrl: "https://cli.bones.sh/docs/components", - }, + { + name: "ShipKit", + url: "https://shipkit.io/r/registry.json", + description: + "ShipKit integrations and blocks: auth, payments, email, analytics, CMS, storage and more", + baseComponentUrl: "https://shipkit.io/r", + baseBlockUrl: "https://shipkit.io/r", + baseDocsUrl: "https://shipkit.io/docs/features/registry", + }, + { + name: "shadcn/ui", + url: "https://ui.shadcn.com/r", + description: "Official shadcn/ui component registry with customizable components and blocks", + baseComponentUrl: "https://ui.shadcn.com/registry/styles", + baseBlockUrl: "https://ui.shadcn.com/registry/blocks", + baseDocsUrl: "https://ui.shadcn.com/docs/components", + }, + { + name: "Magic UI", + url: "https://magicui.design/r/index.json", + description: "Beautiful animated components and effects for modern web applications", + baseComponentUrl: "https://magicui.design/registry/styles", + baseBlockUrl: "https://magicui.design/registry/blocks", + baseDocsUrl: "https://magicui.design/docs/components", + }, + { + name: "Bones Registry", + url: "https://cli.bones.sh", + description: "Community-driven component registry", + baseComponentUrl: "https://cli.bones.sh", + baseBlockUrl: "https://cli.bones.sh", + baseDocsUrl: "https://cli.bones.sh/docs/components", + }, ] as const; const STORAGE_KEY = "reg-browser:custom-registries"; @@ -53,26 +51,21 @@ const STORAGE_KEY = "reg-browser:custom-registries"; * items live at `/.json` */ function isFlatRegistryUrl(url: string): boolean { - return url.endsWith("registry.json"); + return url.endsWith("registry.json"); } function registryIndexUrl(registryUrl: string): string { - const trimmed = registryUrl.replace(/\/$/, ""); - if (isFlatRegistryUrl(trimmed) || trimmed.endsWith("index.json")) - return trimmed; - return `${trimmed}/index.json`; + const trimmed = registryUrl.replace(/\/$/, ""); + if (isFlatRegistryUrl(trimmed) || trimmed.endsWith("index.json")) return trimmed; + return `${trimmed}/index.json`; } function itemsFromIndex(data: unknown): RegistryItem[] { - if (Array.isArray(data)) return data as RegistryItem[]; - if ( - data && - typeof data === "object" && - Array.isArray((data as { items?: unknown }).items) - ) { - return (data as { items: RegistryItem[] }).items; - } - return []; + if (Array.isArray(data)) return data as RegistryItem[]; + if (data && typeof data === "object" && Array.isArray((data as { items?: unknown }).items)) { + return (data as { items: RegistryItem[] }).items; + } + return []; } export type RegistryName = (typeof BUILT_IN_REGISTRIES)[number]["name"]; @@ -81,26 +74,26 @@ export type RegistryName = (typeof BUILT_IN_REGISTRIES)[number]["name"]; * Get custom registries from local storage */ function getCustomRegistries(): Registry[] { - if (typeof window === "undefined") return []; - try { - const stored = localStorage.getItem(STORAGE_KEY); - return stored ? JSON.parse(stored) : []; - } catch (error) { - console.error("Failed to parse custom registries:", error); - return []; - } + if (typeof window === "undefined") return []; + try { + const stored = localStorage.getItem(STORAGE_KEY); + return stored ? JSON.parse(stored) : []; + } catch (error) { + console.error("Failed to parse custom registries:", error); + return []; + } } /** * Save custom registries to local storage */ function saveCustomRegistries(registries: Registry[]): void { - if (typeof window === "undefined") return; - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(registries)); - } catch (error) { - console.error("Failed to save custom registries:", error); - } + if (typeof window === "undefined") return; + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(registries)); + } catch (error) { + console.error("Failed to save custom registries:", error); + } } /** @@ -109,7 +102,7 @@ function saveCustomRegistries(registries: Registry[]): void { // Async signature kept stable for callers that await this API. // eslint-disable-next-line @typescript-eslint/require-await export async function getRegistries(): Promise { - return [...BUILT_IN_REGISTRIES, ...getCustomRegistries()]; + return [...BUILT_IN_REGISTRIES, ...getCustomRegistries()]; } /** @@ -117,66 +110,66 @@ export async function getRegistries(): Promise { * @throws Error if the registry is invalid */ export async function validateRegistry(registry: Registry): Promise { - // Check if registry with this name already exists - const existingRegistries = await getRegistries(); - if (existingRegistries.some((r) => r.name === registry.name)) { - throw new Error(`Registry "${registry.name}" already exists`); - } - - try { - const url = registryIndexUrl(registry.url); - - // Try to fetch the registry index - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to fetch registry: ${response.statusText}`); - } - - // Validate the registry structure (array, or `{ items }` from `npx shadcn build`) - const items = itemsFromIndex(await response.json()); - - // Validate at least one item has the correct structure - if (items.length === 0) { - throw new Error("Registry is empty"); - } - - const validItem = items.some( - (item) => - typeof item === "object" && - item !== null && - typeof item.name === "string" && - typeof item.type === "string" && - (item.type === "registry:ui" || item.type === "registry:block"), - ); - - if (!validItem) { - throw new Error("Registry does not contain valid components"); - } - } catch (error) { - if (error instanceof Error) { - throw error; - } - throw new Error("Failed to validate registry"); - } + // Check if registry with this name already exists + const existingRegistries = await getRegistries(); + if (existingRegistries.some((r) => r.name === registry.name)) { + throw new Error(`Registry "${registry.name}" already exists`); + } + + try { + const url = registryIndexUrl(registry.url); + + // Try to fetch the registry index + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch registry: ${response.statusText}`); + } + + // Validate the registry structure (array, or `{ items }` from `npx shadcn build`) + const items = itemsFromIndex(await response.json()); + + // Validate at least one item has the correct structure + if (items.length === 0) { + throw new Error("Registry is empty"); + } + + const validItem = items.some( + (item) => + typeof item === "object" && + item !== null && + typeof item.name === "string" && + typeof item.type === "string" && + (item.type === "registry:ui" || item.type === "registry:block") + ); + + if (!validItem) { + throw new Error("Registry does not contain valid components"); + } + } catch (error) { + if (error instanceof Error) { + throw error; + } + throw new Error("Failed to validate registry"); + } } /** * Add a custom registry */ export async function addCustomRegistry(registry: Registry): Promise { - await validateRegistry(registry); - const customRegistries = getCustomRegistries(); - customRegistries.push(registry); - saveCustomRegistries(customRegistries); + await validateRegistry(registry); + const customRegistries = getCustomRegistries(); + customRegistries.push(registry); + saveCustomRegistries(customRegistries); } /** * Remove a custom registry */ export function removeCustomRegistry(name: string): void { - const customRegistries = getCustomRegistries(); - const filtered = customRegistries.filter((r) => r.name !== name); - saveCustomRegistries(filtered); + const customRegistries = getCustomRegistries(); + const filtered = customRegistries.filter((r) => r.name !== name); + saveCustomRegistries(filtered); } /** @@ -184,201 +177,187 @@ export function removeCustomRegistry(name: string): void { */ // eslint-disable-next-line @typescript-eslint/require-await export async function getRegistry(name: RegistryName): Promise { - const registry = [...BUILT_IN_REGISTRIES, ...getCustomRegistries()].find( - (r) => r.name === name, - ); - if (!registry) { - throw new Error(`Registry ${name} not found`); - } - return registry; + const registry = [...BUILT_IN_REGISTRIES, ...getCustomRegistries()].find((r) => r.name === name); + if (!registry) { + throw new Error(`Registry ${name} not found`); + } + return registry; } /** * Fetch registry index with error handling and caching */ -export async function fetchRegistryIndex( - registryUrl: string, -): Promise { - const url = new URL(registryIndexUrl(registryUrl)); - - try { - const response = await fetch(url, { - next: { revalidate: 3600 }, // Cache for 1 hour - }).catch((error) => { - console.error(`Failed to fetch registry index from ${url}:`, error); - return; - }); - - if (!response) { - return []; - } - - return itemsFromIndex(await response.json()); - } catch (_error) { - return []; - } +export async function fetchRegistryIndex(registryUrl: string): Promise { + const url = new URL(registryIndexUrl(registryUrl)); + + try { + const response = await fetch(url, { + next: { revalidate: 3600 }, // Cache for 1 hour + }).catch((error) => { + console.error(`Failed to fetch registry index from ${url}:`, error); + return; + }); + + if (!response) { + return []; + } + + return itemsFromIndex(await response.json()); + } catch (_error) { + return []; + } } /** * Fetch item details with error handling and caching */ export async function fetchItemDetails( - baseUrl: string, - itemName: string, - style = "default", + baseUrl: string, + itemName: string, + style = "default" ): Promise { - const detailsUrl = isFlatRegistryUrl(baseUrl) - ? new URL(`${baseUrl.replace(/\/registry\.json$/, "")}/${itemName}.json`) - : new URL( - `${baseUrl.replace(/\/index\.json$/, "")}/styles/${style}/${itemName}.json`, - ); - - try { - const response = await fetch(detailsUrl, { - next: { revalidate: 3600 }, // Cache for 1 hour - }); - - if (!response.ok) { - throw new Error(`Failed to fetch item details: ${response.statusText}`); - } - - const data = await response.json(); - return { - ...data, - componentUrl: detailsUrl.toString(), - }; - } catch (error) { - console.error(`Failed to fetch item details from ${detailsUrl}:`, error); - throw error; - } + const detailsUrl = isFlatRegistryUrl(baseUrl) + ? new URL(`${baseUrl.replace(/\/registry\.json$/, "")}/${itemName}.json`) + : new URL(`${baseUrl.replace(/\/index\.json$/, "")}/styles/${style}/${itemName}.json`); + + try { + const response = await fetch(detailsUrl, { + next: { revalidate: 3600 }, // Cache for 1 hour + }); + + if (!response.ok) { + throw new Error(`Failed to fetch item details: ${response.statusText}`); + } + + const data = await response.json(); + return { + ...data, + componentUrl: detailsUrl.toString(), + }; + } catch (error) { + console.error(`Failed to fetch item details from ${detailsUrl}:`, error); + throw error; + } } /** * Categorize items by type (Components/Blocks) */ -export function categorizeItems( - items: RegistryItem[], -): Record { - return items.reduce( - (acc, item) => { - const category = item.type === "registry:block" ? "Blocks" : "Components"; - acc[category] ??= []; - acc[category].push(item); - return acc; - }, - {} as Record, - ); +export function categorizeItems(items: RegistryItem[]): Record { + return items.reduce( + (acc, item) => { + const category = item.type === "registry:block" ? "Blocks" : "Components"; + acc[category] ??= []; + acc[category].push(item); + return acc; + }, + {} as Record + ); } /** * Group items by their categories */ -export function groupItemsByType( - items: RegistryItem[], -): Record { - return items.reduce( - (acc, item) => { - const categories = item.categories ?? ["Uncategorized"]; - for (const category of categories) { - acc[category] ??= []; - acc[category].push(item); - } - return acc; - }, - {} as Record, - ); +export function groupItemsByType(items: RegistryItem[]): Record { + return items.reduce( + (acc, item) => { + const categories = item.categories ?? ["Uncategorized"]; + for (const category of categories) { + acc[category] ??= []; + acc[category].push(item); + } + return acc; + }, + {} as Record + ); } /** * Search and filter items */ export function searchItems( - items: RegistryItem[] | Record, - query = "", - filters: RegistryFilters = {}, + items: RegistryItem[] | Record, + query = "", + filters: RegistryFilters = {} ): RegistryItem[] { - // Convert items object to array if needed - const itemsArray = Array.isArray(items) ? items : Object.values(items).flat(); - - return itemsArray.filter((item) => { - // Type filter - if (filters.type && filters.type !== "all") { - if (filters.type === "components" && item.type !== "registry:ui") { - return false; - } - if (filters.type === "blocks" && item.type !== "registry:block") { - return false; - } - } - - // Category filter - if (filters.category && filters.category !== "all") { - if (!item.categories?.includes(filters.category)) { - return false; - } - } - - // Search query - if (query) { - const searchString = `${item.name} ${item.description}`.toLowerCase(); - return searchString.includes(query.toLowerCase()); - } - - return true; - }); + // Convert items object to array if needed + const itemsArray = Array.isArray(items) ? items : Object.values(items).flat(); + + return itemsArray.filter((item) => { + // Type filter + if (filters.type && filters.type !== "all") { + if (filters.type === "components" && item.type !== "registry:ui") { + return false; + } + if (filters.type === "blocks" && item.type !== "registry:block") { + return false; + } + } + + // Category filter + if (filters.category && filters.category !== "all") { + if (!item.categories?.includes(filters.category)) { + return false; + } + } + + // Search query + if (query) { + const searchString = `${item.name} ${item.description}`.toLowerCase(); + return searchString.includes(query.toLowerCase()); + } + + return true; + }); } /** * Get the install command for a component * @see https://ui.shadcn.com/docs/cli */ -export function getInstallCommand( - component: RegistryItem, - registry?: Registry, -) { - const componentUrl = - component.componentUrl ?? - `${registry?.baseComponentUrl}/default/${component.name}.json`; - return `npx shadcn@latest add "${componentUrl}"`; +export function getInstallCommand(component: RegistryItem, registry?: Registry) { + const componentUrl = + component.componentUrl ?? `${registry?.baseComponentUrl}/default/${component.name}.json`; + return `npx shadcn@latest add "${componentUrl}"`; } /** * Get the documentation URL for a component */ export function getDocumentationUrl( - component: RegistryItem, - registry?: Registry, + component: RegistryItem, + registry?: Registry ): string | undefined { - if (!registry?.baseDocsUrl) return undefined; - return `${registry.baseDocsUrl}/${component.name}`; + if (!registry?.baseDocsUrl) return undefined; + return `${registry.baseDocsUrl}/${component.name}`; } /** * Validate if a string is a URL */ export function isValidUrl(str: string): boolean { - try { - new URL(str.trim()); - return true; - } catch { - return false; - } + try { + new URL(str.trim()); + return true; + } catch { + return false; + } } /** * Validate if a string is a valid install command */ export function isValidCommand(str: string): boolean { - const commandPatterns = [ - /^(npx|pnpm dlx|bunx --bun) shadcn@latest add/, - /^(npx|pnpm dlx|bunx --bun) shadcn@latest add "https?:\/\/[^"]+"/, - ]; - return commandPatterns.some((pattern) => pattern.test(str.trim())); + const commandPatterns = [ + /^(npx|pnpm dlx|bunx --bun) shadcn@latest add/, + /^(npx|pnpm dlx|bunx --bun) shadcn@latest add "https?:\/\/[^"]+"/, + ]; + return commandPatterns.some((pattern) => pattern.test(str.trim())); } /** * Format a URL into a valid install command */ export function formatUrlToCommand(url: string): string { - return `npx shadcn@latest add "${url.trim()}"`; + return `npx shadcn@latest add "${url.trim()}"`; } diff --git a/src/app/(app)/bones/cli-www/_lib/types.ts b/src/app/(app)/bones/cli-www/_lib/types.ts index 96b274ad..c479a5b7 100644 --- a/src/app/(app)/bones/cli-www/_lib/types.ts +++ b/src/app/(app)/bones/cli-www/_lib/types.ts @@ -4,62 +4,54 @@ */ export interface Registry { - name: string; - url: string; - description?: string; - baseComponentUrl: string; - baseBlockUrl: string; - baseDocsUrl: string; - custom?: boolean; + name: string; + url: string; + description?: string; + baseComponentUrl: string; + baseBlockUrl: string; + baseDocsUrl: string; + custom?: boolean; } export interface RegistryItem { - name: string; - type: - | "registry:block" - | "registry:ui" - | "registry:hook" - | "registry:lib" - | "registry:component" - | "registry:theme" - | "registry:page"; - description?: string; - dependencies?: string[]; - categories?: string[]; - componentUrl?: string; - style?: string; - files?: Array<{ - path: string; - content?: string; - type?: - | "preview" - | "ui" - | "block" - | "hook" - | "lib" - | "component" - | "theme" - | "page"; - target?: string; - }>; - registryDependencies?: string[]; - registry?: string; - meta?: { - library?: string; - hasPreview?: boolean; - [key: string]: any; - }; + name: string; + type: + | "registry:block" + | "registry:ui" + | "registry:hook" + | "registry:lib" + | "registry:component" + | "registry:theme" + | "registry:page"; + description?: string; + dependencies?: string[]; + categories?: string[]; + componentUrl?: string; + style?: string; + files?: Array<{ + path: string; + content?: string; + type?: "preview" | "ui" | "block" | "hook" | "lib" | "component" | "theme" | "page"; + target?: string; + }>; + registryDependencies?: string[]; + registry?: string; + meta?: { + library?: string; + hasPreview?: boolean; + [key: string]: any; + }; } export interface RegistryFilters { - type?: "all" | "components" | "blocks"; - category?: string; - style?: string; + type?: "all" | "components" | "blocks"; + category?: string; + style?: string; } export interface InstallOptions { - overwrite?: boolean; - style?: string; - typescript?: boolean; - path?: string; + overwrite?: boolean; + style?: string; + typescript?: boolean; + path?: string; } diff --git a/src/app/(app)/bones/cli-www/page.tsx b/src/app/(app)/bones/cli-www/page.tsx index 82fe2595..5d6862a8 100644 --- a/src/app/(app)/bones/cli-www/page.tsx +++ b/src/app/(app)/bones/cli-www/page.tsx @@ -8,9 +8,7 @@ const spaceGrotesk = Space_Grotesk({ export default function Page() { return ( -
+
); diff --git a/src/app/(app)/changelog/[...slug]/layout.tsx b/src/app/(app)/changelog/[...slug]/layout.tsx index 97da9825..b571ba08 100644 --- a/src/app/(app)/changelog/[...slug]/layout.tsx +++ b/src/app/(app)/changelog/[...slug]/layout.tsx @@ -3,8 +3,8 @@ import type { ReactNode } from "react"; import { getChangelogEntry, isLegacyUnreleasedSlug, UNRELEASED_SLUG } from "@/lib/changelog"; interface Props { - children: ReactNode; - params: Promise<{ slug: string[] }>; + children: ReactNode; + params: Promise<{ slug: string[] }>; } /** @@ -20,12 +20,12 @@ interface Props { * still carry those slugs. They redirect to the stable slug instead of 404ing. */ export default async function ChangelogEntryLayout({ children, params }: Props) { - const { slug } = await params; - const key = slug.join("/"); - const entry = await getChangelogEntry(key); - if (!entry) { - if (isLegacyUnreleasedSlug(key)) permanentRedirect(`/changelog/${UNRELEASED_SLUG}`); - notFound(); - } - return children; + const { slug } = await params; + const key = slug.join("/"); + const entry = await getChangelogEntry(key); + if (!entry) { + if (isLegacyUnreleasedSlug(key)) permanentRedirect(`/changelog/${UNRELEASED_SLUG}`); + notFound(); + } + return children; } diff --git a/src/app/(app)/changelog/[...slug]/page.tsx b/src/app/(app)/changelog/[...slug]/page.tsx index 21f0a9bf..d1081d9f 100644 --- a/src/app/(app)/changelog/[...slug]/page.tsx +++ b/src/app/(app)/changelog/[...slug]/page.tsx @@ -52,7 +52,7 @@ export default async function ChangelogEntryPage({ params }: Props) { const displayDate = formatDate(entry.publishedAt); return ( -
+
-
+
{entry.badge && ( {entry.badge} @@ -88,7 +88,7 @@ export default async function ChangelogEntryPage({ params }: Props) { )}
-
+
{entry.content}
diff --git a/src/app/(app)/changelog/_components/changelog-entries.tsx b/src/app/(app)/changelog/_components/changelog-entries.tsx index 836dd04f..05979569 100644 --- a/src/app/(app)/changelog/_components/changelog-entries.tsx +++ b/src/app/(app)/changelog/_components/changelog-entries.tsx @@ -11,55 +11,55 @@ import { formatDate } from "@/lib/utils/format-date"; * into a soft 404. */ export async function ChangelogEntries() { - const entries = await getChangelogEntries(); + const entries = await getChangelogEntries(); - if (entries.length === 0) { - return

No changelog entries yet. Check back soon.

; - } + if (entries.length === 0) { + return

No changelog entries yet. Check back soon.

; + } - return ( -
-
+ return ( +
+
- {entries.map((entry) => { - const date = formatDate(entry.publishedAt); - return ( -
-
+ {entries.map((entry) => { + const date = formatDate(entry.publishedAt); + return ( +
+
-
- {entry.badge && ( - - {entry.badge} - - )} - {date && {date}} - - {entry.commitCount} commit - {entry.commitCount !== 1 ? "s" : ""} - -
+
+ {entry.badge && ( + + {entry.badge} + + )} + {date && {date}} + + {entry.commitCount} commit + {entry.commitCount !== 1 ? "s" : ""} + +
- -

- {entry.title} -

- + +

+ {entry.title} +

+ - {entry.description &&

{entry.description}

} + {entry.description &&

{entry.description}

} - {entry.categories.length > 0 && ( -
- {entry.categories.map((cat) => ( - - {cat} - - ))} -
- )} -
- ); - })} -
- ); + {entry.categories.length > 0 && ( +
+ {entry.categories.map((cat) => ( + + {cat} + + ))} +
+ )} +
+ ); + })} +
+ ); } diff --git a/src/app/(app)/changelog/layout.tsx b/src/app/(app)/changelog/layout.tsx index fa1d87d8..1286c05e 100644 --- a/src/app/(app)/changelog/layout.tsx +++ b/src/app/(app)/changelog/layout.tsx @@ -1,9 +1,5 @@ import type { ReactNode } from "react"; export default function ChangelogLayout({ children }: { children: ReactNode }) { - return ( -
- {children} -
- ); + return
{children}
; } diff --git a/src/app/(app)/changelog/page.tsx b/src/app/(app)/changelog/page.tsx index e3903609..bf683c46 100644 --- a/src/app/(app)/changelog/page.tsx +++ b/src/app/(app)/changelog/page.tsx @@ -9,18 +9,18 @@ export const revalidate = 3600; export const metadata: Metadata = constructMetadata(routeMetadata.changelog); export default function ChangelogPage() { - return ( -
-
-

Changelog

-

New updates, features, and fixes.

-
+ return ( +
+
+

Changelog

+

New updates, features, and fixes.

+
- {/* In-page Suspense keeps the skeleton without a segment loading.tsx, + {/* In-page Suspense keeps the skeleton without a segment loading.tsx, which would also wrap [...slug] and break its 404 status. */} - }> - - -
- ); + }> + + +
+ ); } diff --git a/src/app/(app)/cli/_components/install-section.tsx b/src/app/(app)/cli/_components/install-section.tsx index c47b56a1..26d0e106 100644 --- a/src/app/(app)/cli/_components/install-section.tsx +++ b/src/app/(app)/cli/_components/install-section.tsx @@ -126,11 +126,7 @@ export function InstallSection() { onClick={copyToClipboard} className="grow bg-blue-600 text-white hover:bg-blue-700" > - {copied ? ( - - ) : ( - - )} + {copied ? : } {copied ? "Copied!" : "Copy Command"} diff --git a/src/app/(app)/cli/page.tsx b/src/app/(app)/cli/page.tsx index a9b5fbf6..42668a7d 100644 --- a/src/app/(app)/cli/page.tsx +++ b/src/app/(app)/cli/page.tsx @@ -5,29 +5,29 @@ import Link from "next/link"; import { InstallSection } from "./_components/install-section"; export default function Home() { - return ( -
-
-
- + return ( +
+
+
+ -
-

- Run this command in your Next.js project after setting up{" "} - ShadCN UI. -
- The Next.js starter for Shadcn/UI is{" "} - Shipkit Bones. -

- - Get more components and ship even faster with Shipkit. - -
-
-
-
- ); +
+

+ Run this command in your Next.js project after setting up{" "} + ShadCN UI. +
+ The Next.js starter for Shadcn/UI is{" "} + Shipkit Bones. +

+ + Get more components and ship even faster with Shipkit. + +
+
+
+
+ ); } diff --git a/src/app/(app)/contact/page.tsx b/src/app/(app)/contact/page.tsx index ab1d21b8..5bd50545 100644 --- a/src/app/(app)/contact/page.tsx +++ b/src/app/(app)/contact/page.tsx @@ -26,7 +26,7 @@ export default function ContactPage() {
Home diff --git a/src/app/(app)/global-error.tsx b/src/app/(app)/global-error.tsx index acf0f695..c6a70797 100644 --- a/src/app/(app)/global-error.tsx +++ b/src/app/(app)/global-error.tsx @@ -10,85 +10,85 @@ import type { CSSProperties } from "react"; interface GlobalErrorProps { - error: Error & { digest?: string }; - reset: () => void; + error: Error & { digest?: string }; + reset: () => void; } const styles = { - body: { - margin: 0, - minHeight: "100vh", - display: "flex", - alignItems: "center", - justifyContent: "center", - padding: "2rem", - background: "#fafafa", - color: "#171717", - fontFamily: - 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', - textAlign: "center", - WebkitFontSmoothing: "antialiased", - }, - card: { maxWidth: "28rem" }, - title: { fontSize: "1.75rem", fontWeight: 600, letterSpacing: "-0.01em", margin: "0 0 0.75rem" }, - text: { color: "#525252", lineHeight: 1.6, margin: "0 0 1.5rem" }, - actions: { display: "flex", flexWrap: "wrap", gap: "0.75rem", justifyContent: "center" }, - primary: { - background: "#171717", - color: "#fafafa", - border: "1px solid #171717", - borderRadius: "0.5rem", - padding: "0.6rem 1.1rem", - fontSize: "0.9rem", - fontWeight: 500, - cursor: "pointer", - }, - secondary: { - display: "inline-block", - background: "#fff", - color: "#171717", - border: "1px solid #d4d4d4", - borderRadius: "0.5rem", - padding: "0.6rem 1.1rem", - fontSize: "0.9rem", - fontWeight: 500, - textDecoration: "none", - }, - meta: { fontSize: "0.75rem", color: "#737373", marginTop: "1.5rem" }, - pre: { - marginTop: "1.5rem", - padding: "0.75rem", - background: "#f5f5f5", - borderRadius: "0.5rem", - fontSize: "0.75rem", - textAlign: "left", - overflow: "auto", - }, + body: { + margin: 0, + minHeight: "100vh", + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "2rem", + background: "#fafafa", + color: "#171717", + fontFamily: + 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', + textAlign: "center", + WebkitFontSmoothing: "antialiased", + }, + card: { maxWidth: "28rem" }, + title: { fontSize: "1.75rem", fontWeight: 600, letterSpacing: "-0.01em", margin: "0 0 0.75rem" }, + text: { color: "#525252", lineHeight: 1.6, margin: "0 0 1.5rem" }, + actions: { display: "flex", flexWrap: "wrap", gap: "0.75rem", justifyContent: "center" }, + primary: { + background: "#171717", + color: "#fafafa", + border: "1px solid #171717", + borderRadius: "0.5rem", + padding: "0.6rem 1.1rem", + fontSize: "0.9rem", + fontWeight: 500, + cursor: "pointer", + }, + secondary: { + display: "inline-block", + background: "#fff", + color: "#171717", + border: "1px solid #d4d4d4", + borderRadius: "0.5rem", + padding: "0.6rem 1.1rem", + fontSize: "0.9rem", + fontWeight: 500, + textDecoration: "none", + }, + meta: { fontSize: "0.75rem", color: "#737373", marginTop: "1.5rem" }, + pre: { + marginTop: "1.5rem", + padding: "0.75rem", + background: "#f5f5f5", + borderRadius: "0.5rem", + fontSize: "0.75rem", + textAlign: "left", + overflow: "auto", + }, } satisfies Record; export default function GlobalError({ error, reset }: GlobalErrorProps) { - const isDevelopment = process.env.NODE_ENV === "development"; + const isDevelopment = process.env.NODE_ENV === "development"; - return ( - - -
-

Something went wrong

-

- The rest of the site is working. Try again, or head back to the homepage. -

-
- - - Go to homepage - -
- {error.digest && !isDevelopment &&

Reference {error.digest}

} - {isDevelopment &&
{error.message}
} -
- - - ); + return ( + + +
+

Something went wrong

+

+ The rest of the site is working. Try again, or head back to the homepage. +

+
+ + + Go to homepage + +
+ {error.digest && !isDevelopment &&

Reference {error.digest}

} + {isDevelopment &&
{error.message}
} +
+ + + ); } diff --git a/src/app/(app)/not-found.tsx b/src/app/(app)/not-found.tsx index 8934f558..79309d44 100644 --- a/src/app/(app)/not-found.tsx +++ b/src/app/(app)/not-found.tsx @@ -5,9 +5,9 @@ import { RootLayout } from "@/components/layouts/root-layout"; import { - PageHeader, - PageHeaderDescription, - PageHeaderHeading, + PageHeader, + PageHeaderDescription, + PageHeaderHeading, } from "@/components/primitives/page-header"; import { WavesBackground } from "@/components/ui/background-waves"; import { buttonVariants } from "@/components/ui/button"; @@ -16,50 +16,50 @@ import { RocketIcon } from "lucide-react"; import Link from "next/link"; const config = { - speed: 0.002, - saturation: 12, - brightness: 60, - amplitude: 1, - frequency: 1, - layers: 3, - fadeOpacity: 0.05, - transparent: true, + speed: 0.002, + saturation: 12, + brightness: 60, + amplitude: 1, + frequency: 1, + layers: 3, + fadeOpacity: 0.05, + transparent: true, }; export default function NotFound() { - return ( - - -
-
-
- -
- - Lost in space - - The page you're looking for has drifted into deep space. Let's get you back - to familiar territory. - - - - Launch me back to earth -
- -

- We can't find the page you're looking for. -

-
-
- - ); + return ( + + +
+
+
+ +
+ + Lost in space + + The page you're looking for has drifted into deep space. Let's get you back + to familiar territory. + + + + Launch me back to earth +
+ +

+ We can't find the page you're looking for. +

+
+
+ + ); } diff --git a/src/app/(app)/og/route.tsx b/src/app/(app)/og/route.tsx index 1f78e33a..d4694bbc 100644 --- a/src/app/(app)/og/route.tsx +++ b/src/app/(app)/og/route.tsx @@ -32,318 +32,318 @@ import { v4 } from "uuid"; // eslint-disable-next-line @typescript-eslint/require-await export async function GET(req: NextRequest) { - try { - // const [interRegularData, interBoldData] = await Promise.all([ - // interRegular, - // interBold, - // ]); + try { + // const [interRegularData, interBoldData] = await Promise.all([ + // interRegular, + // interBold, + // ]); - const { searchParams } = new URL(req.url); - const title = searchParams.get("title") ?? siteConfig.title; - const mode = searchParams.get("mode") ?? "dark"; - const description = searchParams.get("description") ?? siteConfig.description; - const type = searchParams.get("type") ?? "default"; + const { searchParams } = new URL(req.url); + const title = searchParams.get("title") ?? siteConfig.title; + const mode = searchParams.get("mode") ?? "dark"; + const description = searchParams.get("description") ?? siteConfig.description; + const type = searchParams.get("type") ?? "default"; - const isDark = mode === "dark"; - const bgColor = isDark ? "#000000" : "#ffffff"; + const isDark = mode === "dark"; + const bgColor = isDark ? "#000000" : "#ffffff"; - // Generate random positions for stars - const stars = Array.from({ length: 100 }, () => ({ - x: Math.random() * 100, - y: Math.random() * 100, - size: Math.random() * 3 + 1, - opacity: Math.random() * 0.5 + 0.3, - })); + // Generate random positions for stars + const stars = Array.from({ length: 100 }, () => ({ + x: Math.random() * 100, + y: Math.random() * 100, + size: Math.random() * 3 + 1, + opacity: Math.random() * 0.5 + 0.3, + })); - // Generate random positions for nebula clouds - const nebulaClouds = Array.from({ length: 5 }, () => ({ - x: Math.random() * 100, - y: Math.random() * 100, - size: Math.random() * 300 + 200, - rotation: Math.random() * 360, - color: [ - "rgba(147, 51, 234, 0.1)", - "rgba(59, 130, 246, 0.1)", - "rgba(236, 72, 153, 0.1)", - "rgba(16, 185, 129, 0.1)", - "rgba(245, 158, 11, 0.1)", - ][Math.floor(Math.random() * 5)], - })); + // Generate random positions for nebula clouds + const nebulaClouds = Array.from({ length: 5 }, () => ({ + x: Math.random() * 100, + y: Math.random() * 100, + size: Math.random() * 300 + 200, + rotation: Math.random() * 360, + color: [ + "rgba(147, 51, 234, 0.1)", + "rgba(59, 130, 246, 0.1)", + "rgba(236, 72, 153, 0.1)", + "rgba(16, 185, 129, 0.1)", + "rgba(245, 158, 11, 0.1)", + ][Math.floor(Math.random() * 5)], + })); - return new ImageResponse( -
- {/* Deep Space Background */} -
+ return new ImageResponse( +
+ {/* Deep Space Background */} +
- {/* Nebula Clouds */} - {nebulaClouds.map((cloud) => ( -
- ))} + {/* Nebula Clouds */} + {nebulaClouds.map((cloud) => ( +
+ ))} - {/* Star Field */} - {stars.map((star) => ( -
- ))} + {/* Star Field */} + {stars.map((star) => ( +
+ ))} - {/* Orbital Rings */} -
-
-
+ {/* Orbital Rings */} +
+
+
- {/* Content Container */} -
- {/* Logo */} -
- {/* Logo Background Glow */} -
-
- 🚀{/* Rocket Trail */} -
-
-
- Shipkit -
-
+ padding: "48px", + background: isDark + ? "linear-gradient(135deg, rgba(17, 24, 39, 0.9), rgba(0, 0, 0, 0.8))" + : "linear-gradient(135deg, rgba(255, 255, 255, 0.9), rgba(243, 244, 246, 0.8))", + borderRadius: "32px", + border: `1px solid ${isDark ? "rgba(59, 130, 246, 0.3)" : "rgba(37, 99, 235, 0.2)"}`, + boxShadow: isDark + ? "0 0 100px rgba(59, 130, 246, 0.2)" + : "0 0 100px rgba(37, 99, 235, 0.1)", + backdropFilter: "blur(20px)", + }} + > + {/* Logo */} +
+ {/* Logo Background Glow */} +
+
+ 🚀{/* Rocket Trail */} +
+
+
+ Shipkit +
+
- {/* Title */} -
- {/* Title Glow Effect */} -
-

- {title} -

-

- {description} -

-
+ {/* Title */} +
+ {/* Title Glow Effect */} +
+

+ {title} +

+

+ {description} +

+
- {/* Type Badge */} - {type !== "default" && ( -
- {type} -
- )} -
-
, - { - width: 1200, - height: 630, - // fonts: [ - // { - // name: "Inter", - // data: interRegularData, - // weight: 400, - // style: "normal", - // }, - // { - // name: "Inter", - // data: interBoldData, - // weight: 700, - // style: "normal", - // }, - // ], - } - ); - } catch (e) { - console.error(e); - return new Response("Failed to generate image", { status: 500 }); - } + {/* Type Badge */} + {type !== "default" && ( +
+ {type} +
+ )} +
+
, + { + width: 1200, + height: 630, + // fonts: [ + // { + // name: "Inter", + // data: interRegularData, + // weight: 400, + // style: "normal", + // }, + // { + // name: "Inter", + // data: interBoldData, + // weight: 700, + // style: "normal", + // }, + // ], + } + ); + } catch (e) { + console.error(e); + return new Response("Failed to generate image", { status: 500 }); + } } diff --git a/src/app/(app)/page.tsx b/src/app/(app)/page.tsx index c0007adc..dc985da6 100644 --- a/src/app/(app)/page.tsx +++ b/src/app/(app)/page.tsx @@ -34,7 +34,8 @@ export default function Page() { softwareSourceCode product={{ name: "Bones - Next.js SaaS Boilerplate", - description: "Free, open-source Next.js starter kit with authentication, Shadcn UI, and one-click Vercel deploy.", + description: + "Free, open-source Next.js starter kit with authentication, Shadcn UI, and one-click Vercel deploy.", image: "https://bones.sh/og", price: "0", priceCurrency: "USD", @@ -43,29 +44,33 @@ export default function Page() { questions: [ { question: "Is Bones free to use?", - answer: "Yes, Bones is completely free and open-source under the MIT license. You own 100% of the code.", + answer: + "Yes, Bones is completely free and open-source under the MIT license. You own 100% of the code.", }, { question: "What tech stack does Bones use?", - answer: "Bones uses Next.js 16, React 19, TypeScript, Tailwind CSS, Shadcn/UI, and supports NextAuth + Better Auth for authentication.", + answer: + "Bones uses Next.js 16, React 19, TypeScript, Tailwind CSS, Shadcn/UI, and supports NextAuth + Better Auth for authentication.", }, { question: "How do I deploy Bones?", - answer: "Click the Deploy with Vercel button for one-click deployment. No environment variables are needed to start — the setup wizard guides you through configuration.", + answer: + "Click the Deploy with Vercel button for one-click deployment. No environment variables are needed to start — the setup wizard guides you through configuration.", }, { question: "What is the difference between Bones and ShipKit?", - answer: "Bones is the free, open-source foundation. ShipKit is the premium framework built on top of Bones that adds database (Drizzle + PostgreSQL), payments (Stripe), CMS (Payload + Builder.io), AI integrations, and 100+ additional components.", + answer: + "Bones is the free, open-source foundation. ShipKit is the premium framework built on top of Bones that adds database (Drizzle + PostgreSQL), payments (Stripe), CMS (Payload + Builder.io), AI integrations, and 100+ additional components.", }, ], }} /> -
+
Bones @@ -74,8 +79,8 @@ export default function Page() { Ship websites fast with AI. - Launch a full-stack React + Next.js starter kit configured for - Cursor, Claude, and Shadcn UI. + Launch a full-stack React + Next.js starter kit configured for Cursor, Claude, and + Shadcn UI.
@@ -93,7 +98,7 @@ export default function Page() { href={routes.external.shipkit} className={cn( buttonVariants({ variant: "link", size: "sm" }), - "text-muted-foreground hover:text-primary transition-colors", + "text-muted-foreground transition-colors hover:text-primary" )} > Need a starter with batteries included? diff --git a/src/app/manifest.ts b/src/app/manifest.ts index f7061949..4b30f713 100644 --- a/src/app/manifest.ts +++ b/src/app/manifest.ts @@ -3,155 +3,155 @@ import { siteConfig } from "@/config/site-config"; import type { MetadataRoute } from "next"; export default function manifest(): MetadataRoute.Manifest { - return { - /* Basic Application Information - * @see https://developer.mozilla.org/en-US/docs/Web/Manifest#name - */ - name: siteConfig.title, // Full name of the application - short_name: siteConfig.name, // Short name for app shortcuts and home screen - description: siteConfig.description, // Description shown in app stores and install prompts + return { + /* Basic Application Information + * @see https://developer.mozilla.org/en-US/docs/Web/Manifest#name + */ + name: siteConfig.title, // Full name of the application + short_name: siteConfig.name, // Short name for app shortcuts and home screen + description: siteConfig.description, // Description shown in app stores and install prompts - /* Application Entry Points and Identification - * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/start_url - */ - start_url: routes.home, // URL that loads when app is launched - id: routes.home, // Unique identifier for the app + /* Application Entry Points and Identification + * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/start_url + */ + start_url: routes.home, // URL that loads when app is launched + id: routes.home, // Unique identifier for the app - /* Display and UI Settings - * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/display - */ - display: "standalone", // App appears as standalone application (no browser UI) - display_override: ["window-controls-overlay"], // Enables customization of title bar in PWA + /* Display and UI Settings + * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/display + */ + display: "standalone", // App appears as standalone application (no browser UI) + display_override: ["window-controls-overlay"], // Enables customization of title bar in PWA - /* Theme and Color Settings - * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/theme_color - */ - background_color: siteConfig.metadata.themeColor.light, // Background color during app load - theme_color: siteConfig.metadata.themeColor.dark, // Theme color for OS integration + /* Theme and Color Settings + * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/theme_color + */ + background_color: siteConfig.metadata.themeColor.light, // Background color during app load + theme_color: siteConfig.metadata.themeColor.dark, // Theme color for OS integration - /* Device and Locale Settings - * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/orientation - */ - orientation: "portrait-primary", // Preferred screen orientation - categories: ["development", "productivity", "utilities"], // App store categories - dir: "ltr", // Text direction - lang: "en-US", // Primary language + /* Device and Locale Settings + * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/orientation + */ + orientation: "portrait-primary", // Preferred screen orientation + categories: ["development", "productivity", "utilities"], // App store categories + dir: "ltr", // Text direction + lang: "en-US", // Primary language - /* Installation and Platform Preferences - * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/prefer_related_applications - */ - prefer_related_applications: false, // Don't prefer native apps over this web app - scope: "/", // Pages that are part of the PWA experience + /* Installation and Platform Preferences + * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/prefer_related_applications + */ + prefer_related_applications: false, // Don't prefer native apps over this web app + scope: "/", // Pages that are part of the PWA experience - /* Launch Behavior - * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/launch_handler - */ - launch_handler: { - client_mode: ["navigate-existing", "auto"], // Controls how app launches when clicked - }, + /* Launch Behavior + * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/launch_handler + */ + launch_handler: { + client_mode: ["navigate-existing", "auto"], // Controls how app launches when clicked + }, - /* App Icons - * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/icons - */ - icons: [ - { - src: "/favicon.ico", // Standard favicon - sizes: "48x48", - type: "image/x-icon", - }, - { - src: "/app/web-app-manifest-192x192.png", - sizes: "192x192", - type: "image/png", - purpose: "maskable", - }, - { - src: "/app/web-app-manifest-192x192.png", - sizes: "192x192", - type: "image/png", - purpose: "any", - }, - { - src: "/app/web-app-manifest-512x512.png", - sizes: "512x512", - type: "image/png", - purpose: "maskable", - }, - ], + /* App Icons + * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/icons + */ + icons: [ + { + src: "/favicon.ico", // Standard favicon + sizes: "48x48", + type: "image/x-icon", + }, + { + src: "/app/web-app-manifest-192x192.png", + sizes: "192x192", + type: "image/png", + purpose: "maskable", + }, + { + src: "/app/web-app-manifest-192x192.png", + sizes: "192x192", + type: "image/png", + purpose: "any", + }, + { + src: "/app/web-app-manifest-512x512.png", + sizes: "512x512", + type: "image/png", + purpose: "maskable", + }, + ], - /* App Screenshots - * Used in app stores and install prompts to showcase the app - * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/screenshots - * @note You need to create these screenshot files in `public/screenshots/` - */ - screenshots: [ - { - src: "/app/screenshots/screenshot-desktop.png", // Updated path - sizes: "1280x720", - type: "image/png", - form_factor: "wide", // Indicates this is for wide screens (desktop) - label: `Desktop view of ${siteConfig.branding.projectName}`, - }, - { - src: "/app/screenshots/screenshot-mobile.png", // Updated path - sizes: "720x1280", - type: "image/png", - form_factor: "narrow", // Indicates this is for narrow screens (mobile) - label: `Mobile view of ${siteConfig.branding.projectName}`, - }, - ], + /* App Screenshots + * Used in app stores and install prompts to showcase the app + * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/screenshots + * @note You need to create these screenshot files in `public/screenshots/` + */ + screenshots: [ + { + src: "/app/screenshots/screenshot-desktop.png", // Updated path + sizes: "1280x720", + type: "image/png", + form_factor: "wide", // Indicates this is for wide screens (desktop) + label: `Desktop view of ${siteConfig.branding.projectName}`, + }, + { + src: "/app/screenshots/screenshot-mobile.png", // Updated path + sizes: "720x1280", + type: "image/png", + form_factor: "narrow", // Indicates this is for narrow screens (mobile) + label: `Mobile view of ${siteConfig.branding.projectName}`, + }, + ], - /* App Shortcuts (currently commented out) - * Provides quick access to key features from the app icon - * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/shortcuts - */ - // shortcuts: [ - // { - // name: "Documentation", - // short_name: "Docs", - // description: `View ${siteConfig.branding.projectName} documentation`, - // url: routes.docs, - // icons: [{ src: "/icons/docs.png", sizes: "192x192" }], - // }, - // { - // name: "Examples", - // short_name: "Examples", - // description: `View ${siteConfig.branding.projectName} examples`, - // url: routes.examples.root, - // icons: [{ src: "/icons/examples.png", sizes: "192x192" }], - // }, - // ], + /* App Shortcuts (currently commented out) + * Provides quick access to key features from the app icon + * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/shortcuts + */ + // shortcuts: [ + // { + // name: "Documentation", + // short_name: "Docs", + // description: `View ${siteConfig.branding.projectName} documentation`, + // url: routes.docs, + // icons: [{ src: "/icons/docs.png", sizes: "192x192" }], + // }, + // { + // name: "Examples", + // short_name: "Examples", + // description: `View ${siteConfig.branding.projectName} examples`, + // url: routes.examples.root, + // icons: [{ src: "/icons/examples.png", sizes: "192x192" }], + // }, + // ], - /* Web Share Target - * Allows the app to receive shared content from other apps - * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target - */ - share_target: { - action: "/share", - method: "POST", // Changed from GET to POST - enctype: "application/x-www-form-urlencoded", // Added enctype for POST - params: { - title: "title", - text: "text", - url: "url", - }, - }, + /* Web Share Target + * Allows the app to receive shared content from other apps + * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target + */ + share_target: { + action: "/share", + method: "POST", // Changed from GET to POST + enctype: "application/x-www-form-urlencoded", // Added enctype for POST + params: { + title: "title", + text: "text", + url: "url", + }, + }, - /* Custom Protocol Handlers - * Allows the app to handle custom URL schemes - * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/protocol_handlers - */ - protocol_handlers: [ - { - protocol: siteConfig.branding.protocol, // Custom protocol scheme - url: "/protocol?type=%s", // URL that handles the protocol - }, - ], + /* Custom Protocol Handlers + * Allows the app to handle custom URL schemes + * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/protocol_handlers + */ + protocol_handlers: [ + { + protocol: siteConfig.branding.protocol, // Custom protocol scheme + url: "/protocol?type=%s", // URL that handles the protocol + }, + ], - /* Related Applications - * Links to related native applications - * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/related_applications - */ - related_applications: [], // No related native apps - }; + /* Related Applications + * Links to related native applications + * @see https://developer.mozilla.org/en-US/docs/Web/Manifest/related_applications + */ + related_applications: [], // No related native apps + }; } diff --git a/src/app/robots.ts b/src/app/robots.ts index b8a8c605..909dfca7 100644 --- a/src/app/robots.ts +++ b/src/app/robots.ts @@ -6,45 +6,45 @@ import type { MetadataRoute } from "next"; * @see https://developers.google.com/search/docs/crawling-indexing/robots/create-robots-txt */ export default function robots(): MetadataRoute.Robots { - return { - /* Rules array defines crawler behavior - * Each rule object specifies which robots it applies to and what they can/cannot access - */ - rules: [ - { - userAgent: "*", // Applies to all web robots/crawlers - allow: "/", // Allows crawling of the root path and all unspecified paths + return { + /* Rules array defines crawler behavior + * Each rule object specifies which robots it applies to and what they can/cannot access + */ + rules: [ + { + userAgent: "*", // Applies to all web robots/crawlers + allow: "/", // Allows crawling of the root path and all unspecified paths - /* Paths that should not be crawled or indexed - * @note Common patterns to exclude: - * - API routes to prevent unnecessary crawling of data endpoints - * - Next.js internal routes and static files - * - Admin areas for security - * - Draft/preview content - * - Search pages to avoid duplicate content - */ + /* Paths that should not be crawled or indexed + * @note Common patterns to exclude: + * - API routes to prevent unnecessary crawling of data endpoints + * - Next.js internal routes and static files + * - Admin areas for security + * - Draft/preview content + * - Search pages to avoid duplicate content + */ - disallow: [ - // "/api/*", // Blocks access to API routes - // "/static/*", // Blocks access to static assets directory - // routes.admin.root, // Blocks access to admin panel - // "*/private/*", // Blocks access to private content - // "*/draft/*", // Blocks access to draft content - // "*/preview/*", // Blocks access to preview content - ], - }, - ], + disallow: [ + // "/api/*", // Blocks access to API routes + // "/static/*", // Blocks access to static assets directory + // routes.admin.root, // Blocks access to admin panel + // "*/private/*", // Blocks access to private content + // "*/draft/*", // Blocks access to draft content + // "*/preview/*", // Blocks access to preview content + ], + }, + ], - /* Sitemap URL - * Points search engines to your XML sitemap for efficient crawling - * @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview - */ - sitemap: `${siteConfig.url}/sitemap.xml`, + /* Sitemap URL + * Points search engines to your XML sitemap for efficient crawling + * @see https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview + */ + sitemap: `${siteConfig.url}/sitemap.xml`, - /* Host Directive - * Specifies the preferred domain version of your site - * @see https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls - */ - host: siteConfig.url, - }; + /* Host Directive + * Specifies the preferred domain version of your site + * @see https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls + */ + host: siteConfig.url, + }; } diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 1e493815..0a5c700f 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -18,11 +18,7 @@ export default async function sitemap(): Promise { for (const [key, value] of Object.entries(obj)) { if (typeof value === "string" && !isExternalRoute(value)) { result.push(value); - } else if ( - typeof value === "object" && - value !== null && - !("params" in value) - ) { + } else if (typeof value === "object" && value !== null && !("params" in value)) { // Recursively flatten nested objects (but skip route objects with params) result = [...result, ...flattenRoutes(value)]; } @@ -70,12 +66,7 @@ export default async function sitemap(): Promise { url: `${baseUrl}${route}`, lastModified: new Date(), changeFrequency: "monthly" as const, - priority: - route === "/" - ? 1 - : route.includes("auth") || route.includes("sign") - ? 0.5 - : 0.8, + priority: route === "/" ? 1 : route.includes("auth") || route.includes("sign") ? 0.5 : 0.8, })); // Changelog entries are indexable pages linked from /changelog; without diff --git a/src/components/assets/icons.tsx b/src/components/assets/icons.tsx index 70b4c966..c48d3dc1 100644 --- a/src/components/assets/icons.tsx +++ b/src/components/assets/icons.tsx @@ -4,229 +4,229 @@ import type React from "react"; export type IconProps = React.HTMLAttributes; export const Icons = { - calendar: (props: IconProps) => , - email: (props: IconProps) => , - linkedin: (props: IconProps) => ( - - LinkedIn - - - ), - x: (props: IconProps) => ( - - X - - - ), - youtube: (props: IconProps) => ( - - Youtube - - - ), - github: (props: IconProps) => ( - - Github - - - ), + calendar: (props: IconProps) => , + email: (props: IconProps) => , + linkedin: (props: IconProps) => ( + + LinkedIn + + + ), + x: (props: IconProps) => ( + + X + + + ), + youtube: (props: IconProps) => ( + + Youtube + + + ), + github: (props: IconProps) => ( + + Github + + + ), - next: (props: IconProps) => ( - - Next - - - ), - typescript: (props: IconProps & { textClassName?: string }) => { - const { textClassName = "text-background", ...rest } = props; - return ( - - Typescript - - - - - ); - }, - shadcn: (props: IconProps) => ( - - Shadcn - - - - - ), - twitter: (props: IconProps) => ( - - Twitter - - - ), - radix: (props: IconProps) => ( - - Radix - - - - - ), - aria: (props: IconProps) => ( - - Aria - - - ), - bun: (props: IconProps) => ( - - Bun - - - ), - npm: (props: IconProps) => ( - - NPM - - - ), - yarn: (props: IconProps) => ( - - Yarn - - - ), - pnpm: (props: IconProps) => ( - - Pnpm - - - ), - react: (props: IconProps) => ( - - React - - - ), - tailwind: (props: IconProps) => ( - - Tailwind - - - ), - google: (props: IconProps) => ( - - Google - - - ), - apple: (props: IconProps) => ( - - Apple - - - ), - paypal: (props: IconProps) => ( - - Paypal - - - ), - spinner: (props: IconProps) => ( - - Loading Spinner - - - ), + next: (props: IconProps) => ( + + Next + + + ), + typescript: (props: IconProps & { textClassName?: string }) => { + const { textClassName = "text-background", ...rest } = props; + return ( + + Typescript + + + + + ); + }, + shadcn: (props: IconProps) => ( + + Shadcn + + + + + ), + twitter: (props: IconProps) => ( + + Twitter + + + ), + radix: (props: IconProps) => ( + + Radix + + + + + ), + aria: (props: IconProps) => ( + + Aria + + + ), + bun: (props: IconProps) => ( + + Bun + + + ), + npm: (props: IconProps) => ( + + NPM + + + ), + yarn: (props: IconProps) => ( + + Yarn + + + ), + pnpm: (props: IconProps) => ( + + Pnpm + + + ), + react: (props: IconProps) => ( + + React + + + ), + tailwind: (props: IconProps) => ( + + Tailwind + + + ), + google: (props: IconProps) => ( + + Google + + + ), + apple: (props: IconProps) => ( + + Apple + + + ), + paypal: (props: IconProps) => ( + + Paypal + + + ), + spinner: (props: IconProps) => ( + + Loading Spinner + + + ), }; diff --git a/src/components/buttons/animated-button/animated-button.module.css b/src/components/buttons/animated-button/animated-button.module.css index 31b451af..80b72ec8 100644 --- a/src/components/buttons/animated-button/animated-button.module.css +++ b/src/components/buttons/animated-button/animated-button.module.css @@ -1,49 +1,49 @@ .buttonWrapper::after { - content: ""; - position: absolute; - z-index: -1; - top: 1px; - right: 1px; - bottom: 1px; - left: 1px; - width: calc(100% - 2px); - height: calc(100% - 2px); - border-radius: 0.3125rem; - background-color: white; /* Todo: make this dynamic */ - background-color: var(--background); + content: ""; + position: absolute; + z-index: -1; + top: 1px; + right: 1px; + bottom: 1px; + left: 1px; + width: calc(100% - 2px); + height: calc(100% - 2px); + border-radius: 0.3125rem; + background-color: white; /* Todo: make this dynamic */ + background-color: var(--background); } .animatedBackground { - background-image: conic-gradient( - from 0deg at 50% 50%, - transparent 0deg, - #4f46e5 60deg, - transparent 120deg, - transparent 360deg - ); - animation: spin 6s linear infinite; + background-image: conic-gradient( + from 0deg at 50% 50%, + transparent 0deg, + #4f46e5 60deg, + transparent 120deg, + transparent 360deg + ); + animation: spin 6s linear infinite; } @keyframes spin { - from { - transform: rotate(0deg); - } - 50% { - } - to { - transform: rotate(360deg); - } + from { + transform: rotate(0deg); + } + 50% { + } + to { + transform: rotate(360deg); + } } @media (prefers-color-scheme: dark) { - .animatedBackground { - background-color: rgba(255, 255, 255, 0.1); /* dark:bg-white/10 */ - background-image: conic-gradient( - from 0deg at 50% 50%, - transparent 0deg, - #818cf8 60deg, - transparent 120deg, - transparent 360deg - ); - } + .animatedBackground { + background-color: rgba(255, 255, 255, 0.1); /* dark:bg-white/10 */ + background-image: conic-gradient( + from 0deg at 50% 50%, + transparent 0deg, + #818cf8 60deg, + transparent 120deg, + transparent 360deg + ); + } } diff --git a/src/components/buttons/animated-button/animated-button.tsx b/src/components/buttons/animated-button/animated-button.tsx index 9e469e0a..16cb7e49 100644 --- a/src/components/buttons/animated-button/animated-button.tsx +++ b/src/components/buttons/animated-button/animated-button.tsx @@ -6,11 +6,11 @@ import styles from "./animated-button.module.css"; // Extend the ButtonHTMLAttributes to include all possible button props interface AnimatedButtonProps - // Todo: fix type - extends ButtonProps { - children: React.ReactNode; - href?: string; - color?: string; + // Todo: fix type + extends ButtonProps { + children: React.ReactNode; + href?: string; + color?: string; } /** @@ -21,53 +21,53 @@ interface AnimatedButtonProps * It passes all received props to the underlying Button. */ const AnimatedButton: React.FC = ({ - children, - className, - href, - color = "#818cf8", - ...props + children, + className, + href, + color = "#818cf8", + ...props }) => { - const Element = href ? ( - - {children} - - ) : ( - - ); + const Element = href ? ( + + {children} + + ) : ( + + ); - return ( -
-
- {Element} -
- ); + return ( +
+
+ {Element} +
+ ); }; export { AnimatedButton }; diff --git a/src/components/buttons/buy-button.tsx b/src/components/buttons/buy-button.tsx index d3e6ceff..8717fcb6 100644 --- a/src/components/buttons/buy-button.tsx +++ b/src/components/buttons/buy-button.tsx @@ -13,26 +13,17 @@ export const BuyButton = () => { // Construct the checkout URL with user's email as custom_data const checkoutUrl = new URL(routes.external.buy); if (session?.user?.email) { - checkoutUrl.searchParams.set( - "checkout[custom][user_email]", - session.user.email - ); + checkoutUrl.searchParams.set("checkout[custom][user_email]", session.user.email); // Also pass user ID for additional verification if (session.user.id) { - checkoutUrl.searchParams.set( - "checkout[custom][user_id]", - session.user.id - ); + checkoutUrl.searchParams.set("checkout[custom][user_id]", session.user.id); } // Pre-fill the email field checkoutUrl.searchParams.set("checkout[email]", session.user.email); } return ( - + Get {siteConfig.name} ); diff --git a/src/components/buttons/vercel-deploy-button.tsx b/src/components/buttons/vercel-deploy-button.tsx index 143c419a..0a64b35b 100644 --- a/src/components/buttons/vercel-deploy-button.tsx +++ b/src/components/buttons/vercel-deploy-button.tsx @@ -5,37 +5,40 @@ import Link from "next/link"; import type { FC } from "react"; interface VercelDeployButtonProps { - href?: string; - className?: string; + href?: string; + className?: string; } -export const VercelDeployButton: FC = ({ href = routes.external.vercelDeployBones, className }) => { - return ( - - - - Deploy Now - - - ); +export const VercelDeployButton: FC = ({ + href = routes.external.vercelDeployBones, + className, +}) => { + return ( + + + + Deploy Now + + + ); }; const VercelIcon: FC<{ className?: string }> = ({ className }) => ( - - - + + + ); diff --git a/src/components/footers/footer.tsx b/src/components/footers/footer.tsx index 3529fe7c..770927b7 100644 --- a/src/components/footers/footer.tsx +++ b/src/components/footers/footer.tsx @@ -22,9 +22,7 @@ interface FooterGroup { items: FooterItem[]; } -type FooterElement = - | { type: "group"; content: FooterGroup } - | { type: "node"; content: ReactNode }; +type FooterElement = { type: "group"; content: FooterGroup } | { type: "node"; content: ReactNode }; export const defaultFooterGroups: FooterElement[] = [ { @@ -133,14 +131,12 @@ export const Footer: FC = ({
{siteConfig.name}
-
- {groupElements} -
+
{groupElements}
@@ -149,10 +145,5 @@ export const Footer: FC = ({ // Type guard for LinkItem function isLinkItem(item: FooterItem): item is LinkItem { - return ( - item !== null && - typeof item === "object" && - "href" in item && - "label" in item - ); + return item !== null && typeof item === "object" && "href" in item && "label" in item; } diff --git a/src/components/forms/contact-form.tsx b/src/components/forms/contact-form.tsx index 991b3942..9d67383f 100644 --- a/src/components/forms/contact-form.tsx +++ b/src/components/forms/contact-form.tsx @@ -82,7 +82,7 @@ export function ContactForm({ defaultValues, onSuccess, className }: ContactForm
{/* Name Field */} diff --git a/src/components/forms/feedback-dialog.tsx b/src/components/forms/feedback-dialog.tsx index e54910db..8bc9f317 100644 --- a/src/components/forms/feedback-dialog.tsx +++ b/src/components/forms/feedback-dialog.tsx @@ -122,9 +122,9 @@ export const FeedbackDialog = ({ trigger, className }: FeedbackDialogProps) => { {error &&

{error}

} {success &&

Thank you for your feedback! 🚀

} {showEmailFallback && ( -
+
- +

Email service not configured. Click below to open your email client: @@ -133,7 +133,7 @@ export const FeedbackDialog = ({ trigger, className }: FeedbackDialogProps) => { type="button" size="sm" onClick={handleEmailFallback} - className="bg-blue-600 hover:bg-blue-700 text-white" + className="bg-blue-600 text-white hover:bg-blue-700" > Open Email Client diff --git a/src/components/forms/feedback-popover.tsx b/src/components/forms/feedback-popover.tsx index 956de34a..e192b71a 100644 --- a/src/components/forms/feedback-popover.tsx +++ b/src/components/forms/feedback-popover.tsx @@ -81,9 +81,9 @@ export const FeedbackPopover = () => { {success && Sent 🚀} {error && {error}} {showEmailFallback && ( -

+
- +

Email service not configured. Click to open your email client: @@ -92,7 +92,7 @@ export const FeedbackPopover = () => { type="button" size="sm" onClick={handleEmailFallback} - className="h-7 px-2 text-xs bg-blue-600 hover:bg-blue-700 text-white" + className="h-7 bg-blue-600 px-2 text-xs text-white hover:bg-blue-700" > Open Email diff --git a/src/components/headers/header.tsx b/src/components/headers/header.tsx index 6c77cff9..5a5c5f60 100644 --- a/src/components/headers/header.tsx +++ b/src/components/headers/header.tsx @@ -54,10 +54,7 @@ export const Header: React.FC = ({ variant = "default", }) => { const [{ y }] = useWindowScroll(); - const isOpaque = useMemo( - () => variant === "floating" && y && y > 100, - [y, variant] - ); + const isOpaque = useMemo(() => variant === "floating" && y && y > 100, [y, variant]); const { data: session } = useSession(); const signInRedirectUrl = useSignInRedirectUrl(); @@ -106,21 +103,14 @@ export const Header: React.FC = ({ -

- {!session && ( - - )} + {!session && } diff --git a/src/components/layouts/main-layout.tsx b/src/components/layouts/main-layout.tsx index 7953a06c..becc8ca8 100644 --- a/src/components/layouts/main-layout.tsx +++ b/src/components/layouts/main-layout.tsx @@ -4,23 +4,23 @@ import { cn } from "@/lib/utils"; import type React from "react"; export default function MainLayout({ - children, - className, - header, - footer, + children, + className, + header, + footer, }: { - children: React.ReactNode; - header?: React.ReactNode | null; - footer?: React.ReactNode | null; - className?: string; + children: React.ReactNode; + header?: React.ReactNode | null; + footer?: React.ReactNode | null; + className?: string; }) { - return ( - <> -
- {header === undefined ?
: header} - {children} - {footer === undefined ?
: footer} -
- - ); + return ( + <> +
+ {header === undefined ?
: header} + {children} + {footer === undefined ?
: footer} +
+ + ); } diff --git a/src/components/layouts/root-layout.tsx b/src/components/layouts/root-layout.tsx index b03dcf30..2a9b008b 100644 --- a/src/components/layouts/root-layout.tsx +++ b/src/components/layouts/root-layout.tsx @@ -7,10 +7,7 @@ import { cn } from "@/lib/utils"; import { SessionProvider } from "next-auth/react"; import { ThemeProvider } from "next-themes"; import { ViewTransitions } from "next-view-transitions"; -import { - Space_Grotesk as FontSans, - Noto_Serif as FontSerif, -} from "next/font/google"; +import { Space_Grotesk as FontSans, Noto_Serif as FontSerif } from "next/font/google"; import { NuqsAdapter } from "nuqs/adapters/next/app"; import type { ReactNode } from "react"; diff --git a/src/components/loaders/loader-atoms.tsx b/src/components/loaders/loader-atoms.tsx index 22ae8ff9..17a79a9b 100644 --- a/src/components/loaders/loader-atoms.tsx +++ b/src/components/loaders/loader-atoms.tsx @@ -5,116 +5,115 @@ import { type VariantProps, cva } from "class-variance-authority"; import * as React from "react"; const loaderVariants = cva("loader", { - variants: { - size: { - default: "w-16 h-16", - sm: "w-12 h-12", - lg: "w-20 h-20", - }, - color: { - default: "[--loader-color:theme(colors.primary.DEFAULT)]", - primary: "[--loader-color:theme(colors.primary.DEFAULT)]", - secondary: "[--loader-color:theme(colors.secondary.DEFAULT)]", - muted: "[--loader-color:theme(colors.muted.DEFAULT)]", - }, - }, - defaultVariants: { - size: "default", - color: "default", - }, + variants: { + size: { + default: "w-16 h-16", + sm: "w-12 h-12", + lg: "w-20 h-20", + }, + color: { + default: "[--loader-color:theme(colors.primary.DEFAULT)]", + primary: "[--loader-color:theme(colors.primary.DEFAULT)]", + secondary: "[--loader-color:theme(colors.secondary.DEFAULT)]", + muted: "[--loader-color:theme(colors.muted.DEFAULT)]", + }, + }, + defaultVariants: { + size: "default", + color: "default", + }, }); export interface LoaderAtomsProps - extends Omit, "color">, - VariantProps { - label?: string; + extends Omit, "color">, VariantProps { + label?: string; } export const LoaderAtoms = React.forwardRef( - ({ className, size, color, label, ...props }, ref) => { - return ( - // biome-ignore lint/a11y/useSemanticElements: keeping a div preserves the exported HTMLDivElement ref type -
-
-
-
-
-
- {label && {label}} - -
- ); - } + @keyframes rotate-three { + 0% { + transform: rotateX(35deg) rotateY(55deg) rotateZ(0deg); + } + 100% { + transform: rotateX(35deg) rotateY(55deg) rotateZ(360deg); + } + } + `} +
+ ); + } ); diff --git a/src/components/mdx/card-group.tsx b/src/components/mdx/card-group.tsx index 4886acc5..fe1c32dd 100644 --- a/src/components/mdx/card-group.tsx +++ b/src/components/mdx/card-group.tsx @@ -1,24 +1,24 @@ import { cn } from "@/lib/utils"; interface CardGroupProps extends React.HTMLAttributes { - cols?: 1 | 2 | 3 | 4; - children: React.ReactNode; + cols?: 1 | 2 | 3 | 4; + children: React.ReactNode; } export function CardGroup({ cols = 2, children, className, ...props }: CardGroupProps) { - return ( -
- {children} -
- ); + return ( +
+ {children} +
+ ); } diff --git a/src/components/mdx/card.tsx b/src/components/mdx/card.tsx index e403c31d..4c396064 100644 --- a/src/components/mdx/card.tsx +++ b/src/components/mdx/card.tsx @@ -8,57 +8,57 @@ import type { LucideIcon } from "lucide-react"; import * as React from "react"; interface CardProps extends React.HTMLAttributes { - title: string; - icon?: React.ReactElement | string | LucideIcon; - children: React.ReactNode; + title: string; + icon?: React.ReactElement | string | LucideIcon; + children: React.ReactNode; } export function Card({ title, icon, children, className, ...props }: CardProps) { - const renderIcon = () => { - if (!icon) return null; + const renderIcon = () => { + if (!icon) return null; - if (React.isValidElement(icon)) { - return React.cloneElement(icon, { - className: "h-5 w-5 text-primary/80 shrink-0", - } as React.SVGProps); - } + if (React.isValidElement(icon)) { + return React.cloneElement(icon, { + className: "h-5 w-5 text-primary/80 shrink-0", + } as React.SVGProps); + } - if (typeof icon === "string") { - // Handle Font Awesome icon string format (e.g., "user", "home", etc.) - return ( - - ); - } + if (typeof icon === "string") { + // Handle Font Awesome icon string format (e.g., "user", "home", etc.) + return ( + + ); + } - if (typeof icon === "function") { - const Icon = icon; - return ; - } + if (typeof icon === "function") { + const Icon = icon; + return ; + } - return null; - }; + return null; + }; - return ( -
-
-
-
-

{title}

-
{renderIcon()}
-
-
{children}
-
-
-
- ); + return ( +
+
+
+
+

{title}

+
{renderIcon()}
+
+
{children}
+
+
+
+ ); } diff --git a/src/components/primitives/analytics.tsx b/src/components/primitives/analytics.tsx index e5958e78..88cb5156 100644 --- a/src/components/primitives/analytics.tsx +++ b/src/components/primitives/analytics.tsx @@ -4,18 +4,15 @@ import { Analytics as VercelAnalytics } from "@vercel/analytics/react"; import { SpeedInsights } from "@vercel/speed-insights/next"; import Script from "next/script"; -const WEBSITE_ID = process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID ?? "6e6573f3-66c3-40d7-a161-031b22099a17"; +const WEBSITE_ID = + process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID ?? "6e6573f3-66c3-40d7-a161-031b22099a17"; export function Analytics() { - return ( - <> - - -