@nubase/create 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +148 -0
  3. package/package.json +55 -0
  4. package/templates/backend/.env.development.template +4 -0
  5. package/templates/backend/.env.test.template +4 -0
  6. package/templates/backend/db/schema.sql +95 -0
  7. package/templates/backend/docker/dev/docker-compose.yml +21 -0
  8. package/templates/backend/docker/dev/postgresql-init/dump.sql +95 -0
  9. package/templates/backend/docker/test/docker-compose.yml +21 -0
  10. package/templates/backend/docker/test/postgresql-init/dump.sql +95 -0
  11. package/templates/backend/drizzle.config.ts +10 -0
  12. package/templates/backend/package.json +47 -0
  13. package/templates/backend/src/api/routes/auth.ts +209 -0
  14. package/templates/backend/src/api/routes/index.ts +2 -0
  15. package/templates/backend/src/api/routes/ticket.ts +62 -0
  16. package/templates/backend/src/auth/index.ts +37 -0
  17. package/templates/backend/src/db/helpers/drizzle.ts +34 -0
  18. package/templates/backend/src/db/schema/index.ts +4 -0
  19. package/templates/backend/src/db/schema/ticket.ts +13 -0
  20. package/templates/backend/src/db/schema/user-workspace.ts +17 -0
  21. package/templates/backend/src/db/schema/user.ts +10 -0
  22. package/templates/backend/src/db/schema/workspace.ts +9 -0
  23. package/templates/backend/src/db/seed.ts +71 -0
  24. package/templates/backend/src/helpers/env.ts +10 -0
  25. package/templates/backend/src/index.ts +55 -0
  26. package/templates/backend/src/middleware/workspace-middleware.ts +11 -0
  27. package/templates/backend/tsconfig.json +15 -0
  28. package/templates/backend/vitest.config.ts +8 -0
  29. package/templates/frontend/.env.development.template +1 -0
  30. package/templates/frontend/index.html +13 -0
  31. package/templates/frontend/package.json +30 -0
  32. package/templates/frontend/postcss.config.js +5 -0
  33. package/templates/frontend/src/auth/__PROJECT_NAME_PASCAL__AuthController.ts +59 -0
  34. package/templates/frontend/src/config.tsx +76 -0
  35. package/templates/frontend/src/main.tsx +6 -0
  36. package/templates/frontend/src/resources/ticket.ts +32 -0
  37. package/templates/frontend/src/styles/theme.css +3 -0
  38. package/templates/frontend/src/vite-env.d.ts +9 -0
  39. package/templates/frontend/tsconfig.json +17 -0
  40. package/templates/frontend/vite.config.ts +14 -0
  41. package/templates/root/README.md +94 -0
  42. package/templates/root/biome.json.template +40 -0
  43. package/templates/root/package.json +30 -0
  44. package/templates/root/turbo.json +18 -0
  45. package/templates/schema/package.json +28 -0
  46. package/templates/schema/src/api-endpoints.ts +34 -0
  47. package/templates/schema/src/index.ts +3 -0
  48. package/templates/schema/src/schema/auth.ts +89 -0
  49. package/templates/schema/src/schema/ticket.ts +68 -0
  50. package/templates/schema/tsconfig.json +14 -0
