mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
add automatic prisma postgres setup
fix tanstack auth template
This commit is contained in:
5
.changeset/curvy-poems-camp.md
Normal file
5
.changeset/curvy-poems-camp.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"create-better-t-stack": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add automatic prisma postgres setup
|
||||||
@@ -7,10 +7,7 @@
|
|||||||
"bin": {
|
"bin": {
|
||||||
"create-better-t-stack": "dist/index.js"
|
"create-better-t-stack": "dist/index.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": ["dist", "template"],
|
||||||
"dist",
|
|
||||||
"template"
|
|
||||||
],
|
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"typescript",
|
"typescript",
|
||||||
"scaffold",
|
"scaffold",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const DEFAULT_CONFIG: ProjectConfig = {
|
|||||||
packageManager: getUserPkgManager(),
|
packageManager: getUserPkgManager(),
|
||||||
noInstall: false,
|
noInstall: false,
|
||||||
turso: false,
|
turso: false,
|
||||||
|
prismaPostgres: false,
|
||||||
backend: "hono",
|
backend: "hono",
|
||||||
runtime: "bun",
|
runtime: "bun",
|
||||||
};
|
};
|
||||||
@@ -62,6 +63,8 @@ export const dependencyVersionMap = {
|
|||||||
|
|
||||||
ai: "^4.2.8",
|
ai: "^4.2.8",
|
||||||
"@ai-sdk/google": "^1.2.3",
|
"@ai-sdk/google": "^1.2.3",
|
||||||
|
|
||||||
|
"@prisma/extension-accelerate": "^1.3.0",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type AvailableDependencies = keyof typeof dependencyVersionMap;
|
export type AvailableDependencies = keyof typeof dependencyVersionMap;
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export async function createProject(options: ProjectConfig): Promise<string> {
|
|||||||
projectDir,
|
projectDir,
|
||||||
options.database,
|
options.database,
|
||||||
options.orm,
|
options.orm,
|
||||||
|
options.packageManager,
|
||||||
options.turso ?? options.database === "sqlite",
|
options.turso ?? options.database === "sqlite",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ The API is running at [http://localhost:3000](http://localhost:3000).
|
|||||||
|
|
||||||
${
|
${
|
||||||
addons.includes("pwa") && hasReactRouter
|
addons.includes("pwa") && hasReactRouter
|
||||||
? "\n## PWA Support with React Router v7\n\nThere is a known compatibility issue between VitePWA and React Router v7.\nSee: https://github.com/vite-pwa/vite-plugin-pwa/issues/809\n\nIf you encounter problems with the PWA functionality, you may need to manually modify\nthe service worker registration or consider waiting for a fix from VitePWA.\n"
|
? "\n## PWA Support with React Router v7\n\nThere is a known compatibility issue between VitePWA and React Router v7.\nSee: https://github.com/vite-pwa/vite-plugin-pwa/issues/809\n"
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,20 @@ import path from "node:path";
|
|||||||
import { log, spinner } from "@clack/prompts";
|
import { log, spinner } from "@clack/prompts";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { ProjectDatabase, ProjectOrm } from "../types";
|
import type {
|
||||||
|
ProjectDatabase,
|
||||||
|
ProjectOrm,
|
||||||
|
ProjectPackageManager,
|
||||||
|
} from "../types";
|
||||||
import { addPackageDependency } from "../utils/add-package-deps";
|
import { addPackageDependency } from "../utils/add-package-deps";
|
||||||
|
import { setupPrismaPostgres } from "./prisma-postgres-setup";
|
||||||
import { setupTurso } from "./turso-setup";
|
import { setupTurso } from "./turso-setup";
|
||||||
|
|
||||||
export async function setupDatabase(
|
export async function setupDatabase(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
databaseType: ProjectDatabase,
|
databaseType: ProjectDatabase,
|
||||||
orm: ProjectOrm,
|
orm: ProjectOrm,
|
||||||
|
packageManager: ProjectPackageManager,
|
||||||
setupTursoDb = true,
|
setupTursoDb = true,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const s = spinner();
|
const s = spinner();
|
||||||
@@ -52,6 +58,10 @@ export async function setupDatabase(
|
|||||||
devDependencies: ["prisma"],
|
devDependencies: ["prisma"],
|
||||||
projectDir: serverDir,
|
projectDir: serverDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (databaseType === "postgres" && orm === "prisma") {
|
||||||
|
await setupPrismaPostgres(projectDir, true, packageManager);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ function getDatabaseInstructions(
|
|||||||
|
|
||||||
if (runtime === "bun") {
|
if (runtime === "bun") {
|
||||||
instructions.push(
|
instructions.push(
|
||||||
`${pc.yellow("NOTE:")} Prisma with Bun may require additional configuration. If you encounter errors, follow the guidance provided in the error messages`,
|
`${pc.yellow("NOTE:")} Prisma with Bun may require additional configuration. If you encounter errors,\nfollow the guidance provided in the error messages`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
178
apps/cli/src/helpers/prisma-postgres-setup.ts
Normal file
178
apps/cli/src/helpers/prisma-postgres-setup.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { cancel, isCancel, log, password } from "@clack/prompts";
|
||||||
|
import { execa } from "execa";
|
||||||
|
import fs from "fs-extra";
|
||||||
|
import pc from "picocolors";
|
||||||
|
import type { ProjectPackageManager } from "../types";
|
||||||
|
import { addPackageDependency } from "../utils/add-package-deps";
|
||||||
|
|
||||||
|
type PrismaConfig = {
|
||||||
|
databaseUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function initPrismaDatabase(
|
||||||
|
serverDir: string,
|
||||||
|
packageManager: ProjectPackageManager,
|
||||||
|
): Promise<PrismaConfig | null> {
|
||||||
|
try {
|
||||||
|
log.info(pc.blue("Initializing Prisma PostgreSQL"));
|
||||||
|
|
||||||
|
const prismaDir = path.join(serverDir, "prisma");
|
||||||
|
await fs.ensureDir(prismaDir);
|
||||||
|
|
||||||
|
const initCmd =
|
||||||
|
packageManager === "npm"
|
||||||
|
? "npx"
|
||||||
|
: packageManager === "pnpm"
|
||||||
|
? "pnpm dlx"
|
||||||
|
: "bunx";
|
||||||
|
|
||||||
|
await execa(initCmd, ["prisma", "init", "--db"], {
|
||||||
|
cwd: serverDir,
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
pc.yellow(
|
||||||
|
"Please copy the Prisma Postgres URL from the output above.\nIt looks like: prisma+postgres://accelerate.prisma-data.net/?api_key=...",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const databaseUrl = await password({
|
||||||
|
message: "Paste your Prisma Postgres database URL:",
|
||||||
|
validate(value) {
|
||||||
|
if (!value) return "Please enter a database URL";
|
||||||
|
if (!value.startsWith("prisma+postgres://")) {
|
||||||
|
return "URL should start with prisma+postgres://";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isCancel(databaseUrl)) {
|
||||||
|
cancel("Database setup cancelled");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
databaseUrl: databaseUrl as string,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
log.error(pc.red(error.message));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeEnvFile(projectDir: string, config?: PrismaConfig) {
|
||||||
|
const envPath = path.join(projectDir, "apps/server", ".env");
|
||||||
|
let envContent = "";
|
||||||
|
|
||||||
|
if (await fs.pathExists(envPath)) {
|
||||||
|
envContent = await fs.readFile(envPath, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
const databaseUrlLine = config
|
||||||
|
? `DATABASE_URL="${config.databaseUrl}"`
|
||||||
|
: `DATABASE_URL="postgresql://postgres:postgres@localhost:5432/mydb?schema=public"`;
|
||||||
|
|
||||||
|
if (!envContent.includes("DATABASE_URL=")) {
|
||||||
|
envContent += `\n${databaseUrlLine}`;
|
||||||
|
} else {
|
||||||
|
envContent = envContent.replace(
|
||||||
|
/DATABASE_URL=.*(\r?\n|$)/,
|
||||||
|
`${databaseUrlLine}$1`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(envPath, envContent.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayManualSetupInstructions() {
|
||||||
|
log.info(`Manual Prisma PostgreSQL Setup Instructions:
|
||||||
|
|
||||||
|
1. Visit https://console.prisma.io and create an account
|
||||||
|
2. Create a new PostgreSQL database from the dashboard
|
||||||
|
3. Get your database URL
|
||||||
|
4. Add the database URL to the .env file in apps/server/.env
|
||||||
|
|
||||||
|
DATABASE_URL="your_database_url"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setupPrismaPostgres(
|
||||||
|
projectDir: string,
|
||||||
|
shouldSetupPrisma: boolean,
|
||||||
|
packageManager: ProjectPackageManager = "npm",
|
||||||
|
) {
|
||||||
|
const serverDir = path.join(projectDir, "apps/server");
|
||||||
|
|
||||||
|
if (!shouldSetupPrisma) {
|
||||||
|
await writeEnvFile(projectDir);
|
||||||
|
log.info(
|
||||||
|
pc.blue(
|
||||||
|
"Using default Postgres configuration. You'll need to provide your own database.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const config = await initPrismaDatabase(serverDir, packageManager);
|
||||||
|
|
||||||
|
if (config) {
|
||||||
|
await writeEnvFile(projectDir, config);
|
||||||
|
await addPrismaAccelerateExtension(serverDir);
|
||||||
|
log.success(
|
||||||
|
pc.green("Prisma PostgreSQL database configured successfully!"),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await writeEnvFile(projectDir);
|
||||||
|
displayManualSetupInstructions();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error(pc.red(`Error during Prisma PostgreSQL setup: ${error}`));
|
||||||
|
await writeEnvFile(projectDir);
|
||||||
|
displayManualSetupInstructions();
|
||||||
|
log.info("Setup completed with manual configuration required.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addPrismaAccelerateExtension(serverDir: string) {
|
||||||
|
try {
|
||||||
|
addPackageDependency({
|
||||||
|
dependencies: ["@prisma/extension-accelerate"],
|
||||||
|
projectDir: serverDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
const prismaIndexPath = path.join(serverDir, "prisma/index.ts");
|
||||||
|
const prismaIndexContent = `
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import { withAccelerate } from "@prisma/extension-accelerate";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient().$extends(withAccelerate());
|
||||||
|
|
||||||
|
export default prisma;
|
||||||
|
`;
|
||||||
|
await fs.writeFile(prismaIndexPath, prismaIndexContent.trim());
|
||||||
|
|
||||||
|
const dbFilePath = path.join(serverDir, "src/db/index.ts");
|
||||||
|
if (await fs.pathExists(dbFilePath)) {
|
||||||
|
let dbFileContent = await fs.readFile(dbFilePath, "utf8");
|
||||||
|
|
||||||
|
if (!dbFileContent.includes("@prisma/extension-accelerate")) {
|
||||||
|
dbFileContent = `import { withAccelerate } from "@prisma/extension-accelerate";\n${dbFileContent}`;
|
||||||
|
|
||||||
|
dbFileContent = dbFileContent.replace(
|
||||||
|
"export const db = new PrismaClient();",
|
||||||
|
"export const db = new PrismaClient().$extends(withAccelerate());",
|
||||||
|
);
|
||||||
|
|
||||||
|
await fs.writeFile(dbFilePath, dbFileContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.warn(
|
||||||
|
pc.yellow("Could not add Prisma Accelerate extension automatically"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,6 +58,8 @@ async function main() {
|
|||||||
.option("--no-install", "Skip installing dependencies")
|
.option("--no-install", "Skip installing dependencies")
|
||||||
.option("--turso", "Set up Turso for SQLite database")
|
.option("--turso", "Set up Turso for SQLite database")
|
||||||
.option("--no-turso", "Skip Turso setup")
|
.option("--no-turso", "Skip Turso setup")
|
||||||
|
.option("--prisma-postgres", "Set up Prisma Postgres")
|
||||||
|
.option("--no-prisma-postgres", "Skip Prisma Postgres setup")
|
||||||
.option("--backend <framework>", "Backend framework (hono, elysia)")
|
.option("--backend <framework>", "Backend framework (hono, elysia)")
|
||||||
.option("--runtime <runtime>", "Runtime (bun, node)")
|
.option("--runtime <runtime>", "Runtime (bun, node)")
|
||||||
.parse();
|
.parse();
|
||||||
@@ -198,6 +200,20 @@ function validateOptions(options: CLIOptions): void {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("prismaPostgres" in options && options.prismaPostgres === true) {
|
||||||
|
if (
|
||||||
|
(options.database && options.database !== "postgres") ||
|
||||||
|
(options.orm && options.orm !== "prisma")
|
||||||
|
) {
|
||||||
|
cancel(
|
||||||
|
pc.red(
|
||||||
|
"Prisma PostgreSQL setup requires PostgreSQL database with Prisma ORM. Cannot use --prisma-postgres with incompatible database or ORM options.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
options.packageManager &&
|
options.packageManager &&
|
||||||
!["npm", "pnpm", "bun"].includes(options.packageManager)
|
!["npm", "pnpm", "bun"].includes(options.packageManager)
|
||||||
@@ -384,16 +400,36 @@ function processFlags(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const database = options.database as ProjectDatabase | undefined;
|
let database = options.database as ProjectDatabase | undefined;
|
||||||
let orm: ProjectOrm | undefined;
|
let orm: ProjectOrm | undefined;
|
||||||
if (options.orm) {
|
if (options.orm) {
|
||||||
orm = options.orm as ProjectOrm;
|
orm = options.orm as ProjectOrm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("prismaPostgres" in options && options.prismaPostgres === true) {
|
||||||
|
if (!database) {
|
||||||
|
database = "postgres" as ProjectDatabase;
|
||||||
|
}
|
||||||
|
if (!orm) {
|
||||||
|
orm = "prisma" as ProjectOrm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let auth: boolean | undefined = "auth" in options ? options.auth : undefined;
|
let auth: boolean | undefined = "auth" in options ? options.auth : undefined;
|
||||||
let tursoOption: boolean | undefined =
|
let tursoOption: boolean | undefined =
|
||||||
"turso" in options ? options.turso : undefined;
|
"turso" in options ? options.turso : undefined;
|
||||||
|
|
||||||
|
let prismaPostgresOption: boolean | undefined =
|
||||||
|
"prismaPostgres" in options ? options.prismaPostgres : undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
database === "none" ||
|
||||||
|
(database === "sqlite" && database !== undefined) ||
|
||||||
|
(orm !== undefined && orm !== "prisma")
|
||||||
|
) {
|
||||||
|
prismaPostgresOption = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (database === "none") {
|
if (database === "none") {
|
||||||
orm = "none";
|
orm = "none";
|
||||||
auth = false;
|
auth = false;
|
||||||
@@ -473,21 +509,25 @@ function processFlags(
|
|||||||
| ProjectPackageManager
|
| ProjectPackageManager
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
return {
|
const config: Partial<ProjectConfig> = {};
|
||||||
...(projectDirectory && { projectName: projectDirectory }),
|
|
||||||
...(database !== undefined && { database }),
|
if (projectDirectory) config.projectName = projectDirectory;
|
||||||
...(orm !== undefined && { orm }),
|
if (database !== undefined) config.database = database;
|
||||||
...(auth !== undefined && { auth }),
|
if (orm !== undefined) config.orm = orm;
|
||||||
...(packageManager && { packageManager }),
|
if (auth !== undefined) config.auth = auth;
|
||||||
...("git" in options && { git: options.git }),
|
if (packageManager) config.packageManager = packageManager;
|
||||||
...("install" in options && { noInstall: !options.install }),
|
if ("git" in options) config.git = options.git;
|
||||||
...(tursoOption !== undefined && { turso: tursoOption }),
|
if ("install" in options) config.noInstall = !options.install;
|
||||||
...(backend && { backend }),
|
if (tursoOption !== undefined) config.turso = tursoOption;
|
||||||
...(runtime && { runtime }),
|
if (prismaPostgresOption !== undefined)
|
||||||
...(frontend !== undefined && { frontend }),
|
config.prismaPostgres = prismaPostgresOption;
|
||||||
...(addons !== undefined && { addons }),
|
if (backend) config.backend = backend;
|
||||||
...(examples !== undefined && { examples }),
|
if (runtime) config.runtime = runtime;
|
||||||
};
|
if (frontend !== undefined) config.frontend = frontend;
|
||||||
|
if (addons !== undefined) config.addons = addons;
|
||||||
|
if (examples !== undefined) config.examples = examples;
|
||||||
|
|
||||||
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((err) => {
|
main().catch((err) => {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { getGitChoice } from "./git";
|
|||||||
import { getNoInstallChoice } from "./install";
|
import { getNoInstallChoice } from "./install";
|
||||||
import { getORMChoice } from "./orm";
|
import { getORMChoice } from "./orm";
|
||||||
import { getPackageManagerChoice } from "./package-manager";
|
import { getPackageManagerChoice } from "./package-manager";
|
||||||
|
import { getPrismaSetupChoice } from "./prisma-postgres";
|
||||||
import { getProjectName } from "./project-name";
|
import { getProjectName } from "./project-name";
|
||||||
import { getRuntimeChoice } from "./runtime";
|
import { getRuntimeChoice } from "./runtime";
|
||||||
import { getTursoSetupChoice } from "./turso";
|
import { getTursoSetupChoice } from "./turso";
|
||||||
@@ -36,6 +37,7 @@ type PromptGroupResults = {
|
|||||||
packageManager: ProjectPackageManager;
|
packageManager: ProjectPackageManager;
|
||||||
noInstall: boolean;
|
noInstall: boolean;
|
||||||
turso: boolean;
|
turso: boolean;
|
||||||
|
prismaPostgres: boolean;
|
||||||
backend: ProjectBackend;
|
backend: ProjectBackend;
|
||||||
runtime: ProjectRuntime;
|
runtime: ProjectRuntime;
|
||||||
frontend: ProjectFrontend[];
|
frontend: ProjectFrontend[];
|
||||||
@@ -65,6 +67,10 @@ export async function gatherConfig(
|
|||||||
results.database === "sqlite" && results.orm !== "prisma"
|
results.database === "sqlite" && results.orm !== "prisma"
|
||||||
? getTursoSetupChoice(flags.turso)
|
? getTursoSetupChoice(flags.turso)
|
||||||
: Promise.resolve(false),
|
: Promise.resolve(false),
|
||||||
|
prismaPostgres: ({ results }) =>
|
||||||
|
results.database === "postgres" && results.orm === "prisma"
|
||||||
|
? getPrismaSetupChoice(flags.prismaPostgres)
|
||||||
|
: Promise.resolve(false),
|
||||||
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend),
|
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend),
|
||||||
examples: ({ results }) =>
|
examples: ({ results }) =>
|
||||||
getExamplesChoice(
|
getExamplesChoice(
|
||||||
@@ -97,6 +103,7 @@ export async function gatherConfig(
|
|||||||
packageManager: result.packageManager,
|
packageManager: result.packageManager,
|
||||||
noInstall: result.noInstall,
|
noInstall: result.noInstall,
|
||||||
turso: result.turso,
|
turso: result.turso,
|
||||||
|
prismaPostgres: result.prismaPostgres,
|
||||||
backend: result.backend,
|
backend: result.backend,
|
||||||
runtime: result.runtime,
|
runtime: result.runtime,
|
||||||
};
|
};
|
||||||
|
|||||||
21
apps/cli/src/prompts/prisma-postgres.ts
Normal file
21
apps/cli/src/prompts/prisma-postgres.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { cancel, confirm, isCancel } from "@clack/prompts";
|
||||||
|
import pc from "picocolors";
|
||||||
|
import { DEFAULT_CONFIG } from "../constants";
|
||||||
|
|
||||||
|
export async function getPrismaSetupChoice(
|
||||||
|
prismaSetup?: boolean,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (prismaSetup !== undefined) return prismaSetup;
|
||||||
|
|
||||||
|
const response = await confirm({
|
||||||
|
message: "Set up Prisma Postgres database?",
|
||||||
|
initialValue: DEFAULT_CONFIG.prismaPostgres,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isCancel(response)) {
|
||||||
|
cancel(pc.red("Operation cancelled"));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ export interface ProjectConfig {
|
|||||||
packageManager: ProjectPackageManager;
|
packageManager: ProjectPackageManager;
|
||||||
noInstall?: boolean;
|
noInstall?: boolean;
|
||||||
turso?: boolean;
|
turso?: boolean;
|
||||||
|
prismaPostgres: boolean;
|
||||||
frontend: ProjectFrontend[];
|
frontend: ProjectFrontend[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ export type CLIOptions = {
|
|||||||
packageManager?: string;
|
packageManager?: string;
|
||||||
install?: boolean;
|
install?: boolean;
|
||||||
turso?: boolean;
|
turso?: boolean;
|
||||||
|
prismaPostgres?: boolean;
|
||||||
backend?: string;
|
backend?: string;
|
||||||
runtime?: string;
|
runtime?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -64,5 +64,11 @@ export function displayConfig(config: Partial<ProjectConfig>) {
|
|||||||
configDisplay.push(`${pc.blue("Turso Setup:")} ${config.turso}`);
|
configDisplay.push(`${pc.blue("Turso Setup:")} ${config.turso}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.prismaPostgres !== undefined) {
|
||||||
|
configDisplay.push(
|
||||||
|
`${pc.blue("Prisma Postgres Setup:")} ${config.prismaPostgres ? "Yes" : "No"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return configDisplay.join("\n");
|
return configDisplay.join("\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import StackArchitect from "./StackArchitech";
|
|||||||
|
|
||||||
export default function CustomizableSection() {
|
export default function CustomizableSection() {
|
||||||
return (
|
return (
|
||||||
<section className="w-full max-w-6xl mx-auto space-y-16 mt-20 relative z-10 px-4 sm:px-6">
|
<section className="w-full max-w-7xl mx-auto space-y-16 mt-20 relative z-10 px-4 sm:px-6">
|
||||||
<div className="text-center space-y-8 relative">
|
<div className="text-center space-y-8 relative">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
|||||||
@@ -181,6 +181,24 @@ const TECH_OPTIONS = {
|
|||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
prismaPostgres: [
|
||||||
|
{
|
||||||
|
id: "true",
|
||||||
|
name: "Prisma PostgreSQL",
|
||||||
|
description: "Set up PostgreSQL with Prisma",
|
||||||
|
icon: "🐘",
|
||||||
|
color: "from-indigo-400 to-indigo-600",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "false",
|
||||||
|
name: "Skip Prisma PostgreSQL",
|
||||||
|
description: "Basic Prisma setup",
|
||||||
|
icon: "🚫",
|
||||||
|
color: "from-gray-400 to-gray-600",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
packageManager: [
|
packageManager: [
|
||||||
{
|
{
|
||||||
id: "npm",
|
id: "npm",
|
||||||
@@ -302,6 +320,7 @@ interface StackState {
|
|||||||
orm: string | null;
|
orm: string | null;
|
||||||
auth: string;
|
auth: string;
|
||||||
turso: string;
|
turso: string;
|
||||||
|
prismaPostgres: string;
|
||||||
packageManager: string;
|
packageManager: string;
|
||||||
addons: string[];
|
addons: string[];
|
||||||
examples: string[];
|
examples: string[];
|
||||||
@@ -318,6 +337,7 @@ const DEFAULT_STACK: StackState = {
|
|||||||
orm: "drizzle",
|
orm: "drizzle",
|
||||||
auth: "true",
|
auth: "true",
|
||||||
turso: "false",
|
turso: "false",
|
||||||
|
prismaPostgres: "false",
|
||||||
packageManager: "bun",
|
packageManager: "bun",
|
||||||
addons: [],
|
addons: [],
|
||||||
examples: [],
|
examples: [],
|
||||||
@@ -349,6 +369,24 @@ const StackArchitect = () => {
|
|||||||
}
|
}
|
||||||
}, [stack.frontend, stack.auth]);
|
}, [stack.frontend, stack.auth]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (stack.database === "none" && stack.orm !== "none") {
|
||||||
|
setStack((prev) => ({ ...prev, orm: "none" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stack.database !== "postgres" || stack.orm !== "prisma") {
|
||||||
|
if (stack.prismaPostgres === "true") {
|
||||||
|
setStack((prev) => ({ ...prev, prismaPostgres: "false" }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stack.database !== "sqlite" || stack.orm === "prisma") {
|
||||||
|
if (stack.turso === "true") {
|
||||||
|
setStack((prev) => ({ ...prev, turso: "false" }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [stack.database, stack.orm, stack.prismaPostgres, stack.turso]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cmd = generateCommand(stack);
|
const cmd = generateCommand(stack);
|
||||||
setCommand(cmd);
|
setCommand(cmd);
|
||||||
@@ -387,13 +425,26 @@ const StackArchitect = () => {
|
|||||||
"Turso integration is only available with SQLite database.",
|
"Turso integration is only available with SQLite database.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (stack.orm === "prisma") {
|
||||||
|
notes.turso.push("Turso is not compatible with Prisma ORM.");
|
||||||
|
}
|
||||||
|
|
||||||
|
notes.prismaPostgres = [];
|
||||||
|
if (stack.database !== "postgres" || stack.orm !== "prisma") {
|
||||||
|
notes.prismaPostgres.push(
|
||||||
|
"Prisma PostgreSQL setup requires PostgreSQL database with Prisma ORM.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
notes.examples = [];
|
notes.examples = [];
|
||||||
if (!hasWebFrontend) {
|
if (!hasWebFrontend) {
|
||||||
notes.examples.push(
|
notes.examples.push(
|
||||||
"Todo and Ai example are only available with React Web.",
|
"Todo and AI examples are only available with React Web.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (stack.backendFramework === "elysia") {
|
||||||
|
notes.examples.push("AI example is only compatible with Hono backend.");
|
||||||
|
}
|
||||||
|
|
||||||
setCompatNotes(notes);
|
setCompatNotes(notes);
|
||||||
}, [stack]);
|
}, [stack]);
|
||||||
@@ -438,6 +489,10 @@ const StackArchitect = () => {
|
|||||||
flags.push("--turso");
|
flags.push("--turso");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (stackState.prismaPostgres === "true") {
|
||||||
|
flags.push("--prisma-postgres");
|
||||||
|
}
|
||||||
|
|
||||||
if (stackState.backendFramework !== "hono") {
|
if (stackState.backendFramework !== "hono") {
|
||||||
flags.push(`--backend ${stackState.backendFramework}`);
|
flags.push(`--backend ${stackState.backendFramework}`);
|
||||||
}
|
}
|
||||||
@@ -481,18 +536,14 @@ const StackArchitect = () => {
|
|||||||
...prev,
|
...prev,
|
||||||
frontend: ["none"],
|
frontend: ["none"],
|
||||||
auth: "false",
|
auth: "false",
|
||||||
examples: prev.examples.filter(
|
examples: [],
|
||||||
(ex) => ex !== "todo" && ex !== "ai",
|
|
||||||
),
|
|
||||||
addons: prev.addons.filter(
|
addons: prev.addons.filter(
|
||||||
(addon) => addon !== "pwa" && addon !== "tauri",
|
(addon) => addon !== "pwa" && addon !== "tauri",
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle web router types (tanstack-router or react-router)
|
|
||||||
if (webTypes.includes(techId)) {
|
if (webTypes.includes(techId)) {
|
||||||
// If clicking on an already selected web router, do nothing
|
|
||||||
if (
|
if (
|
||||||
currentSelection.includes(techId) &&
|
currentSelection.includes(techId) &&
|
||||||
currentSelection.length === 1
|
currentSelection.length === 1
|
||||||
@@ -500,7 +551,6 @@ const StackArchitect = () => {
|
|||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If selecting a web router while another one is active, replace it
|
|
||||||
if (currentSelection.some((id) => webTypes.includes(id))) {
|
if (currentSelection.some((id) => webTypes.includes(id))) {
|
||||||
const nonWebSelections = currentSelection.filter(
|
const nonWebSelections = currentSelection.filter(
|
||||||
(id) => !webTypes.includes(id),
|
(id) => !webTypes.includes(id),
|
||||||
@@ -508,11 +558,10 @@ const StackArchitect = () => {
|
|||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
frontend: [...nonWebSelections, techId],
|
frontend: [...nonWebSelections, techId],
|
||||||
auth: prev.auth, // Keep existing auth setting
|
auth: prev.auth,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no web router was selected before
|
|
||||||
if (currentSelection.includes("none")) {
|
if (currentSelection.includes("none")) {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
@@ -531,11 +580,10 @@ const StackArchitect = () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle native selection
|
|
||||||
if (techId === "native") {
|
if (techId === "native") {
|
||||||
if (currentSelection.includes(techId)) {
|
if (currentSelection.includes(techId)) {
|
||||||
if (currentSelection.length === 1) {
|
if (currentSelection.length === 1) {
|
||||||
return prev; // Don't allow removing the last frontend
|
return prev;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
@@ -571,11 +619,20 @@ const StackArchitect = () => {
|
|||||||
} else {
|
} else {
|
||||||
if (
|
if (
|
||||||
category === "examples" &&
|
category === "examples" &&
|
||||||
techId === "todo" &&
|
(techId === "todo" || techId === "ai") &&
|
||||||
!hasWebFrontend
|
!hasWebFrontend
|
||||||
) {
|
) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
category === "examples" &&
|
||||||
|
techId === "ai" &&
|
||||||
|
prev.backendFramework === "elysia"
|
||||||
|
) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
category === "addons" &&
|
category === "addons" &&
|
||||||
(techId === "pwa" || techId === "tauri") &&
|
(techId === "pwa" || techId === "tauri") &&
|
||||||
@@ -583,6 +640,15 @@ const StackArchitect = () => {
|
|||||||
) {
|
) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
category === "addons" &&
|
||||||
|
techId === "husky" &&
|
||||||
|
!currentArray.includes("biome")
|
||||||
|
) {
|
||||||
|
currentArray.push("biome");
|
||||||
|
}
|
||||||
|
|
||||||
currentArray.push(techId);
|
currentArray.push(techId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,8 +663,10 @@ const StackArchitect = () => {
|
|||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
database: techId,
|
database: techId,
|
||||||
orm: null,
|
orm: "none",
|
||||||
turso: "false",
|
turso: "false",
|
||||||
|
prismaPostgres: "false",
|
||||||
|
auth: hasWebFrontend(prev.frontend) ? prev.auth : "false",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,25 +675,63 @@ const StackArchitect = () => {
|
|||||||
...prev,
|
...prev,
|
||||||
database: techId,
|
database: techId,
|
||||||
orm: "drizzle",
|
orm: "drizzle",
|
||||||
|
turso: techId === "sqlite" ? prev.turso : "false",
|
||||||
|
prismaPostgres:
|
||||||
|
techId === "postgres" && prev.orm === "prisma"
|
||||||
|
? prev.prismaPostgres
|
||||||
|
: "false",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (techId === "sqlite") {
|
const updatedState = {
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
database: techId,
|
|
||||||
turso: prev.turso,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...prev,
|
...prev,
|
||||||
database: techId,
|
database: techId,
|
||||||
turso: "false",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (techId === "sqlite") {
|
||||||
|
updatedState.prismaPostgres = "false";
|
||||||
|
} else if (techId === "postgres" && prev.orm === "prisma") {
|
||||||
|
} else {
|
||||||
|
updatedState.turso = "false";
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedState;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (category === "turso" && prev.database !== "sqlite") {
|
if (category === "orm") {
|
||||||
|
if (prev.database === "none") {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedState = {
|
||||||
|
...prev,
|
||||||
|
orm: techId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (techId === "prisma") {
|
||||||
|
updatedState.turso = "false";
|
||||||
|
if (prev.database === "postgres") {
|
||||||
|
} else {
|
||||||
|
updatedState.prismaPostgres = "false";
|
||||||
|
}
|
||||||
|
} else if (techId === "drizzle" || techId === "none") {
|
||||||
|
updatedState.prismaPostgres = "false";
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedState;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
category === "turso" &&
|
||||||
|
(prev.database !== "sqlite" || prev.orm === "prisma")
|
||||||
|
) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
category === "prismaPostgres" &&
|
||||||
|
(prev.database !== "postgres" || prev.orm !== "prisma")
|
||||||
|
) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,6 +744,13 @@ const StackArchitect = () => {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hasWebFrontend = useCallback((frontendOptions: string[]) => {
|
||||||
|
return (
|
||||||
|
frontendOptions.includes("tanstack-router") ||
|
||||||
|
frontendOptions.includes("react-router")
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const copyToClipboard = useCallback(() => {
|
const copyToClipboard = useCallback(() => {
|
||||||
navigator.clipboard.writeText(command);
|
navigator.clipboard.writeText(command);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
@@ -742,32 +855,40 @@ const StackArchitect = () => {
|
|||||||
stack[activeTab as keyof StackState] === tech.id;
|
stack[activeTab as keyof StackState] === tech.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasWebFrontend =
|
const hasWebFrontendSelected =
|
||||||
stack.frontend.includes("tanstack-router") ||
|
stack.frontend.includes("tanstack-router") ||
|
||||||
stack.frontend.includes("react-router");
|
stack.frontend.includes("react-router");
|
||||||
|
|
||||||
const isDisabled =
|
const isDisabled =
|
||||||
(activeTab === "orm" && stack.database === "none") ||
|
(activeTab === "orm" && stack.database === "none") ||
|
||||||
(activeTab === "turso" && stack.database !== "sqlite") ||
|
(activeTab === "turso" &&
|
||||||
(activeTab === "auth" && !hasWebFrontend) ||
|
(stack.database !== "sqlite" ||
|
||||||
|
stack.orm === "prisma")) ||
|
||||||
|
(activeTab === "prismaPostgres" &&
|
||||||
|
(stack.database !== "postgres" ||
|
||||||
|
stack.orm !== "prisma")) ||
|
||||||
|
(activeTab === "auth" && !hasWebFrontendSelected) ||
|
||||||
(activeTab === "examples" &&
|
(activeTab === "examples" &&
|
||||||
((tech.id === "todo" && !hasWebFrontend) ||
|
(((tech.id === "todo" || tech.id === "ai") &&
|
||||||
(tech.id === "ai" && !hasWebFrontend))) ||
|
!hasWebFrontendSelected) ||
|
||||||
|
(tech.id === "ai" &&
|
||||||
|
stack.backendFramework === "elysia"))) ||
|
||||||
(activeTab === "addons" &&
|
(activeTab === "addons" &&
|
||||||
(tech.id === "pwa" || tech.id === "tauri") &&
|
(tech.id === "pwa" || tech.id === "tauri") &&
|
||||||
!hasWebFrontend);
|
!hasWebFrontendSelected);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={tech.id}
|
key={tech.id}
|
||||||
className={`
|
className={`
|
||||||
p-2 px-3 rounded
|
p-2 px-3 rounded
|
||||||
${isDisabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}
|
${isDisabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}
|
||||||
${
|
${
|
||||||
isSelected
|
isSelected
|
||||||
? "bg-blue-100 dark:bg-blue-900/40 border border-blue-300 dark:border-blue-500/50"
|
? "bg-blue-100 dark:bg-blue-900/40 border border-blue-300 dark:border-blue-500/50"
|
||||||
: "hover:bg-gray-200 dark:hover:bg-gray-800 border border-gray-300 dark:border-gray-700"
|
: "hover:bg-gray-200 dark:hover:bg-gray-800 border border-gray-300 dark:border-gray-700"
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
whileHover={!isDisabled ? { scale: 1.02 } : undefined}
|
whileHover={!isDisabled ? { scale: 1.02 } : undefined}
|
||||||
whileTap={!isDisabled ? { scale: 0.98 } : undefined}
|
whileTap={!isDisabled ? { scale: 0.98 } : undefined}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -868,25 +989,52 @@ const StackArchitect = () => {
|
|||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{stack.orm && (
|
{stack.orm && stack.database !== "none" && (
|
||||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-cyan-100 dark:bg-cyan-900/30 text-cyan-800 dark:text-cyan-300 border border-cyan-300 dark:border-cyan-700/30">
|
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-cyan-100 dark:bg-cyan-900/30 text-cyan-800 dark:text-cyan-300 border border-cyan-300 dark:border-cyan-700/30">
|
||||||
{TECH_OPTIONS.orm.find((t) => t.id === stack.orm)?.icon}{" "}
|
{TECH_OPTIONS.orm.find((t) => t.id === stack.orm)?.icon}{" "}
|
||||||
{TECH_OPTIONS.orm.find((t) => t.id === stack.orm)?.name}
|
{TECH_OPTIONS.orm.find((t) => t.id === stack.orm)?.name}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 border border-green-300 dark:border-green-700/30">
|
{hasWebFrontend(stack.frontend) && (
|
||||||
{TECH_OPTIONS.auth.find((t) => t.id === stack.auth)?.icon}{" "}
|
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 border border-green-300 dark:border-green-700/30">
|
||||||
{TECH_OPTIONS.auth.find((t) => t.id === stack.auth)?.name}
|
{TECH_OPTIONS.auth.find((t) => t.id === stack.auth)?.icon}{" "}
|
||||||
</span>
|
{TECH_OPTIONS.auth.find((t) => t.id === stack.auth)?.name}
|
||||||
|
|
||||||
{stack.turso === "true" && stack.database === "sqlite" && (
|
|
||||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-pink-100 dark:bg-pink-900/30 text-pink-800 dark:text-pink-300 border border-pink-300 dark:border-pink-700/30">
|
|
||||||
{TECH_OPTIONS.turso.find((t) => t.id === stack.turso)?.icon}{" "}
|
|
||||||
{TECH_OPTIONS.turso.find((t) => t.id === stack.turso)?.name}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{stack.turso === "true" &&
|
||||||
|
stack.database === "sqlite" &&
|
||||||
|
stack.orm !== "prisma" && (
|
||||||
|
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-pink-100 dark:bg-pink-900/30 text-pink-800 dark:text-pink-300 border border-pink-300 dark:border-pink-700/30">
|
||||||
|
{
|
||||||
|
TECH_OPTIONS.turso.find((t) => t.id === stack.turso)
|
||||||
|
?.icon
|
||||||
|
}{" "}
|
||||||
|
{
|
||||||
|
TECH_OPTIONS.turso.find((t) => t.id === stack.turso)
|
||||||
|
?.name
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{stack.prismaPostgres === "true" &&
|
||||||
|
stack.database === "postgres" &&
|
||||||
|
stack.orm === "prisma" && (
|
||||||
|
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-indigo-100 dark:bg-indigo-900/30 text-indigo-800 dark:text-indigo-300 border border-indigo-300 dark:border-indigo-700/30">
|
||||||
|
{
|
||||||
|
TECH_OPTIONS.prismaPostgres.find(
|
||||||
|
(t) => t.id === stack.prismaPostgres,
|
||||||
|
)?.icon
|
||||||
|
}{" "}
|
||||||
|
{
|
||||||
|
TECH_OPTIONS.prismaPostgres.find(
|
||||||
|
(t) => t.id === stack.prismaPostgres,
|
||||||
|
)?.name
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
{stack.addons.map((addonId) => {
|
{stack.addons.map((addonId) => {
|
||||||
const addon = TECH_OPTIONS.addons.find(
|
const addon = TECH_OPTIONS.addons.find(
|
||||||
(a) => a.id === addonId,
|
(a) => a.id === addonId,
|
||||||
@@ -925,13 +1073,13 @@ const StackArchitect = () => {
|
|||||||
type="button"
|
type="button"
|
||||||
key={category}
|
key={category}
|
||||||
className={`
|
className={`
|
||||||
py-2 px-4 text-xs font-mono whitespace-nowrap transition-colors
|
py-2 px-4 text-xs font-mono whitespace-nowrap transition-colors
|
||||||
${
|
${
|
||||||
activeTab === category
|
activeTab === category
|
||||||
? "bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border-t-2 border-blue-500"
|
? "bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border-t-2 border-blue-500"
|
||||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-800"
|
: "text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-800"
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
onClick={() => setActiveTab(category)}
|
onClick={() => setActiveTab(category)}
|
||||||
>
|
>
|
||||||
{category}
|
{category}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { useEffect, useState } from "react";
|
|||||||
import { Tweet } from "react-tweet";
|
import { Tweet } from "react-tweet";
|
||||||
|
|
||||||
const TWEET_IDS = [
|
const TWEET_IDS = [
|
||||||
|
"1907728148294447538",
|
||||||
|
"1907723601731530820",
|
||||||
"1904144343125860404",
|
"1904144343125860404",
|
||||||
"1904215768272654825",
|
"1904215768272654825",
|
||||||
"1904233896851521980",
|
"1904233896851521980",
|
||||||
|
|||||||
Reference in New Issue
Block a user