add automatic prisma postgres setup

fix tanstack auth template
This commit is contained in:
Aman Varshney
2025-04-03 18:32:10 +05:30
parent 31c7f8f7f7
commit cc563816ea
22 changed files with 499 additions and 79 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": minor
---
Add automatic prisma postgres setup

View File

@@ -7,10 +7,7 @@
"bin": {
"create-better-t-stack": "dist/index.js"
},
"files": [
"dist",
"template"
],
"files": ["dist", "template"],
"keywords": [
"typescript",
"scaffold",

View File

@@ -19,6 +19,7 @@ export const DEFAULT_CONFIG: ProjectConfig = {
packageManager: getUserPkgManager(),
noInstall: false,
turso: false,
prismaPostgres: false,
backend: "hono",
runtime: "bun",
};
@@ -62,6 +63,8 @@ export const dependencyVersionMap = {
ai: "^4.2.8",
"@ai-sdk/google": "^1.2.3",
"@prisma/extension-accelerate": "^1.3.0",
} as const;
export type AvailableDependencies = keyof typeof dependencyVersionMap;

View File

@@ -53,6 +53,7 @@ export async function createProject(options: ProjectConfig): Promise<string> {
projectDir,
options.database,
options.orm,
options.packageManager,
options.turso ?? options.database === "sqlite",
);

View File

@@ -75,7 +75,7 @@ The API is running at [http://localhost:3000](http://localhost:3000).
${
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"
: ""
}

View File

@@ -2,14 +2,20 @@ import path from "node:path";
import { log, spinner } from "@clack/prompts";
import fs from "fs-extra";
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 { setupPrismaPostgres } from "./prisma-postgres-setup";
import { setupTurso } from "./turso-setup";
export async function setupDatabase(
projectDir: string,
databaseType: ProjectDatabase,
orm: ProjectOrm,
packageManager: ProjectPackageManager,
setupTursoDb = true,
): Promise<void> {
const s = spinner();
@@ -52,6 +58,10 @@ export async function setupDatabase(
devDependencies: ["prisma"],
projectDir: serverDir,
});
if (databaseType === "postgres" && orm === "prisma") {
await setupPrismaPostgres(projectDir, true, packageManager);
}
}
}
} catch (error) {

View File

@@ -89,7 +89,7 @@ function getDatabaseInstructions(
if (runtime === "bun") {
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`,
);
}

View 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"),
);
}
}

View File

@@ -58,6 +58,8 @@ async function main() {
.option("--no-install", "Skip installing dependencies")
.option("--turso", "Set up Turso for SQLite database")
.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("--runtime <runtime>", "Runtime (bun, node)")
.parse();
@@ -198,6 +200,20 @@ function validateOptions(options: CLIOptions): void {
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 (
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;
if (options.orm) {
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 tursoOption: boolean | 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") {
orm = "none";
auth = false;
@@ -473,21 +509,25 @@ function processFlags(
| ProjectPackageManager
| undefined;
return {
...(projectDirectory && { projectName: projectDirectory }),
...(database !== undefined && { database }),
...(orm !== undefined && { orm }),
...(auth !== undefined && { auth }),
...(packageManager && { packageManager }),
...("git" in options && { git: options.git }),
...("install" in options && { noInstall: !options.install }),
...(tursoOption !== undefined && { turso: tursoOption }),
...(backend && { backend }),
...(runtime && { runtime }),
...(frontend !== undefined && { frontend }),
...(addons !== undefined && { addons }),
...(examples !== undefined && { examples }),
};
const config: Partial<ProjectConfig> = {};
if (projectDirectory) config.projectName = projectDirectory;
if (database !== undefined) config.database = database;
if (orm !== undefined) config.orm = orm;
if (auth !== undefined) config.auth = auth;
if (packageManager) config.packageManager = packageManager;
if ("git" in options) config.git = options.git;
if ("install" in options) config.noInstall = !options.install;
if (tursoOption !== undefined) config.turso = tursoOption;
if (prismaPostgresOption !== undefined)
config.prismaPostgres = prismaPostgresOption;
if (backend) config.backend = backend;
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) => {

View File

@@ -21,6 +21,7 @@ import { getGitChoice } from "./git";
import { getNoInstallChoice } from "./install";
import { getORMChoice } from "./orm";
import { getPackageManagerChoice } from "./package-manager";
import { getPrismaSetupChoice } from "./prisma-postgres";
import { getProjectName } from "./project-name";
import { getRuntimeChoice } from "./runtime";
import { getTursoSetupChoice } from "./turso";
@@ -36,6 +37,7 @@ type PromptGroupResults = {
packageManager: ProjectPackageManager;
noInstall: boolean;
turso: boolean;
prismaPostgres: boolean;
backend: ProjectBackend;
runtime: ProjectRuntime;
frontend: ProjectFrontend[];
@@ -65,6 +67,10 @@ export async function gatherConfig(
results.database === "sqlite" && results.orm !== "prisma"
? getTursoSetupChoice(flags.turso)
: Promise.resolve(false),
prismaPostgres: ({ results }) =>
results.database === "postgres" && results.orm === "prisma"
? getPrismaSetupChoice(flags.prismaPostgres)
: Promise.resolve(false),
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend),
examples: ({ results }) =>
getExamplesChoice(
@@ -97,6 +103,7 @@ export async function gatherConfig(
packageManager: result.packageManager,
noInstall: result.noInstall,
turso: result.turso,
prismaPostgres: result.prismaPostgres,
backend: result.backend,
runtime: result.runtime,
};

View 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;
}

View File

@@ -20,6 +20,7 @@ export interface ProjectConfig {
packageManager: ProjectPackageManager;
noInstall?: boolean;
turso?: boolean;
prismaPostgres: boolean;
frontend: ProjectFrontend[];
}
@@ -35,6 +36,7 @@ export type CLIOptions = {
packageManager?: string;
install?: boolean;
turso?: boolean;
prismaPostgres?: boolean;
backend?: string;
runtime?: string;
};

View File

@@ -64,5 +64,11 @@ export function displayConfig(config: Partial<ProjectConfig>) {
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");
}

View File

@@ -4,7 +4,7 @@ import StackArchitect from "./StackArchitech";
export default function CustomizableSection() {
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">
<motion.div
initial={{ opacity: 0, y: 20 }}

View File

@@ -181,6 +181,24 @@ const TECH_OPTIONS = {
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: [
{
id: "npm",
@@ -302,6 +320,7 @@ interface StackState {
orm: string | null;
auth: string;
turso: string;
prismaPostgres: string;
packageManager: string;
addons: string[];
examples: string[];
@@ -318,6 +337,7 @@ const DEFAULT_STACK: StackState = {
orm: "drizzle",
auth: "true",
turso: "false",
prismaPostgres: "false",
packageManager: "bun",
addons: [],
examples: [],
@@ -349,6 +369,24 @@ const StackArchitect = () => {
}
}, [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(() => {
const cmd = generateCommand(stack);
setCommand(cmd);
@@ -387,13 +425,26 @@ const StackArchitect = () => {
"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 = [];
if (!hasWebFrontend) {
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);
}, [stack]);
@@ -438,6 +489,10 @@ const StackArchitect = () => {
flags.push("--turso");
}
if (stackState.prismaPostgres === "true") {
flags.push("--prisma-postgres");
}
if (stackState.backendFramework !== "hono") {
flags.push(`--backend ${stackState.backendFramework}`);
}
@@ -481,18 +536,14 @@ const StackArchitect = () => {
...prev,
frontend: ["none"],
auth: "false",
examples: prev.examples.filter(
(ex) => ex !== "todo" && ex !== "ai",
),
examples: [],
addons: prev.addons.filter(
(addon) => addon !== "pwa" && addon !== "tauri",
),
};
}
// Handle web router types (tanstack-router or react-router)
if (webTypes.includes(techId)) {
// If clicking on an already selected web router, do nothing
if (
currentSelection.includes(techId) &&
currentSelection.length === 1
@@ -500,7 +551,6 @@ const StackArchitect = () => {
return prev;
}
// If selecting a web router while another one is active, replace it
if (currentSelection.some((id) => webTypes.includes(id))) {
const nonWebSelections = currentSelection.filter(
(id) => !webTypes.includes(id),
@@ -508,11 +558,10 @@ const StackArchitect = () => {
return {
...prev,
frontend: [...nonWebSelections, techId],
auth: prev.auth, // Keep existing auth setting
auth: prev.auth,
};
}
// If no web router was selected before
if (currentSelection.includes("none")) {
return {
...prev,
@@ -531,11 +580,10 @@ const StackArchitect = () => {
};
}
// Handle native selection
if (techId === "native") {
if (currentSelection.includes(techId)) {
if (currentSelection.length === 1) {
return prev; // Don't allow removing the last frontend
return prev;
}
return {
...prev,
@@ -571,11 +619,20 @@ const StackArchitect = () => {
} else {
if (
category === "examples" &&
techId === "todo" &&
(techId === "todo" || techId === "ai") &&
!hasWebFrontend
) {
return prev;
}
if (
category === "examples" &&
techId === "ai" &&
prev.backendFramework === "elysia"
) {
return prev;
}
if (
category === "addons" &&
(techId === "pwa" || techId === "tauri") &&
@@ -583,6 +640,15 @@ const StackArchitect = () => {
) {
return prev;
}
if (
category === "addons" &&
techId === "husky" &&
!currentArray.includes("biome")
) {
currentArray.push("biome");
}
currentArray.push(techId);
}
@@ -597,8 +663,10 @@ const StackArchitect = () => {
return {
...prev,
database: techId,
orm: null,
orm: "none",
turso: "false",
prismaPostgres: "false",
auth: hasWebFrontend(prev.frontend) ? prev.auth : "false",
};
}
@@ -607,25 +675,63 @@ const StackArchitect = () => {
...prev,
database: techId,
orm: "drizzle",
turso: techId === "sqlite" ? prev.turso : "false",
prismaPostgres:
techId === "postgres" && prev.orm === "prisma"
? prev.prismaPostgres
: "false",
};
}
if (techId === "sqlite") {
return {
...prev,
database: techId,
turso: prev.turso,
};
}
return {
const updatedState = {
...prev,
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;
}
@@ -638,6 +744,13 @@ const StackArchitect = () => {
[],
);
const hasWebFrontend = useCallback((frontendOptions: string[]) => {
return (
frontendOptions.includes("tanstack-router") ||
frontendOptions.includes("react-router")
);
}, []);
const copyToClipboard = useCallback(() => {
navigator.clipboard.writeText(command);
setCopied(true);
@@ -742,32 +855,40 @@ const StackArchitect = () => {
stack[activeTab as keyof StackState] === tech.id;
}
const hasWebFrontend =
const hasWebFrontendSelected =
stack.frontend.includes("tanstack-router") ||
stack.frontend.includes("react-router");
const isDisabled =
(activeTab === "orm" && stack.database === "none") ||
(activeTab === "turso" && stack.database !== "sqlite") ||
(activeTab === "auth" && !hasWebFrontend) ||
(activeTab === "turso" &&
(stack.database !== "sqlite" ||
stack.orm === "prisma")) ||
(activeTab === "prismaPostgres" &&
(stack.database !== "postgres" ||
stack.orm !== "prisma")) ||
(activeTab === "auth" && !hasWebFrontendSelected) ||
(activeTab === "examples" &&
((tech.id === "todo" && !hasWebFrontend) ||
(tech.id === "ai" && !hasWebFrontend))) ||
(((tech.id === "todo" || tech.id === "ai") &&
!hasWebFrontendSelected) ||
(tech.id === "ai" &&
stack.backendFramework === "elysia"))) ||
(activeTab === "addons" &&
(tech.id === "pwa" || tech.id === "tauri") &&
!hasWebFrontend);
!hasWebFrontendSelected);
return (
<motion.div
key={tech.id}
className={`
p-2 px-3 rounded
${isDisabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}
${
isSelected
? "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"
}
`}
p-2 px-3 rounded
${isDisabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}
${
isSelected
? "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"
}
`}
whileHover={!isDisabled ? { scale: 1.02 } : undefined}
whileTap={!isDisabled ? { scale: 0.98 } : undefined}
onClick={() =>
@@ -868,25 +989,52 @@ const StackArchitect = () => {
}
</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">
{TECH_OPTIONS.orm.find((t) => t.id === stack.orm)?.icon}{" "}
{TECH_OPTIONS.orm.find((t) => t.id === stack.orm)?.name}
</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">
{TECH_OPTIONS.auth.find((t) => t.id === stack.auth)?.icon}{" "}
{TECH_OPTIONS.auth.find((t) => t.id === stack.auth)?.name}
</span>
{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}
{hasWebFrontend(stack.frontend) && (
<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)?.icon}{" "}
{TECH_OPTIONS.auth.find((t) => t.id === stack.auth)?.name}
</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) => {
const addon = TECH_OPTIONS.addons.find(
(a) => a.id === addonId,
@@ -925,13 +1073,13 @@ const StackArchitect = () => {
type="button"
key={category}
className={`
py-2 px-4 text-xs font-mono whitespace-nowrap transition-colors
${
activeTab === category
? "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"
}
`}
py-2 px-4 text-xs font-mono whitespace-nowrap transition-colors
${
activeTab === category
? "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"
}
`}
onClick={() => setActiveTab(category)}
>
{category}

View File

@@ -6,6 +6,8 @@ import { useEffect, useState } from "react";
import { Tweet } from "react-tweet";
const TWEET_IDS = [
"1907728148294447538",
"1907723601731530820",
"1904144343125860404",
"1904215768272654825",
"1904233896851521980",