@@ -0,0 +1,209 @@
1
+ import bcrypt from "bcrypt";
2
+ import { eq } from "drizzle-orm";
3
+ import type { Context } from "hono";
4
+ import { __PROJECT_NAME_PASCAL__AuthController } from "../../auth";
5
+ import { adminDb, db } from "../../db/helpers/drizzle";
6
+ import { userWorkspaces, users, workspaces } from "../../db/schema";
7
+
8
+ const authController = new __PROJECT_NAME_PASCAL__AuthController();
9
+
10
+ export const authHandlers = {
11
+ async loginStart(c: Context) {
12
+ const { email, password } = await c.req.json();
13
+
14
+ const [user] = await db.select().from(users).where(eq(users.email, email));
15
+
16
+ if (!user) {
17
+ return c.json({ error: "Invalid credentials" }, 401);
18
+ }
19
+
20
+ const isValid = await bcrypt.compare(password, user.passwordHash);
21
+ if (!isValid) {
22
+ return c.json({ error: "Invalid credentials" }, 401);
23
+ }
24
+
25
+ // Get user's workspaces
26
+ const userWs = await db
27
+ .select({
28
+ id: workspaces.id,
29
+ slug: workspaces.slug,
30
+ name: workspaces.name,
31
+ })
32
+ .from(userWorkspaces)
33
+ .innerJoin(workspaces, eq(userWorkspaces.workspaceId, workspaces.id))
34
+ .where(eq(userWorkspaces.userId, user.id));
35
+
36
+ return c.json({ workspaces: userWs });
37
+ },
38
+
39
+ async loginComplete(c: Context) {
40
+ const { email, password, workspaceId } = await c.req.json();
41
+
42
+ const [user] = await db.select().from(users).where(eq(users.email, email));
43
+
44
+ if (!user) {
45
+ return c.json({ error: "Invalid credentials" }, 401);
46
+ }
47
+
48
+ const isValid = await bcrypt.compare(password, user.passwordHash);
49
+ if (!isValid) {
50
+ return c.json({ error: "Invalid credentials" }, 401);
51
+ }
52
+
53
+ const [workspace] = await db
54
+ .select()
55
+ .from(workspaces)
56
+ .where(eq(workspaces.id, workspaceId));
57
+
58
+ if (!workspace) {
59
+ return c.json({ error: "Workspace not found" }, 404);
60
+ }
61
+
62
+ const token = authController.generateToken({
63
+ userId: user.id,
64
+ workspaceId: workspace.id,
65
+ email: user.email,
66
+ });
67
+
68
+ return c.json({
69
+ token,
70
+ user: { id: user.id, email: user.email, username: user.username },
71
+ workspace: { id: workspace.id, slug: workspace.slug, name: workspace.name },
72
+ });
73
+ },
74
+
75
+ async login(c: Context) {
76
+ const { email, password } = await c.req.json();
77
+
78
+ const [user] = await db.select().from(users).where(eq(users.email, email));
79
+
80
+ if (!user) {
81
+ return c.json({ error: "Invalid credentials" }, 401);
82
+ }
83
+
84
+ const isValid = await bcrypt.compare(password, user.passwordHash);
85
+ if (!isValid) {
86
+ return c.json({ error: "Invalid credentials" }, 401);
87
+ }
88
+
89
+ // Get first workspace for simple login
90
+ const [userWs] = await db
91
+ .select({
92
+ id: workspaces.id,
93
+ slug: workspaces.slug,
94
+ name: workspaces.name,
95
+ })
96
+ .from(userWorkspaces)
97
+ .innerJoin(workspaces, eq(userWorkspaces.workspaceId, workspaces.id))
98
+ .where(eq(userWorkspaces.userId, user.id));
99
+
100
+ if (!userWs) {
101
+ return c.json({ error: "No workspace found" }, 404);
102
+ }
103
+
104
+ const token = authController.generateToken({
105
+ userId: user.id,
106
+ workspaceId: userWs.id,
107
+ email: user.email,
108
+ });
109
+
110
+ return c.json({
111
+ token,
112
+ user: { id: user.id, email: user.email, username: user.username },
113
+ workspace: userWs,
114
+ });
115
+ },
116
+
117
+ async logout(c: Context) {
118
+ // Client-side token removal
119
+ return c.json({ success: true });
120
+ },
121
+
122
+ async getMe(c: Context) {
123
+ const authHeader = c.req.header("Authorization");
124
+ if (!authHeader?.startsWith("Bearer ")) {
125
+ return c.json({ error: "Unauthorized" }, 401);
126
+ }
127
+
128
+ const token = authHeader.slice(7);
129
+ const payload = authController.verifyToken(token);
130
+
131
+ if (!payload) {
132
+ return c.json({ error: "Invalid token" }, 401);
133
+ }
134
+
135
+ const [user] = await db
136
+ .select()
137
+ .from(users)
138
+ .where(eq(users.id, payload.userId));
139
+
140
+ if (!user) {
141
+ return c.json({ error: "User not found" }, 404);
142
+ }
143
+
144
+ const [workspace] = await db
145
+ .select()
146
+ .from(workspaces)
147
+ .where(eq(workspaces.id, payload.workspaceId));
148
+
149
+ return c.json({
150
+ user: { id: user.id, email: user.email, username: user.username },
151
+ workspace: workspace
152
+ ? { id: workspace.id, slug: workspace.slug, name: workspace.name }
153
+ : null,
154
+ });
155
+ },
156
+
157
+ async signup(c: Context) {
158
+ const { email, username, password, workspaceName } = await c.req.json();
159
+
160
+ // Check if user exists
161
+ const [existingUser] = await db
162
+ .select()
163
+ .from(users)
164
+ .where(eq(users.email, email));
165
+
166
+ if (existingUser) {
167
+ return c.json({ error: "Email already registered" }, 400);
168
+ }
169
+
170
+ // Create user
171
+ const passwordHash = await bcrypt.hash(password, 10);
172
+ const [user] = await adminDb
173
+ .insert(users)
174
+ .values({ email, username, passwordHash })
175
+ .returning();
176
+
177
+ // Create or get workspace
178
+ const wsSlug = workspaceName?.toLowerCase().replace(/\s+/g, "-") || "default";
179
+ let [workspace] = await db
180
+ .select()
181
+ .from(workspaces)
182
+ .where(eq(workspaces.slug, wsSlug));
183
+
184
+ if (!workspace) {
185
+ [workspace] = await adminDb
186
+ .insert(workspaces)
187
+ .values({ slug: wsSlug, name: workspaceName || "Default Workspace" })
188
+ .returning();
189
+ }
190
+
191
+ // Link user to workspace
192
+ await adminDb.insert(userWorkspaces).values({
193
+ userId: user.id,
194
+ workspaceId: workspace.id,
195
+ });
196
+
197
+ const token = authController.generateToken({
198
+ userId: user.id,
199
+ workspaceId: workspace.id,
200
+ email: user.email,
201
+ });
202
+
203
+ return c.json({
204
+ token,
205
+ user: { id: user.id, email: user.email, username: user.username },
206
+ workspace: { id: workspace.id, slug: workspace.slug, name: workspace.name },
207
+ });
208
+ },
209
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./ticket";
2
+ export * from "./auth";
@@ -0,0 +1,62 @@
1
+ import { eq } from "drizzle-orm";
2
+ import type { Context } from "hono";
3
+ import { db } from "../../db/helpers/drizzle";
4
+ import { tickets } from "../../db/schema";
5
+
6
+ export const ticketHandlers = {
7
+ async getTickets(c: Context) {
8
+ const allTickets = await db.select().from(tickets);
9
+ return c.json(allTickets);
10
+ },
11
+
12
+ async getTicket(c: Context) {
13
+ const id = Number(c.req.param("id"));
14
+ const [ticket] = await db.select().from(tickets).where(eq(tickets.id, id));
15
+
16
+ if (!ticket) {
17
+ return c.json({ error: "Ticket not found" }, 404);
18
+ }
19
+
20
+ return c.json(ticket);
21
+ },
22
+
23
+ async postTicket(c: Context) {
24
+ const body = await c.req.json();
25
+ const [ticket] = await db
26
+ .insert(tickets)
27
+ .values({
28
+ workspaceId: 1, // TODO: Get from context
29
+ title: body.title,
30
+ description: body.description,
31
+ })
32
+ .returning();
33
+
34
+ return c.json(ticket, 201);
35
+ },
36
+
37
+ async patchTicket(c: Context) {
38
+ const id = Number(c.req.param("id"));
39
+ const body = await c.req.json();
40
+
41
+ const [ticket] = await db
42
+ .update(tickets)
43
+ .set({
44
+ ...body,
45
+ updatedAt: new Date(),
46
+ })
47
+ .where(eq(tickets.id, id))
48
+ .returning();
49
+
50
+ if (!ticket) {
51
+ return c.json({ error: "Ticket not found" }, 404);
52
+ }
53
+
54
+ return c.json(ticket);
55
+ },
56
+
57
+ async deleteTicket(c: Context) {
58
+ const id = Number(c.req.param("id"));
59
+ await db.delete(tickets).where(eq(tickets.id, id));
60
+ return c.json({ success: true });
61
+ },
62
+ };
@@ -0,0 +1,37 @@
1
+ import bcrypt from "bcrypt";
2
+ import jwt from "jsonwebtoken";
3
+ import type { User, Workspace } from "__PROJECT_NAME__-schema";
4
+
5
+ export interface __PROJECT_NAME_PASCAL__User extends User {
6
+ workspace: Workspace;
7
+ }
8
+
9
+ export interface TokenPayload {
10
+ userId: number;
11
+ workspaceId: number;
12
+ email: string;
13
+ }
14
+
15
+ const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
16
+
17
+ export class __PROJECT_NAME_PASCAL__AuthController {
18
+ async hashPassword(password: string): Promise<string> {
19
+ return bcrypt.hash(password, 10);
20
+ }
21
+
22
+ async verifyPassword(password: string, hash: string): Promise<boolean> {
23
+ return bcrypt.compare(password, hash);
24
+ }
25
+
26
+ generateToken(payload: TokenPayload): string {
27
+ return jwt.sign(payload, JWT_SECRET, { expiresIn: "7d" });
28
+ }
29
+
30
+ verifyToken(token: string): TokenPayload | null {
31
+ try {
32
+ return jwt.verify(token, JWT_SECRET) as TokenPayload;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,34 @@
1
+ import { drizzle } from "drizzle-orm/node-postgres";
2
+ import pg from "pg";
3
+ import * as schema from "../schema";
4
+
5
+ const { Pool } = pg;
6
+
7
+ // Main app pool (uses RLS-restricted user)
8
+ const appPool = new Pool({
9
+ connectionString: process.env.DATABASE_URL,
10
+ });
11
+
12
+ // Admin pool (bypasses RLS for setup operations)
13
+ const adminPool = new Pool({
14
+ connectionString: process.env.DATABASE_URL_ADMIN,
15
+ });
16
+
17
+ export const db = drizzle(appPool, { schema });
18
+ export const adminDb = drizzle(adminPool, { schema });
19
+
20
+ // Set workspace context for RLS
21
+ export async function withWorkspaceContext<T>(
22
+ workspaceId: number,
23
+ fn: () => Promise<T>,
24
+ ): Promise<T> {
25
+ const client = await appPool.connect();
26
+ try {
27
+ await client.query(`SET app.current_workspace_id = '${workspaceId}'`);
28
+ const result = await fn();
29
+ return result;
30
+ } finally {
31
+ await client.query("RESET app.current_workspace_id");
32
+ client.release();
33
+ }
34
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./workspace";
2
+ export * from "./user";
3
+ export * from "./user-workspace";
4
+ export * from "./ticket";
@@ -0,0 +1,13 @@
1
+ import { integer, pgTable, serial, text, timestamp, varchar } from "drizzle-orm/pg-core";
2
+ import { workspaces } from "./workspace";
3
+
4
+ export const tickets = pgTable("tickets", {
5
+ id: serial("id").primaryKey(),
6
+ workspaceId: integer("workspace_id")
7
+ .notNull()
8
+ .references(() => workspaces.id, { onDelete: "cascade" }),
9
+ title: varchar("title", { length: 255 }).notNull(),
10
+ description: text("description"),
11
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
12
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
13
+ });
@@ -0,0 +1,17 @@
1
+ import { integer, pgTable, primaryKey, timestamp } from "drizzle-orm/pg-core";
2
+ import { users } from "./user";
3
+ import { workspaces } from "./workspace";
4
+
5
+ export const userWorkspaces = pgTable(
6
+ "user_workspaces",
7
+ {
8
+ userId: integer("user_id")
9
+ .notNull()
10
+ .references(() => users.id, { onDelete: "cascade" }),
11
+ workspaceId: integer("workspace_id")
12
+ .notNull()
13
+ .references(() => workspaces.id, { onDelete: "cascade" }),
14
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
15
+ },
16
+ (table) => [primaryKey({ columns: [table.userId, table.workspaceId] })],
17
+ );
@@ -0,0 +1,10 @@
1
+ import { pgTable, serial, timestamp, varchar } from "drizzle-orm/pg-core";
2
+
3
+ export const users = pgTable("users", {
4
+ id: serial("id").primaryKey(),
5
+ email: varchar("email", { length: 255 }).unique().notNull(),
6
+ username: varchar("username", { length: 100 }).unique().notNull(),
7
+ passwordHash: varchar("password_hash", { length: 255 }).notNull(),
8
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
9
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
10
+ });
@@ -0,0 +1,9 @@
1
+ import { pgTable, serial, timestamp, varchar } from "drizzle-orm/pg-core";
2
+
3
+ export const workspaces = pgTable("workspaces", {
4
+ id: serial("id").primaryKey(),
5
+ slug: varchar("slug", { length: 100 }).unique().notNull(),
6
+ name: varchar("name", { length: 255 }).notNull(),
7
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
8
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
9
+ });
@@ -0,0 +1,71 @@
1
+ import { faker } from "@faker-js/faker";
2
+ import bcrypt from "bcrypt";
3
+ import { loadEnv } from "../helpers/env";
4
+ import { adminDb } from "./helpers/drizzle";
5
+ import { tickets, userWorkspaces, users, workspaces } from "./schema";
6
+
7
+ loadEnv();
8
+
9
+ async function seed() {
10
+ console.log("Seeding database...");
11
+
12
+ // Create default workspace
13
+ const [workspace] = await adminDb
14
+ .insert(workspaces)
15
+ .values({
16
+ slug: "default",
17
+ name: "Default Workspace",
18
+ })
19
+ .onConflictDoNothing()
20
+ .returning();
21
+
22
+ const workspaceId = workspace?.id ?? 1;
23
+ console.log(`Created/found workspace: ${workspaceId}`);
24
+
25
+ // Create test user
26
+ const passwordHash = await bcrypt.hash("password123", 10);
27
+ const [user] = await adminDb
28
+ .insert(users)
29
+ .values({
30
+ email: "demo@example.com",
31
+ username: "demo",
32
+ passwordHash,
33
+ })
34
+ .onConflictDoNothing()
35
+ .returning();
36
+
37
+ if (user) {
38
+ console.log(`Created user: ${user.email}`);
39
+
40
+ // Link user to workspace
41
+ await adminDb
42
+ .insert(userWorkspaces)
43
+ .values({
44
+ userId: user.id,
45
+ workspaceId,
46
+ })
47
+ .onConflictDoNothing();
48
+ }
49
+
50
+ // Create sample tickets
51
+ const ticketData = Array.from({ length: 10 }, () => ({
52
+ workspaceId,
53
+ title: faker.lorem.sentence(),
54
+ description: faker.lorem.paragraph(),
55
+ }));
56
+
57
+ await adminDb.insert(tickets).values(ticketData).onConflictDoNothing();
58
+ console.log(`Created ${ticketData.length} sample tickets`);
59
+
60
+ console.log("Database seeded successfully!");
61
+ console.log("\nTest credentials:");
62
+ console.log(" Email: demo@example.com");
63
+ console.log(" Password: password123");
64
+
65
+ process.exit(0);
66
+ }
67
+
68
+ seed().catch((error) => {
69
+ console.error("Seed failed:", error);
70
+ process.exit(1);
71
+ });
@@ -0,0 +1,10 @@
1
+ import dotenv from "dotenv";
2
+ import path from "node:path";
3
+
4
+ export function loadEnv(): void {
5
+ const nodeEnv = process.env.NODE_ENV || "development";
6
+ const envFile = `.env.${nodeEnv}`;
7
+
8
+ dotenv.config({ path: path.resolve(process.cwd(), envFile) });
9
+ dotenv.config({ path: path.resolve(process.cwd(), ".env") });
10
+ }
@@ -0,0 +1,55 @@
1
+ import { serve } from "@hono/node-server";
2
+ import { Hono } from "hono";
3
+ import { cors } from "hono/cors";
4
+ import { ticketHandlers } from "./api/routes/ticket";
5
+ import { authHandlers } from "./api/routes/auth";
6
+ import { workspaceMiddleware } from "./middleware/workspace-middleware";
7
+ import { __PROJECT_NAME_PASCAL__AuthController } from "./auth";
8
+ import { loadEnv } from "./helpers/env";
9
+
10
+ // Load environment variables
11
+ loadEnv();
12
+
13
+ const app = new Hono();
14
+
15
+ // CORS configuration
16
+ app.use(
17
+ "*",
18
+ cors({
19
+ origin: ["http://localhost:__FRONTEND_PORT__"],
20
+ credentials: true,
21
+ }),
22
+ );
23
+
24
+ // Workspace middleware (extracts workspace from path)
25
+ app.use("/:workspace/*", workspaceMiddleware);
26
+
27
+ // Auth controller
28
+ const authController = new __PROJECT_NAME_PASCAL__AuthController();
29
+
30
+ // Routes
31
+ app.get("/:workspace", (c) => c.json({ message: "Welcome to __PROJECT_NAME_PASCAL__ API" }));
32
+
33
+ // Auth routes
34
+ app.post("/:workspace/auth/login/start", authHandlers.loginStart);
35
+ app.post("/:workspace/auth/login/complete", authHandlers.loginComplete);
36
+ app.post("/:workspace/auth/login", authHandlers.login);
37
+ app.post("/:workspace/auth/logout", authHandlers.logout);
38
+ app.get("/:workspace/auth/me", authHandlers.getMe);
39
+ app.post("/:workspace/auth/signup", authHandlers.signup);
40
+
41
+ // Ticket routes
42
+ app.get("/:workspace/tickets", ticketHandlers.getTickets);
43
+ app.get("/:workspace/tickets/:id", ticketHandlers.getTicket);
44
+ app.post("/:workspace/tickets", ticketHandlers.postTicket);
45
+ app.patch("/:workspace/tickets/:id", ticketHandlers.patchTicket);
46
+ app.delete("/:workspace/tickets/:id", ticketHandlers.deleteTicket);
47
+
48
+ const port = Number(process.env.PORT) || __BACKEND_PORT__;
49
+
50
+ console.log(`Server is running on http://localhost:${port}`);
51
+
52
+ serve({
53
+ fetch: app.fetch,
54
+ port,
55
+ });
@@ -0,0 +1,11 @@
1
+ import type { Context, Next } from "hono";
2
+
3
+ export async function workspaceMiddleware(c: Context, next: Next) {
4
+ const workspace = c.req.param("workspace");
5
+
6
+ if (workspace) {
7
+ c.set("workspace", workspace);
8
+ }
9
+
10
+ await next();
11
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "declaration": true,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src"
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules", "dist"]
15
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: "node",
7
+ },
8
+ });
@@ -0,0 +1 @@
1
+ VITE_API_BASE_URL=http://localhost:__BACKEND_PORT__
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>__PROJECT_NAME_PASCAL__</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "__PROJECT_NAME__-frontend",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "preview": "vite preview",
10
+ "typecheck": "tsc --noEmit",
11
+ "lint": "biome check .",
12
+ "lint:fix": "biome check . --write --unsafe"
13
+ },
14
+ "dependencies": {
15
+ "@nubase/frontend": "*",
16
+ "@tailwindcss/vite": "^4.1.18",
17
+ "@tanstack/react-router": "^1.141.8",
18
+ "react": "^19.0.0",
19
+ "react-dom": "^19.0.0",
20
+ "tailwindcss": "^4.1.18",
21
+ "__PROJECT_NAME__-schema": "*"
22
+ },
23
+ "devDependencies": {
24
+ "@types/react": "^19.0.10",
25
+ "@types/react-dom": "^19.0.4",
26
+ "@vitejs/plugin-react": "^5.1.2",
27
+ "typescript": "^5.9.3",
28
+ "vite": "^7.3.0"
29
+ }
30
+ }
@@ -0,0 +1,5 @@
1
+ export default {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };