mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat: add ai chat example and update flags structure
This commit is contained in:
5
.changeset/sad-candies-like.md
Normal file
5
.changeset/sad-candies-like.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-better-t-stack": minor
|
||||
---
|
||||
|
||||
Add AI chat example and update flags structures
|
||||
@@ -19,7 +19,7 @@ export const DEFAULT_CONFIG: ProjectConfig = {
|
||||
packageManager: getUserPkgManager(),
|
||||
noInstall: false,
|
||||
turso: false,
|
||||
backendFramework: "hono",
|
||||
backend: "hono",
|
||||
runtime: "bun",
|
||||
};
|
||||
|
||||
@@ -59,6 +59,9 @@ export const dependencyVersionMap = {
|
||||
|
||||
"@hono/trpc-server": "^0.3.4",
|
||||
hono: "^4.7.5",
|
||||
|
||||
ai: "^4.2.8",
|
||||
"@ai-sdk/google": "^1.2.3",
|
||||
} as const;
|
||||
|
||||
export type AvailableDependencies = keyof typeof dependencyVersionMap;
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import { PKG_ROOT } from "../constants";
|
||||
import type { PackageManager, ProjectAddons, ProjectFrontend } from "../types";
|
||||
import type {
|
||||
ProjectAddons,
|
||||
ProjectFrontend,
|
||||
ProjectPackageManager,
|
||||
} from "../types";
|
||||
import { addPackageDependency } from "../utils/add-package-deps";
|
||||
import { setupTauri } from "./tauri-setup";
|
||||
|
||||
export async function setupAddons(
|
||||
projectDir: string,
|
||||
addons: ProjectAddons[],
|
||||
packageManager: PackageManager,
|
||||
packageManager: ProjectPackageManager,
|
||||
frontends: ProjectFrontend[],
|
||||
) {
|
||||
const hasWebFrontend = frontends.includes("web");
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import path from "node:path";
|
||||
import type { AvailableDependencies } from "../constants";
|
||||
import type { BackendFramework, Runtime } from "../types";
|
||||
import type { ProjectBackend, ProjectRuntime } from "../types";
|
||||
import { addPackageDependency } from "../utils/add-package-deps";
|
||||
|
||||
export async function setupBackendDependencies(
|
||||
projectDir: string,
|
||||
framework: BackendFramework,
|
||||
runtime: Runtime,
|
||||
framework: ProjectBackend,
|
||||
runtime: ProjectRuntime,
|
||||
): Promise<void> {
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
|
||||
|
||||
@@ -34,10 +34,10 @@ export async function createProject(options: ProjectConfig): Promise<string> {
|
||||
|
||||
await fixGitignoreFiles(projectDir);
|
||||
|
||||
await setupBackendFramework(projectDir, options.backendFramework);
|
||||
await setupBackendFramework(projectDir, options.backend);
|
||||
await setupBackendDependencies(
|
||||
projectDir,
|
||||
options.backendFramework,
|
||||
options.backend,
|
||||
options.runtime,
|
||||
);
|
||||
|
||||
@@ -58,19 +58,20 @@ export async function createProject(options: ProjectConfig): Promise<string> {
|
||||
await setupAuthTemplate(
|
||||
projectDir,
|
||||
options.auth,
|
||||
options.backendFramework,
|
||||
options.backend,
|
||||
options.orm,
|
||||
options.database,
|
||||
);
|
||||
await setupAuth(projectDir, options.auth);
|
||||
|
||||
await setupRuntime(projectDir, options.runtime, options.backendFramework);
|
||||
await setupRuntime(projectDir, options.runtime, options.backend);
|
||||
|
||||
await setupExamples(
|
||||
projectDir,
|
||||
options.examples,
|
||||
options.orm,
|
||||
options.auth,
|
||||
options.backend,
|
||||
options.frontend,
|
||||
);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
ProjectConfig,
|
||||
ProjectDatabase,
|
||||
ProjectOrm,
|
||||
Runtime,
|
||||
ProjectRuntime,
|
||||
} from "../types";
|
||||
|
||||
export async function createReadme(projectDir: string, options: ProjectConfig) {
|
||||
@@ -80,7 +80,7 @@ function generateFeaturesList(
|
||||
auth: boolean,
|
||||
addons: ProjectAddons[],
|
||||
orm: ProjectOrm,
|
||||
runtime: Runtime,
|
||||
runtime: ProjectRuntime,
|
||||
): string {
|
||||
const addonsList = [
|
||||
"- **TypeScript** - For type safety and improved developer experience",
|
||||
|
||||
@@ -46,6 +46,13 @@ export async function setupEnvironmentVariables(
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
options.examples?.includes("ai") &&
|
||||
!envContent.includes("GOOGLE_GENERATIVE_AI_API_KEY")
|
||||
) {
|
||||
envContent += "\nGOOGLE_GENERATIVE_AI_API_KEY=";
|
||||
}
|
||||
|
||||
await fs.writeFile(envPath, envContent.trim());
|
||||
|
||||
if (options.frontend.includes("web")) {
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import { PKG_ROOT } from "../constants";
|
||||
import type { ProjectFrontend, ProjectOrm } from "../types";
|
||||
import type { ProjectBackend, ProjectFrontend, ProjectOrm } from "../types";
|
||||
import { addPackageDependency } from "../utils/add-package-deps";
|
||||
|
||||
export async function setupExamples(
|
||||
projectDir: string,
|
||||
examples: string[],
|
||||
orm: ProjectOrm,
|
||||
auth: boolean,
|
||||
backend: ProjectBackend,
|
||||
frontend: ProjectFrontend[] = ["web"],
|
||||
): Promise<void> {
|
||||
console.log("EXAMPLEs:", examples);
|
||||
const hasWebFrontend = frontend.includes("web");
|
||||
|
||||
const webAppExists = await fs.pathExists(path.join(projectDir, "apps/web"));
|
||||
|
||||
if (examples.includes("todo") && hasWebFrontend && webAppExists) {
|
||||
@@ -19,6 +21,135 @@ export async function setupExamples(
|
||||
} else {
|
||||
await cleanupTodoFiles(projectDir, orm);
|
||||
}
|
||||
|
||||
if (
|
||||
examples.includes("ai") &&
|
||||
backend === "hono" &&
|
||||
hasWebFrontend &&
|
||||
webAppExists
|
||||
) {
|
||||
await setupAIExample(projectDir);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupAIExample(projectDir: string): Promise<void> {
|
||||
const aiExampleDir = path.join(PKG_ROOT, "template/examples/ai");
|
||||
|
||||
if (await fs.pathExists(aiExampleDir)) {
|
||||
await fs.copy(aiExampleDir, projectDir);
|
||||
|
||||
await updateHeaderWithAILink(projectDir);
|
||||
|
||||
const clientDir = path.join(projectDir, "apps/web");
|
||||
addPackageDependency({
|
||||
dependencies: ["ai"],
|
||||
projectDir: clientDir,
|
||||
});
|
||||
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
addPackageDependency({
|
||||
dependencies: ["ai", "@ai-sdk/google"],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
|
||||
await updateServerIndexWithAIRoute(projectDir);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateServerIndexWithAIRoute(projectDir: string): Promise<void> {
|
||||
const serverIndexPath = path.join(projectDir, "apps/server/src/index.ts");
|
||||
|
||||
if (await fs.pathExists(serverIndexPath)) {
|
||||
let indexContent = await fs.readFile(serverIndexPath, "utf8");
|
||||
const isHono = indexContent.includes("hono");
|
||||
|
||||
if (isHono) {
|
||||
const importSection = `import { streamText } from "ai";\nimport { google } from "@ai-sdk/google";\nimport { stream } from "hono/streaming";`;
|
||||
|
||||
const aiRouteHandler = `
|
||||
app.post("/ai", async (c) => {
|
||||
const body = await c.req.json();
|
||||
const messages = body.messages || [];
|
||||
|
||||
const result = streamText({
|
||||
model: google("gemini-2.0-flash-exp"),
|
||||
messages,
|
||||
});
|
||||
|
||||
c.header("X-Vercel-AI-Data-Stream", "v1");
|
||||
c.header("Content-Type", "text/plain; charset=utf-8");
|
||||
|
||||
return stream(c, (stream) => stream.pipe(result.toDataStream()));
|
||||
});`;
|
||||
|
||||
// Add the import section
|
||||
if (indexContent.includes("import {")) {
|
||||
const lastImportIndex = indexContent.lastIndexOf("import");
|
||||
const endOfLastImport = indexContent.indexOf("\n", lastImportIndex);
|
||||
indexContent = `${indexContent.substring(0, endOfLastImport + 1)}
|
||||
${importSection}
|
||||
${indexContent.substring(endOfLastImport + 1)}`;
|
||||
} else {
|
||||
indexContent = `${importSection}
|
||||
|
||||
${indexContent}`;
|
||||
}
|
||||
|
||||
// Add the route handler
|
||||
const trpcHandlerIndex =
|
||||
indexContent.indexOf('app.use("/trpc"') ||
|
||||
indexContent.indexOf("app.use(trpc(");
|
||||
if (trpcHandlerIndex !== -1) {
|
||||
indexContent = `${indexContent.substring(0, trpcHandlerIndex)}${aiRouteHandler}
|
||||
|
||||
${indexContent.substring(trpcHandlerIndex)}`;
|
||||
} else {
|
||||
// Add it near the end before export
|
||||
const exportIndex = indexContent.indexOf("export default");
|
||||
if (exportIndex !== -1) {
|
||||
indexContent = `${indexContent.substring(0, exportIndex)}${aiRouteHandler}
|
||||
|
||||
${indexContent.substring(exportIndex)}`;
|
||||
} else {
|
||||
indexContent = `${indexContent}
|
||||
|
||||
${aiRouteHandler}`;
|
||||
}
|
||||
}
|
||||
|
||||
await fs.writeFile(serverIndexPath, indexContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function updateHeaderWithAILink(projectDir: string): Promise<void> {
|
||||
const headerPath = path.join(
|
||||
projectDir,
|
||||
"apps/web/src/components/header.tsx",
|
||||
);
|
||||
|
||||
if (await fs.pathExists(headerPath)) {
|
||||
let headerContent = await fs.readFile(headerPath, "utf8");
|
||||
|
||||
if (headerContent.includes('{ to: "/todos"')) {
|
||||
headerContent = headerContent.replace(
|
||||
/{ to: "\/todos", label: "Todos" },/,
|
||||
`{ to: "/todos", label: "Todos" },\n { to: "/ai", label: "AI Chat" },`,
|
||||
);
|
||||
} else if (headerContent.includes('{ to: "/dashboard"')) {
|
||||
headerContent = headerContent.replace(
|
||||
/{ to: "\/dashboard", label: "Dashboard" },/,
|
||||
`{ to: "/dashboard", label: "Dashboard" },\n { to: "/ai", label: "AI Chat" },`,
|
||||
);
|
||||
} else {
|
||||
headerContent = headerContent.replace(
|
||||
/const links = \[\s*{ to: "\/", label: "Home" },/,
|
||||
`const links = [\n { to: "/", label: "Home" },\n { to: "/ai", label: "AI Chat" },`,
|
||||
);
|
||||
}
|
||||
|
||||
await fs.writeFile(headerPath, headerContent);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupTodoExample(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { log, spinner } from "@clack/prompts";
|
||||
import { $ } from "execa";
|
||||
import pc from "picocolors";
|
||||
import type { PackageManager, ProjectAddons } from "../types";
|
||||
import type { ProjectAddons, ProjectPackageManager } from "../types";
|
||||
|
||||
export async function installDependencies({
|
||||
projectDir,
|
||||
@@ -9,7 +9,7 @@ export async function installDependencies({
|
||||
addons = [],
|
||||
}: {
|
||||
projectDir: string;
|
||||
packageManager: PackageManager;
|
||||
packageManager: ProjectPackageManager;
|
||||
addons?: ProjectAddons[];
|
||||
}) {
|
||||
const s = spinner();
|
||||
@@ -17,20 +17,10 @@ export async function installDependencies({
|
||||
try {
|
||||
s.start(`Running ${packageManager} install...`);
|
||||
|
||||
switch (packageManager) {
|
||||
case "npm":
|
||||
await $({
|
||||
cwd: projectDir,
|
||||
stderr: "inherit",
|
||||
})`${packageManager} install`;
|
||||
break;
|
||||
case "pnpm":
|
||||
case "bun":
|
||||
await $({
|
||||
cwd: projectDir,
|
||||
})`${packageManager} install`;
|
||||
break;
|
||||
}
|
||||
await $({
|
||||
cwd: projectDir,
|
||||
stderr: "inherit",
|
||||
})`${packageManager} install`;
|
||||
|
||||
s.stop("Dependencies installed successfully");
|
||||
|
||||
@@ -48,14 +38,17 @@ export async function installDependencies({
|
||||
|
||||
async function runBiomeCheck(
|
||||
projectDir: string,
|
||||
packageManager: PackageManager,
|
||||
packageManager: ProjectPackageManager,
|
||||
) {
|
||||
const s = spinner();
|
||||
|
||||
try {
|
||||
s.start("Running Biome format check...");
|
||||
|
||||
await $({ cwd: projectDir })`${packageManager} biome check --write .`;
|
||||
await $({
|
||||
cwd: projectDir,
|
||||
stderr: "inherit",
|
||||
})`${packageManager} biome check --write .`;
|
||||
|
||||
s.stop("Biome check completed successfully");
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { log } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type {
|
||||
PackageManager,
|
||||
ProjectAddons,
|
||||
ProjectDatabase,
|
||||
ProjectFrontend,
|
||||
ProjectOrm,
|
||||
Runtime,
|
||||
ProjectPackageManager,
|
||||
ProjectRuntime,
|
||||
} from "../types";
|
||||
|
||||
export function displayPostInstallInstructions(
|
||||
database: ProjectDatabase,
|
||||
projectName: string,
|
||||
packageManager: PackageManager,
|
||||
packageManager: ProjectPackageManager,
|
||||
depsInstalled: boolean,
|
||||
orm: ProjectOrm,
|
||||
addons: ProjectAddons[],
|
||||
runtime: Runtime,
|
||||
runtime: ProjectRuntime,
|
||||
frontends: ProjectFrontend[],
|
||||
) {
|
||||
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
|
||||
@@ -67,7 +67,7 @@ function getDatabaseInstructions(
|
||||
database: ProjectDatabase,
|
||||
orm?: ProjectOrm,
|
||||
runCmd?: string,
|
||||
runtime?: Runtime,
|
||||
runtime?: ProjectRuntime,
|
||||
): string {
|
||||
const instructions = [];
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import type { BackendFramework, Runtime } from "../types";
|
||||
import type { ProjectBackend, ProjectRuntime } from "../types";
|
||||
import { addPackageDependency } from "../utils/add-package-deps";
|
||||
|
||||
export async function setupRuntime(
|
||||
projectDir: string,
|
||||
runtime: Runtime,
|
||||
backendFramework: BackendFramework,
|
||||
runtime: ProjectRuntime,
|
||||
backendFramework: ProjectBackend,
|
||||
): Promise<void> {
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
const serverIndexPath = path.join(serverDir, "src/index.ts");
|
||||
@@ -34,7 +34,7 @@ async function setupBunRuntime(
|
||||
serverDir: string,
|
||||
serverIndexPath: string,
|
||||
indexContent: string,
|
||||
backendFramework: BackendFramework,
|
||||
backendFramework: ProjectBackend,
|
||||
): Promise<void> {
|
||||
const packageJsonPath = path.join(serverDir, "package.json");
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
@@ -62,7 +62,7 @@ async function setupNodeRuntime(
|
||||
serverDir: string,
|
||||
serverIndexPath: string,
|
||||
indexContent: string,
|
||||
backendFramework: BackendFramework,
|
||||
backendFramework: ProjectBackend,
|
||||
): Promise<void> {
|
||||
const packageJsonPath = path.join(serverDir, "package.json");
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
|
||||
@@ -3,12 +3,12 @@ import { log, spinner } from "@clack/prompts";
|
||||
import { $, execa } from "execa";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { PackageManager } from "../types";
|
||||
import type { ProjectPackageManager } from "../types";
|
||||
import { addPackageDependency } from "../utils/add-package-deps";
|
||||
|
||||
export async function setupTauri(
|
||||
projectDir: string,
|
||||
packageManager: PackageManager,
|
||||
packageManager: ProjectPackageManager,
|
||||
): Promise<void> {
|
||||
const s = spinner();
|
||||
const clientPackageDir = path.join(projectDir, "apps/web");
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from "node:path";
|
||||
import fs from "fs-extra";
|
||||
import { PKG_ROOT } from "../constants";
|
||||
import type {
|
||||
BackendFramework,
|
||||
ProjectBackend,
|
||||
ProjectDatabase,
|
||||
ProjectFrontend,
|
||||
ProjectOrm,
|
||||
@@ -42,7 +42,7 @@ export async function setupFrontendTemplates(
|
||||
|
||||
export async function setupBackendFramework(
|
||||
projectDir: string,
|
||||
framework: BackendFramework,
|
||||
framework: ProjectBackend,
|
||||
): Promise<void> {
|
||||
const frameworkDir = path.join(PKG_ROOT, `template/with-${framework}`);
|
||||
if (await fs.pathExists(frameworkDir)) {
|
||||
@@ -88,7 +88,7 @@ export async function setupOrmTemplate(
|
||||
export async function setupAuthTemplate(
|
||||
projectDir: string,
|
||||
auth: boolean,
|
||||
framework: BackendFramework,
|
||||
framework: ProjectBackend,
|
||||
orm: ProjectOrm,
|
||||
database: ProjectDatabase,
|
||||
): Promise<void> {
|
||||
|
||||
@@ -6,12 +6,10 @@ import { createProject } from "./helpers/create-project";
|
||||
import { installDependencies } from "./helpers/install-dependencies";
|
||||
import { gatherConfig } from "./prompts/config-prompts";
|
||||
import type {
|
||||
BackendFramework,
|
||||
ProjectAddons,
|
||||
ProjectConfig,
|
||||
ProjectExamples,
|
||||
ProjectFrontend,
|
||||
Runtime,
|
||||
} from "./types";
|
||||
import { displayConfig } from "./utils/display-config";
|
||||
import { generateReproducibleCommand } from "./utils/generate-reproducible-command";
|
||||
@@ -34,36 +32,36 @@ async function main() {
|
||||
.version(getLatestCLIVersion())
|
||||
.argument("[project-directory]", "Project name/directory")
|
||||
.option("-y, --yes", "Use default configuration")
|
||||
.option("--no-database", "Skip database setup")
|
||||
.option("--sqlite", "Use SQLite database")
|
||||
.option("--postgres", "Use PostgreSQL database")
|
||||
.option("--database <type>", "Database type (none, sqlite, postgres)")
|
||||
.option("--orm <type>", "ORM type (none, drizzle, prisma)")
|
||||
.option("--auth", "Include authentication")
|
||||
.option("--no-auth", "Exclude authentication")
|
||||
.option("--pwa", "Include Progressive Web App support")
|
||||
.option("--tauri", "Include Tauri desktop app support")
|
||||
.option("--biome", "Include Biome for linting and formatting")
|
||||
.option("--husky", "Include Husky, lint-staged for Git hooks")
|
||||
.option(
|
||||
"--frontend <types>",
|
||||
"Frontend types (web,native or both)",
|
||||
(val) => val.split(",") as ProjectFrontend[],
|
||||
)
|
||||
.option(
|
||||
"--addons <types>",
|
||||
"Additional addons (pwa,tauri,biome,husky)",
|
||||
(val) => val.split(",") as ProjectAddons[],
|
||||
)
|
||||
.option("--no-addons", "Skip all additional addons")
|
||||
.option("--examples <examples>", "Include specified examples")
|
||||
.option(
|
||||
"--examples <types>",
|
||||
"Examples to include (todo,ai)",
|
||||
(val) => val.split(",") as ProjectExamples[],
|
||||
)
|
||||
.option("--no-examples", "Skip all examples")
|
||||
.option("--git", "Include git setup")
|
||||
.option("--git", "Initialize git repository")
|
||||
.option("--no-git", "Skip git initialization")
|
||||
.option("--npm", "Use npm package manager")
|
||||
.option("--pnpm", "Use pnpm package manager")
|
||||
.option("--bun", "Use bun package manager")
|
||||
.option("--drizzle", "Use Drizzle ORM")
|
||||
.option("--prisma", "Use Prisma ORM (coming soon)")
|
||||
.option("--package-manager <pm>", "Package manager (npm, pnpm, bun)")
|
||||
.option("--install", "Install dependencies")
|
||||
.option("--no-install", "Skip installing dependencies")
|
||||
.option("--turso", "Set up Turso for SQLite database")
|
||||
.option("--no-turso", "Skip Turso setup for SQLite database")
|
||||
.option("--hono", "Use Hono backend framework")
|
||||
.option("--elysia", "Use Elysia backend framework")
|
||||
.option("--runtime <runtime>", "Specify runtime (bun or node)")
|
||||
.option("--web", "Include web frontend")
|
||||
.option("--native", "Include Expo frontend")
|
||||
.option("--no-web", "Exclude web frontend")
|
||||
.option("--no-native", "Exclude Expo frontend")
|
||||
.option("--no-turso", "Skip Turso setup")
|
||||
.option("--backend <framework>", "Backend framework (hono, elysia)")
|
||||
.option("--runtime <runtime>", "Runtime (bun, node)")
|
||||
.parse();
|
||||
|
||||
const s = spinner();
|
||||
@@ -75,60 +73,88 @@ async function main() {
|
||||
const options = program.opts();
|
||||
const projectDirectory = program.args[0];
|
||||
|
||||
let backendFramework: BackendFramework | undefined;
|
||||
if (options.hono) backendFramework = "hono";
|
||||
if (options.elysia) backendFramework = "elysia";
|
||||
if (
|
||||
options.database &&
|
||||
!["none", "sqlite", "postgres"].includes(options.database)
|
||||
) {
|
||||
cancel(
|
||||
pc.red(
|
||||
`Invalid database type: ${options.database}. Must be none, sqlite, or postgres.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (options.orm && !["none", "drizzle", "prisma"].includes(options.orm)) {
|
||||
cancel(
|
||||
pc.red(
|
||||
`Invalid ORM type: ${options.orm}. Must be none, drizzle, or prisma.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
options.packageManager &&
|
||||
!["npm", "pnpm", "bun"].includes(options.packageManager)
|
||||
) {
|
||||
cancel(
|
||||
pc.red(
|
||||
`Invalid package manager: ${options.packageManager}. Must be npm, pnpm, or bun.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (options.backend && !["hono", "elysia"].includes(options.backend)) {
|
||||
cancel(
|
||||
pc.red(
|
||||
`Invalid backend framework: ${options.backend}. Must be hono or elysia.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (options.runtime && !["bun", "node"].includes(options.runtime)) {
|
||||
cancel(
|
||||
pc.red(`Invalid runtime: ${options.runtime}. Must be bun or node.`),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (options.examples && options.examples.length > 0) {
|
||||
const validExamples = ["todo", "ai"];
|
||||
const invalidExamples = options.examples.filter(
|
||||
(example: ProjectExamples) => !validExamples.includes(example),
|
||||
);
|
||||
|
||||
if (invalidExamples.length > 0) {
|
||||
cancel(
|
||||
pc.red(
|
||||
`Invalid example(s): ${invalidExamples.join(", ")}. Valid options are: ${validExamples.join(", ")}.`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const flagConfig: Partial<ProjectConfig> = {
|
||||
...(projectDirectory && { projectName: projectDirectory }),
|
||||
...(options.database === false && { database: "none" }),
|
||||
...(options.sqlite && { database: "sqlite" }),
|
||||
...(options.postgres && { database: "postgres" }),
|
||||
...(options.drizzle && { orm: "drizzle" }),
|
||||
...(options.prisma && { orm: "prisma" }),
|
||||
...(options.database && { database: options.database }),
|
||||
...(options.orm && { orm: options.orm }),
|
||||
...("auth" in options && { auth: options.auth }),
|
||||
...(options.npm && { packageManager: "npm" }),
|
||||
...(options.pnpm && { packageManager: "pnpm" }),
|
||||
...(options.bun && { packageManager: "bun" }),
|
||||
...(options.packageManager && { packageManager: options.packageManager }),
|
||||
...("git" in options && { git: options.git }),
|
||||
...("install" in options && { noInstall: !options.install }),
|
||||
...("turso" in options && { turso: options.turso }),
|
||||
...(backendFramework && { backendFramework }),
|
||||
...(options.runtime && { runtime: options.runtime as Runtime }),
|
||||
...((options.pwa ||
|
||||
options.tauri ||
|
||||
options.biome ||
|
||||
options.husky ||
|
||||
options.addons === false) && {
|
||||
addons:
|
||||
options.addons === false
|
||||
? []
|
||||
: ([
|
||||
...(options.pwa ? ["pwa"] : []),
|
||||
...(options.tauri ? ["tauri"] : []),
|
||||
...(options.biome ? ["biome"] : []),
|
||||
...(options.husky ? ["husky"] : []),
|
||||
] as ProjectAddons[]),
|
||||
...(options.backend && { backend: options.backend }),
|
||||
...(options.runtime && { runtime: options.runtime }),
|
||||
...(options.frontend && { frontend: options.frontend }),
|
||||
...((options.addons || options.addons === false) && {
|
||||
addons: options.addons === false ? [] : options.addons,
|
||||
}),
|
||||
...((options.examples || options.examples === false) && {
|
||||
examples:
|
||||
options.examples === false
|
||||
? []
|
||||
: typeof options.examples === "string"
|
||||
? (options.examples
|
||||
.split(",")
|
||||
.filter((e) => e === "todo") as ProjectExamples[])
|
||||
: [],
|
||||
}),
|
||||
...((options.web !== undefined || options.native !== undefined) && {
|
||||
frontend: [
|
||||
...(options.web === false ? [] : ["web"]),
|
||||
...(options.native === false
|
||||
? []
|
||||
: options.native === true
|
||||
? ["native"]
|
||||
: []),
|
||||
].filter(Boolean) as ProjectFrontend[],
|
||||
examples: options.examples === false ? [] : options.examples,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -137,55 +163,12 @@ async function main() {
|
||||
log.message(displayConfig(flagConfig));
|
||||
log.message("");
|
||||
}
|
||||
|
||||
const config = options.yes
|
||||
? {
|
||||
...DEFAULT_CONFIG,
|
||||
projectName: projectDirectory ?? DEFAULT_CONFIG.projectName,
|
||||
database:
|
||||
options.database === false
|
||||
? "none"
|
||||
: options.sqlite
|
||||
? "sqlite"
|
||||
: options.postgres
|
||||
? "postgres"
|
||||
: DEFAULT_CONFIG.database,
|
||||
orm:
|
||||
options.database === false
|
||||
? "none"
|
||||
: options.drizzle
|
||||
? "drizzle"
|
||||
: options.prisma
|
||||
? "prisma"
|
||||
: DEFAULT_CONFIG.orm,
|
||||
auth: "auth" in options ? options.auth : DEFAULT_CONFIG.auth,
|
||||
git: "git" in options ? options.git : DEFAULT_CONFIG.git,
|
||||
noInstall:
|
||||
"install" in options ? !options.install : DEFAULT_CONFIG.noInstall,
|
||||
packageManager:
|
||||
flagConfig.packageManager ?? DEFAULT_CONFIG.packageManager,
|
||||
addons: flagConfig.addons?.length
|
||||
? flagConfig.addons
|
||||
: DEFAULT_CONFIG.addons,
|
||||
examples: flagConfig.examples?.length
|
||||
? flagConfig.examples
|
||||
: DEFAULT_CONFIG.examples,
|
||||
turso:
|
||||
"turso" in options
|
||||
? options.turso
|
||||
: flagConfig.database === "sqlite"
|
||||
? DEFAULT_CONFIG.turso
|
||||
: false,
|
||||
backendFramework: backendFramework ?? DEFAULT_CONFIG.backendFramework,
|
||||
runtime: options.runtime
|
||||
? (options.runtime as Runtime)
|
||||
: DEFAULT_CONFIG.runtime,
|
||||
frontend:
|
||||
options.web === false || options.native === true
|
||||
? ([
|
||||
...(options.web === false ? [] : ["web"]),
|
||||
...(options.native ? ["native"] : []),
|
||||
] as ProjectFrontend[])
|
||||
: DEFAULT_CONFIG.frontend,
|
||||
...flagConfig,
|
||||
}
|
||||
: await gatherConfig(flagConfig);
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { BackendFramework } from "../types";
|
||||
import type { ProjectBackend } from "../types";
|
||||
|
||||
export async function getBackendFrameworkChoice(
|
||||
backendFramework?: BackendFramework,
|
||||
): Promise<BackendFramework> {
|
||||
backendFramework?: ProjectBackend,
|
||||
): Promise<ProjectBackend> {
|
||||
if (backendFramework !== undefined) return backendFramework;
|
||||
|
||||
const response = await select<BackendFramework>({
|
||||
const response = await select<ProjectBackend>({
|
||||
message: "Which backend framework would you like to use?",
|
||||
options: [
|
||||
{
|
||||
@@ -22,7 +22,7 @@ export async function getBackendFrameworkChoice(
|
||||
hint: "TypeScript framework with end-to-end type safety)",
|
||||
},
|
||||
],
|
||||
initialValue: DEFAULT_CONFIG.backendFramework,
|
||||
initialValue: DEFAULT_CONFIG.backend,
|
||||
});
|
||||
|
||||
if (isCancel(response)) {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { cancel, group } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type {
|
||||
BackendFramework,
|
||||
PackageManager,
|
||||
ProjectAddons,
|
||||
ProjectBackend,
|
||||
ProjectConfig,
|
||||
ProjectDatabase,
|
||||
ProjectExamples,
|
||||
ProjectFrontend,
|
||||
ProjectOrm,
|
||||
Runtime,
|
||||
ProjectPackageManager,
|
||||
ProjectRuntime,
|
||||
} from "../types";
|
||||
import { getAddonsChoice } from "./addons";
|
||||
import { getAuthChoice } from "./auth";
|
||||
@@ -33,11 +33,11 @@ type PromptGroupResults = {
|
||||
addons: ProjectAddons[];
|
||||
examples: ProjectExamples[];
|
||||
git: boolean;
|
||||
packageManager: PackageManager;
|
||||
packageManager: ProjectPackageManager;
|
||||
noInstall: boolean;
|
||||
turso: boolean;
|
||||
backendFramework: BackendFramework;
|
||||
runtime: Runtime;
|
||||
backend: ProjectBackend;
|
||||
runtime: ProjectRuntime;
|
||||
frontend: ProjectFrontend[];
|
||||
};
|
||||
|
||||
@@ -50,7 +50,7 @@ export async function gatherConfig(
|
||||
return getProjectName(flags.projectName);
|
||||
},
|
||||
frontend: () => getFrontendChoice(flags.frontend),
|
||||
backendFramework: () => getBackendFrameworkChoice(flags.backendFramework),
|
||||
backend: () => getBackendFrameworkChoice(flags.backend),
|
||||
runtime: () => getRuntimeChoice(flags.runtime),
|
||||
database: () => getDatabaseChoice(flags.database),
|
||||
orm: ({ results }) =>
|
||||
@@ -67,7 +67,12 @@ export async function gatherConfig(
|
||||
: Promise.resolve(false),
|
||||
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend),
|
||||
examples: ({ results }) =>
|
||||
getExamplesChoice(flags.examples, results.database, results.frontend),
|
||||
getExamplesChoice(
|
||||
flags.examples,
|
||||
results.database,
|
||||
results.frontend,
|
||||
results.backend,
|
||||
),
|
||||
git: () => getGitChoice(flags.git),
|
||||
packageManager: () => getPackageManagerChoice(flags.packageManager),
|
||||
noInstall: () => getNoInstallChoice(flags.noInstall),
|
||||
@@ -92,7 +97,7 @@ export async function gatherConfig(
|
||||
packageManager: result.packageManager,
|
||||
noInstall: result.noInstall,
|
||||
turso: result.turso,
|
||||
backendFramework: result.backendFramework,
|
||||
backend: result.backend,
|
||||
runtime: result.runtime,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { cancel, isCancel, multiselect } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type {
|
||||
ProjectBackend,
|
||||
ProjectDatabase,
|
||||
ProjectExamples,
|
||||
ProjectFrontend,
|
||||
@@ -11,6 +12,7 @@ export async function getExamplesChoice(
|
||||
examples?: ProjectExamples[],
|
||||
database?: ProjectDatabase,
|
||||
frontends?: ProjectFrontend[],
|
||||
backend?: ProjectBackend,
|
||||
): Promise<ProjectExamples[]> {
|
||||
if (examples !== undefined) return examples;
|
||||
|
||||
@@ -19,18 +21,42 @@ export async function getExamplesChoice(
|
||||
const hasWebFrontend = frontends?.includes("web");
|
||||
if (!hasWebFrontend) return [];
|
||||
|
||||
const response = await multiselect<ProjectExamples>({
|
||||
message: "Which examples would you like to include?",
|
||||
options: [
|
||||
{
|
||||
value: "todo",
|
||||
label: "Todo App",
|
||||
hint: "A simple CRUD example app",
|
||||
},
|
||||
],
|
||||
required: false,
|
||||
initialValues: DEFAULT_CONFIG.examples,
|
||||
});
|
||||
let response: ProjectExamples[] | symbol = [];
|
||||
|
||||
if (backend === "elysia") {
|
||||
response = await multiselect<ProjectExamples>({
|
||||
message: "Which examples would you like to include?",
|
||||
options: [
|
||||
{
|
||||
value: "todo",
|
||||
label: "Todo App",
|
||||
hint: "A simple CRUD example app",
|
||||
},
|
||||
],
|
||||
required: false,
|
||||
initialValues: DEFAULT_CONFIG.examples,
|
||||
});
|
||||
}
|
||||
|
||||
if (backend === "hono") {
|
||||
response = await multiselect<ProjectExamples>({
|
||||
message: "Which examples would you like to include?",
|
||||
options: [
|
||||
{
|
||||
value: "todo",
|
||||
label: "Todo App",
|
||||
hint: "A simple CRUD example app",
|
||||
},
|
||||
{
|
||||
value: "ai",
|
||||
label: "AI Chat",
|
||||
hint: "A simple AI chat interface using AI SDK",
|
||||
},
|
||||
],
|
||||
required: false,
|
||||
initialValues: DEFAULT_CONFIG.examples,
|
||||
});
|
||||
}
|
||||
|
||||
if (isCancel(response)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type { PackageManager, Runtime } from "../types";
|
||||
import type { ProjectPackageManager, ProjectRuntime } from "../types";
|
||||
import { getUserPkgManager } from "../utils/get-package-manager";
|
||||
|
||||
export async function getPackageManagerChoice(
|
||||
packageManager?: PackageManager,
|
||||
): Promise<PackageManager> {
|
||||
packageManager?: ProjectPackageManager,
|
||||
): Promise<ProjectPackageManager> {
|
||||
if (packageManager !== undefined) return packageManager;
|
||||
|
||||
const detectedPackageManager = getUserPkgManager();
|
||||
|
||||
const response = await select<PackageManager>({
|
||||
const response = await select<ProjectPackageManager>({
|
||||
message: "Which package manager do you want to use?",
|
||||
options: [
|
||||
{ value: "npm", label: "npm", hint: "Node Package Manager" },
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { DEFAULT_CONFIG } from "../constants";
|
||||
import type { Runtime } from "../types";
|
||||
import type { ProjectRuntime } from "../types";
|
||||
|
||||
export async function getRuntimeChoice(runtime?: Runtime): Promise<Runtime> {
|
||||
export async function getRuntimeChoice(
|
||||
runtime?: ProjectRuntime,
|
||||
): Promise<ProjectRuntime> {
|
||||
if (runtime !== undefined) return runtime;
|
||||
|
||||
const response = await select<Runtime>({
|
||||
const response = await select<ProjectRuntime>({
|
||||
message: "Which runtime would you like to use?",
|
||||
options: [
|
||||
{
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
export type ProjectDatabase = "sqlite" | "postgres" | "none";
|
||||
export type ProjectOrm = "drizzle" | "prisma" | "none";
|
||||
export type PackageManager = "npm" | "pnpm" | "bun";
|
||||
export type ProjectPackageManager = "npm" | "pnpm" | "bun";
|
||||
export type ProjectAddons = "pwa" | "biome" | "tauri" | "husky";
|
||||
export type BackendFramework = "hono" | "elysia";
|
||||
export type Runtime = "node" | "bun";
|
||||
export type ProjectExamples = "todo";
|
||||
export type ProjectBackend = "hono" | "elysia";
|
||||
export type ProjectRuntime = "node" | "bun";
|
||||
export type ProjectExamples = "todo" | "ai";
|
||||
export type ProjectFrontend = "web" | "native";
|
||||
|
||||
export interface ProjectConfig {
|
||||
projectName: string;
|
||||
backendFramework: BackendFramework;
|
||||
runtime: Runtime;
|
||||
backend: ProjectBackend;
|
||||
runtime: ProjectRuntime;
|
||||
database: ProjectDatabase;
|
||||
orm: ProjectOrm;
|
||||
auth: boolean;
|
||||
addons: ProjectAddons[];
|
||||
examples: ProjectExamples[];
|
||||
git: boolean;
|
||||
packageManager: PackageManager;
|
||||
packageManager: ProjectPackageManager;
|
||||
noInstall?: boolean;
|
||||
turso?: boolean;
|
||||
frontend: ProjectFrontend[];
|
||||
|
||||
@@ -14,10 +14,8 @@ export function displayConfig(config: Partial<ProjectConfig>) {
|
||||
configDisplay.push(`${pc.blue("Frontend:")} ${frontendText}`);
|
||||
}
|
||||
|
||||
if (config.backendFramework !== undefined) {
|
||||
configDisplay.push(
|
||||
`${pc.blue("Backend Framework:")} ${config.backendFramework}`,
|
||||
);
|
||||
if (config.backend !== undefined) {
|
||||
configDisplay.push(`${pc.blue("Backend Framework:")} ${config.backend}`);
|
||||
}
|
||||
|
||||
if (config.runtime !== undefined) {
|
||||
|
||||
@@ -4,12 +4,12 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
|
||||
const flags: string[] = [];
|
||||
|
||||
if (config.database === "none") {
|
||||
flags.push("--no-database");
|
||||
flags.push("--database none");
|
||||
} else {
|
||||
flags.push(`--${config.database}`);
|
||||
flags.push(`--database ${config.database}`);
|
||||
|
||||
if (config.orm) {
|
||||
flags.push(`--${config.orm}`);
|
||||
flags.push(`--orm ${config.orm}`);
|
||||
}
|
||||
|
||||
if (config.database === "sqlite") {
|
||||
@@ -23,36 +23,20 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
|
||||
|
||||
flags.push(config.noInstall ? "--no-install" : "--install");
|
||||
|
||||
if (config.packageManager) {
|
||||
flags.push(`--${config.packageManager}`);
|
||||
}
|
||||
|
||||
if (config.backendFramework) {
|
||||
flags.push(`--${config.backendFramework}`);
|
||||
}
|
||||
|
||||
if (config.runtime) {
|
||||
flags.push(`--runtime ${config.runtime}`);
|
||||
}
|
||||
|
||||
if (config.frontend) {
|
||||
if (config.frontend.includes("web")) {
|
||||
flags.push("--web");
|
||||
} else {
|
||||
flags.push("--no-web");
|
||||
}
|
||||
|
||||
if (config.frontend.includes("native")) {
|
||||
flags.push("--native");
|
||||
} else {
|
||||
flags.push("--no-native");
|
||||
}
|
||||
if (config.backend) {
|
||||
flags.push(`--backend ${config.backend}`);
|
||||
}
|
||||
|
||||
if (config.addons.length > 0) {
|
||||
for (const addon of config.addons) {
|
||||
flags.push(`--${addon}`);
|
||||
}
|
||||
if (config.frontend && config.frontend.length > 0) {
|
||||
flags.push(`--frontend ${config.frontend.join(",")}`);
|
||||
}
|
||||
|
||||
if (config.addons && config.addons.length > 0) {
|
||||
flags.push(`--addons ${config.addons.join(",")}`);
|
||||
} else {
|
||||
flags.push("--no-addons");
|
||||
}
|
||||
@@ -63,7 +47,21 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
|
||||
flags.push("--no-examples");
|
||||
}
|
||||
|
||||
const baseCommand = "npx create-better-t-stack";
|
||||
if (config.packageManager) {
|
||||
flags.push(`--package-manager ${config.packageManager}`);
|
||||
}
|
||||
|
||||
let baseCommand = "";
|
||||
const pkgManager = config.packageManager;
|
||||
|
||||
if (pkgManager === "npm") {
|
||||
baseCommand = "npm create better-t-stack@latest";
|
||||
} else if (pkgManager === "pnpm") {
|
||||
baseCommand = "pnpm create better-t-stack@latest";
|
||||
} else if (pkgManager === "bun") {
|
||||
baseCommand = "bun create better-t-stack@latest";
|
||||
}
|
||||
|
||||
const projectName = config.projectName ? ` ${config.projectName}` : "";
|
||||
|
||||
return `${baseCommand}${projectName} ${flags.join(" ")}`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PackageManager } from "../types";
|
||||
import type { ProjectPackageManager } from "../types";
|
||||
|
||||
export const getUserPkgManager: () => PackageManager = () => {
|
||||
export const getUserPkgManager: () => ProjectPackageManager = () => {
|
||||
const userAgent = process.env.npm_config_user_agent;
|
||||
|
||||
if (userAgent?.startsWith("pnpm")) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
const { getDefaultConfig } = require("expo/metro-config");
|
||||
const { FileStore } = require("metro-cache");
|
||||
const { withNativeWind } = require("nativewind/metro");
|
||||
const path = require("path");
|
||||
const path = require("node:path");
|
||||
|
||||
const config = withTurborepoManagedCache(
|
||||
withMonorepoPaths(
|
||||
|
||||
@@ -46,9 +46,11 @@ function RootComponent() {
|
||||
<>
|
||||
<HeadContent />
|
||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||
<Header />
|
||||
{isFetching && <Loader />}
|
||||
<Outlet />
|
||||
<div className="grid grid-rows-[auto_1fr] h-svh">
|
||||
<Header />
|
||||
{isFetching && <Loader />}
|
||||
<Outlet />
|
||||
</div>
|
||||
<Toaster richColors />
|
||||
</ThemeProvider>
|
||||
<TanStackRouterDevtools position="bottom-left" />
|
||||
|
||||
69
apps/cli/template/examples/ai/apps/web/src/routes/ai.tsx
Normal file
69
apps/cli/template/examples/ai/apps/web/src/routes/ai.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Send } from "lucide-react";
|
||||
import { useRef, useEffect } from "react";
|
||||
|
||||
export const Route = createFileRoute("/ai")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { messages, input, handleInputChange, handleSubmit } = useChat({
|
||||
api: `${import.meta.env.VITE_SERVER_URL}/ai`,
|
||||
});
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
|
||||
<div className="overflow-y-auto space-y-4 pb-4">
|
||||
{messages.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground mt-8">
|
||||
Ask me anything to get started!
|
||||
</div>
|
||||
) : (
|
||||
messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`p-3 rounded-lg ${
|
||||
message.role === "user"
|
||||
? "bg-primary/10 ml-8"
|
||||
: "bg-secondary/20 mr-8"
|
||||
}`}
|
||||
>
|
||||
<p className="text-sm font-semibold mb-1">
|
||||
{message.role === "user" ? "You" : "AI Assistant"}
|
||||
</p>
|
||||
<div className="whitespace-pre-wrap">{message.content}</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="w-full flex items-center space-x-2 pt-2 border-t"
|
||||
>
|
||||
<Input
|
||||
name="prompt"
|
||||
value={input}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Type your message..."
|
||||
className="flex-1"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
/>
|
||||
<Button type="submit" size="icon">
|
||||
<Send size={18} />
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import SignInForm from "./sign-in-form";
|
||||
import SignUpForm from "./sign-up-form";
|
||||
|
||||
export default function AuthForms() {
|
||||
const [showSignIn, setShowSignIn] = useState(false);
|
||||
|
||||
return showSignIn ? (
|
||||
<SignInForm onSwitchToSignUp={() => setShowSignIn(false)} />
|
||||
) : (
|
||||
<SignUpForm onSwitchToSignIn={() => setShowSignIn(true)} />
|
||||
);
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export default function SignInForm({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto mt-10 max-w-md p-6">
|
||||
<div className="mx-auto w-full mt-10 max-w-md p-6">
|
||||
<h1 className="mb-6 text-center text-3xl font-bold">Welcome Back</h1>
|
||||
|
||||
<form
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function SignUpForm({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto mt-10 max-w-md p-6">
|
||||
<div className="mx-auto w-full mt-10 max-w-md p-6">
|
||||
<h1 className="mb-6 text-center text-3xl font-bold">Create Account</h1>
|
||||
|
||||
<form
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import SignInForm from "@/components/sign-in-form";
|
||||
import SignUpForm from "@/components/sign-up-form";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import AuthForms from "@/components/auth-forms";
|
||||
import { useState } from "react";
|
||||
|
||||
export const Route = createFileRoute("/login")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<AuthForms />);
|
||||
const [showSignIn, setShowSignIn] = useState(false);
|
||||
|
||||
return showSignIn ? (
|
||||
<SignInForm onSwitchToSignUp={() => setShowSignIn(false)} />
|
||||
) : (
|
||||
<SignUpForm onSwitchToSignIn={() => setShowSignIn(true)} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { drizzle } from "drizzle-orm/libsql";
|
||||
import { createClient } from "@libsql/client";
|
||||
|
||||
export const db = drizzle({
|
||||
connection: {
|
||||
url: process.env.TURSO_CONNECTION_URL || "",
|
||||
authToken: process.env.TURSO_AUTH_TOKEN,
|
||||
},
|
||||
// logger: true,
|
||||
const client = createClient({
|
||||
url: process.env.TURSO_CONNECTION_URL || "",
|
||||
authToken: process.env.TURSO_AUTH_TOKEN,
|
||||
});
|
||||
|
||||
export const db = drizzle({ client });
|
||||
|
||||
@@ -17,6 +17,7 @@ const TWEET_IDS = [
|
||||
"1904179661086556412",
|
||||
"1906149740095705265",
|
||||
"1906001923456790710",
|
||||
"1906570888897777847",
|
||||
];
|
||||
|
||||
export default function Testimonials() {
|
||||
|
||||
Reference in New Issue
Block a user