├── .github └── FUNDING.yml ├── .gitignore ├── .npmrc ├── README.md ├── apps ├── api │ ├── .dev.vars.example │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── client.ts │ │ ├── durables │ │ │ └── NotificationWebsocketServer.ts │ │ ├── index.ts │ │ ├── lib │ │ │ ├── auth.ts │ │ │ ├── auth │ │ │ │ └── hooks │ │ │ │ │ └── emails.tsx │ │ │ └── database.ts │ │ ├── middleware │ │ │ └── allowBrowser.ts │ │ ├── routes │ │ │ ├── notifications.ts │ │ │ └── transcript.ts │ │ ├── transcriptionConsumer.ts │ │ └── types │ │ │ └── AppEnv.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── wrangler.jsonc └── web │ ├── .env.example │ ├── .gitignore │ ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── settings.json │ ├── README.md │ ├── astro.config.mjs │ ├── components.json │ ├── package.json │ ├── public │ ├── .assetsignore │ └── favicon.svg │ ├── src │ ├── assets │ │ ├── astro.svg │ │ └── background.svg │ ├── components │ │ ├── AppHeader.tsx │ │ ├── DashboardHeader.tsx │ │ ├── LayoutDashboard.tsx │ │ ├── NotificationBell.tsx │ │ ├── PagesDashboard.tsx │ │ ├── PagesDashboardFileUpload.tsx │ │ ├── PagesDashboardTranscript.tsx │ │ ├── PagesDashboardTranscripts.tsx │ │ ├── PagesHome.tsx │ │ ├── PagesLogin.tsx │ │ ├── PagesRegister.tsx │ │ ├── PagesResetPassword.tsx │ │ ├── PagesSettingsUser.tsx │ │ ├── PagesVerifyEmail.tsx │ │ └── PagesVerifyPhone.tsx │ ├── env.d.ts │ ├── layouts │ │ ├── Layout.astro │ │ ├── LayoutBase.astro │ │ └── LayoutDashboard.astro │ ├── lib │ │ ├── apiClient.ts │ │ └── authClient.ts │ ├── middleware.ts │ ├── pages │ │ ├── dashboard │ │ │ ├── file-upload.astro │ │ │ ├── index.astro │ │ │ ├── record.astro │ │ │ └── transcripts │ │ │ │ ├── [id].astro │ │ │ │ └── index.astro │ │ ├── forgot-password.astro │ │ ├── index.astro │ │ ├── login.astro │ │ ├── register.astro │ │ ├── reset-password.astro │ │ ├── settings │ │ │ ├── billing │ │ │ │ └── index.astro │ │ │ ├── security.astro │ │ │ ├── sessions.astro │ │ │ └── user.astro │ │ └── verify-email.astro │ └── utils │ │ └── deriveUserData.ts │ ├── tsconfig.json │ ├── worker-configuration.d.ts │ └── wrangler.jsonc ├── package.json ├── packages ├── constants │ ├── package.json │ ├── src │ │ ├── appInfo.ts │ │ ├── index.ts │ │ └── tailwindConfig.ts │ └── tsconfig.json ├── database │ ├── .gitignore │ ├── db-local.sh │ ├── drizzle.config.ts │ ├── migrations │ │ ├── 0000_daffy_ego.sql │ │ ├── 0001_watery_satana.sql │ │ ├── 0002_dusty_mephistopheles.sql │ │ └── meta │ │ │ ├── 0000_snapshot.json │ │ │ ├── 0001_snapshot.json │ │ │ ├── 0002_snapshot.json │ │ │ └── _journal.json │ ├── package.json │ ├── src │ │ ├── base.json │ │ ├── index.ts │ │ └── schema │ │ │ ├── auth.ts │ │ │ ├── index.ts │ │ │ └── main.ts │ └── tsconfig.json ├── email │ ├── emails │ │ ├── ResetPassword.tsx │ │ └── VerificationEmail.tsx │ ├── package.json │ ├── readme.md │ └── tsconfig.json ├── typescript-config │ ├── README.md │ ├── base.json │ ├── package.json │ └── react-library.json └── ui │ ├── components.json │ ├── package.json │ ├── src │ ├── components │ │ ├── .gitkeep │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ └── tooltip.tsx │ ├── hooks │ │ ├── .gitkeep │ │ └── use-mobile.ts │ ├── lib │ │ └── utils.ts │ └── styles │ │ └── globals.css │ └── tsconfig.json ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: theneocorner 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # Local env files 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Testing 16 | coverage 17 | 18 | # Turbo 19 | .turbo 20 | 21 | # Vercel 22 | .vercel 23 | 24 | # Build Outputs 25 | .next/ 26 | out/ 27 | build 28 | dist 29 | 30 | 31 | # Debug 32 | npm-debug.log* 33 | 34 | # Misc 35 | .DS_Store 36 | *.pem 37 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeoPrint3D/neostack/ce5ead93abea7ac0f90193eaabd4252cbc6f244c/.npmrc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NeoStack Template 2 | 3 | 4 | If you like the template feel free to donate at https://ko-fi.com/theneocorner 5 | -------------------------------------------------------------------------------- /apps/api/.dev.vars.example: -------------------------------------------------------------------------------- 1 | BETTER_AUTH_SECRET="BETTER_AUTH_SECRET" 2 | BETTER_AUTH_URL="http://localhost:8080" 3 | 4 | TRUSTED_ORIGINS="http://localhost:4321" 5 | DATABASE_URL="postgresql://postgres:mysecretpassword@localhost:5432/postgres" 6 | 7 | GOOGLE_CLIENT_ID="GOOGLE_CLIENT_ID" 8 | GOOGLE_CLIENT_SECRET="GOOGLE_CLIENT_SECRET" 9 | 10 | STRIPE_SECRET_KEY="STRIPE_SECRET_KEY" 11 | STRIPE_WEBHOOK_SECRET="STRIPE_WEBHOOK_SECRET" -------------------------------------------------------------------------------- /apps/api/.gitignore: -------------------------------------------------------------------------------- 1 | # prod 2 | dist/ 3 | 4 | # dev 5 | .yarn/ 6 | !.yarn/releases 7 | .vscode/* 8 | !.vscode/launch.json 9 | !.vscode/*.code-snippets 10 | .idea/workspace.xml 11 | .idea/usage.statistics.xml 12 | .idea/shelf 13 | 14 | # deps 15 | node_modules/ 16 | .wrangler 17 | 18 | # env 19 | .env 20 | .env.production 21 | .dev.vars 22 | 23 | # logs 24 | logs/ 25 | *.log 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | pnpm-debug.log* 30 | lerna-debug.log* 31 | 32 | # misc 33 | .DS_Store 34 | 35 | .dev.vars.* 36 | !.dev.vars.example -------------------------------------------------------------------------------- /apps/api/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | npm install 3 | npm run dev 4 | ``` 5 | 6 | ``` 7 | npm run deploy 8 | ``` 9 | -------------------------------------------------------------------------------- /apps/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neostack/api", 3 | "exports": { 4 | "./client": { 5 | "import": "./dist/client.js", 6 | "types": "./dist/client.d.ts" 7 | } 8 | }, 9 | "scripts": { 10 | "dev": "pnpm --filter @neostack/api --parallel \"/^local:.*/\"", 11 | "local:tsc": "tsc -p tsconfig.build.json --watch", 12 | "local:dev": "wrangler dev --test-scheduled --env local", 13 | "deploy": "wrangler deploy --minify --env prod", 14 | "auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth.ts --output ../../packages/database/src/schema/auth.ts" 15 | }, 16 | "dependencies": { 17 | "@better-auth/stripe": "^1.2.7", 18 | "@hono/zod-validator": "^0.4.3", 19 | "@neostack/constants": "workspace:*", 20 | "@neostack/database": "workspace:*", 21 | "@neostack/email": "workspace:*", 22 | "better-auth": "^1.2.7", 23 | "hono": "^4.7.6", 24 | "nanoid": "^5.1.5", 25 | "react": "^19.1.0", 26 | "resend": "^4.2.0", 27 | "stripe": "^18.0.0", 28 | "zod": "^3.24.2" 29 | }, 30 | "devDependencies": { 31 | "@cloudflare/workers-types": "^4.20250415.0", 32 | "@types/react": "^19.1.2", 33 | "typescript": "^5.8.3", 34 | "wrangler": "^4.11.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /apps/api/src/client.ts: -------------------------------------------------------------------------------- 1 | import { hc } from "hono/client"; 2 | import { app } from "."; 3 | import { createAuthClient } from "./lib/auth"; 4 | import { z } from "zod"; 5 | 6 | const client = hc(""); 7 | 8 | export type ApiClient = typeof client; 9 | 10 | export const api = (...args: Parameters): ApiClient => 11 | hc(...args); 12 | 13 | export type BetterAuthInstance = Awaited>; 14 | export { TranscriptionData } from "@/types/AppEnv"; 15 | export type Notification = z.infer; 16 | 17 | export const notificationSchema = z.object({ 18 | id: z.string(), 19 | type: z.enum(["queueStatus", "invitation"]), 20 | timestamp: z.number(), 21 | redirectPath: z.string().optional(), 22 | title: z.string(), 23 | content: z.string(), 24 | }); 25 | -------------------------------------------------------------------------------- /apps/api/src/durables/NotificationWebsocketServer.ts: -------------------------------------------------------------------------------- 1 | import { Notification, notificationSchema } from "@/client"; 2 | import { AppEnv } from "@/types/AppEnv"; 3 | import { zValidator } from "@hono/zod-validator"; 4 | import { DurableObject } from "cloudflare:workers"; 5 | import { Hono } from "hono"; 6 | 7 | export class NotificationWebsocketServer extends DurableObject { 8 | private app: Hono = new Hono(); 9 | private notifications: Map = new Map(); 10 | 11 | constructor(state: DurableObjectState, env: AppEnv) { 12 | super(state, env); 13 | 14 | // Initialize routes 15 | this.app.post("/create", async (c) => { 16 | try { 17 | const message = await c.req.json(); 18 | 19 | await this.ctx.storage.put(`message:${message.id}`, message); 20 | this.notifications.set(message.id, message); 21 | for (const ws of this.ctx.getWebSockets()) { 22 | ws.send(JSON.stringify(message)); 23 | } 24 | return c.json({ success: true }, 200); 25 | } catch (error) { 26 | console.error("Error creating notification:", error); 27 | return c.json({ error: "Failed to create notification" }, 500); 28 | } 29 | }); 30 | 31 | this.app.post("/clear", async (c) => { 32 | try { 33 | // Clear all notifications from storage and in-memory map 34 | await this.ctx.storage.deleteAll(); 35 | this.notifications.clear(); 36 | for (const ws of this.ctx.getWebSockets()) { 37 | ws.send( 38 | JSON.stringify({ 39 | type: "clear", 40 | }) 41 | ); 42 | } 43 | return c.json({ success: true }, 200); 44 | } catch (error) { 45 | console.error("Error clearing notifications:", error); 46 | return c.json({ error: "Failed to clear notifications" }, 500); 47 | } 48 | }); 49 | 50 | this.app.post("/clear/:id", async (c) => { 51 | const { id } = c.req.param(); 52 | try { 53 | // Clear all notifications from storage and in-memory map 54 | await this.ctx.storage.delete(id); 55 | this.notifications.delete(id); 56 | for (const ws of this.ctx.getWebSockets()) { 57 | ws.send( 58 | JSON.stringify({ 59 | type: "clear-id", 60 | payload: id, 61 | }) 62 | ); 63 | } 64 | return c.json({ success: true }, 200); 65 | } catch (error) { 66 | console.error("Error clearing notifications:", error); 67 | return c.json({ error: "Failed to clear notifications" }, 500); 68 | } 69 | }); 70 | 71 | // Load existing notifications 72 | this.ctx.blockConcurrencyWhile(async () => { 73 | try { 74 | const storedNotifications = await this.ctx.storage.list({ 75 | prefix: "message:", 76 | }); 77 | 78 | for (const [key, value] of storedNotifications) { 79 | const notificationId = key.replace("message:", ""); 80 | this.notifications.set(notificationId, value); 81 | } 82 | } catch (error) { 83 | console.error("Error loading stored notifications:", error); 84 | } 85 | }); 86 | } 87 | 88 | async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) { 89 | try { 90 | const msg = 91 | typeof message === "string" 92 | ? message 93 | : new TextDecoder().decode(message); 94 | 95 | if (msg.toLowerCase() === "ping") { 96 | ws.send("pong"); 97 | return; 98 | } 99 | 100 | const data = JSON.parse(msg); 101 | ws.send(JSON.stringify({ type: "received", data })); 102 | } catch (error) { 103 | console.error("WebSocket message error:", error); 104 | ws.send(JSON.stringify({ error: "Invalid message" })); 105 | } 106 | } 107 | 108 | async webSocketClose( 109 | ws: WebSocket, 110 | code: number, 111 | reason: string, 112 | wasClean: boolean 113 | ) { 114 | try { 115 | ws.close(code, "Durable Object is closing WebSocket"); 116 | } catch (error) { 117 | console.error("Error closing WebSocket:", error); 118 | } 119 | } 120 | 121 | async webSocketError(ws: WebSocket, error: Error) { 122 | console.error("WebSocket error:", error); 123 | try { 124 | ws.close(1001, "WebSocket error occurred"); 125 | } catch (error) { 126 | console.error("Error closing WebSocket:", error); 127 | } 128 | } 129 | 130 | async fetch(request: Request) { 131 | try { 132 | if (request.headers.get("Upgrade") === "websocket") { 133 | const pair = new WebSocketPair(); 134 | const [client, server] = Object.values(pair); 135 | 136 | this.ctx.acceptWebSocket(server); 137 | 138 | const initialMessage = { 139 | type: "initialState", 140 | payload: Array.from(this.notifications.values()).map( 141 | (notification) => notification 142 | ), 143 | }; 144 | server.send(JSON.stringify(initialMessage)); 145 | 146 | return new Response(null, { 147 | status: 101, 148 | webSocket: client, 149 | }); 150 | } 151 | return await this.app.fetch(request); 152 | } catch (error) { 153 | console.error("Fetch error:", error); 154 | return new Response("Internal Server Error", { status: 500 }); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /apps/api/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { createAuthClient } from "@/lib/auth"; 3 | import { appInfo } from "@neostack/constants"; 4 | import { AppContext, AppEnv, TranscriptionData } from "@/types/AppEnv"; // Assuming types are correctly placed 5 | import { allowBrowser } from "./middleware/allowBrowser"; 6 | import { transcriptionRouter } from "./routes/transcript"; // Assume this route handles enqueueing 7 | import { notificationRouter } from "./routes/notifications"; 8 | import { transcriptionConsumer } from "./transcriptionConsumer"; 9 | 10 | export const app = new Hono() 11 | .get("/", (c) => c.redirect("/v1")) 12 | .basePath("/v1") 13 | .get("/", (c) => c.json({ message: `Welcome to ${appInfo.name}` })) 14 | .use("*", async (c, next) => { 15 | try { 16 | const session = await createAuthClient(c.env).api.getSession({ 17 | headers: c.req.raw.headers, 18 | }); 19 | c.set("user", session?.user ?? null); 20 | c.set("session", session?.session ?? null); 21 | } catch (error) { 22 | console.error("Auth middleware error:", error); 23 | c.set("user", null); 24 | c.set("session", null); 25 | } 26 | await next(); 27 | }) 28 | .route("/", transcriptionRouter) 29 | .route("/", notificationRouter); 30 | 31 | app.use("/auth/*", allowBrowser); 32 | app.on(["POST", "GET"], "/auth/*", (c) => { 33 | return createAuthClient(c.env).handler(c.req.raw); 34 | }); 35 | 36 | // Default Export 37 | export default { 38 | fetch: app.fetch, 39 | // @ts-ignore 40 | async queue( 41 | batch: MessageBatch, 42 | env: AppEnv, 43 | ctx: ExecutionContext 44 | ): Promise { 45 | await Promise.all( 46 | batch.messages.map((msg) => transcriptionConsumer(env, msg)) 47 | ); 48 | }, 49 | } satisfies ExportedHandler; 50 | 51 | export { NotificationWebsocketServer } from "@/durables/NotificationWebsocketServer"; 52 | -------------------------------------------------------------------------------- /apps/api/src/lib/auth.ts: -------------------------------------------------------------------------------- 1 | // auth.ts 2 | import { betterAuth } from "better-auth"; 3 | import { drizzleAdapter } from "better-auth/adapters/drizzle"; 4 | import { stripe } from "@better-auth/stripe"; 5 | import { 6 | oneTimeToken, 7 | admin, 8 | haveIBeenPwned, 9 | openAPI, 10 | organization, 11 | } from "better-auth/plugins"; 12 | import Stripe from "stripe"; 13 | import { schema } from "@neostack/database"; 14 | import { drizzle } from "@neostack/database"; 15 | import { 16 | sendResetPasswordEmail, 17 | sendVerificationEmail, 18 | } from "./auth/hooks/emails"; 19 | import { AppEnv } from "@/types/AppEnv"; 20 | 21 | export const createAuthClient = ( 22 | env: AppEnv 23 | ): ReturnType => { 24 | return betterAuth({ 25 | appName: "NeoStack", 26 | baseURL: `${env.BETTER_AUTH_URL}/v1/auth`, 27 | basePath: "/v1/auth", 28 | trustedOrigins: env.TRUSTED_ORIGINS.split(","), 29 | 30 | advanced: { 31 | cookiePrefix: "neostack", 32 | crossSubDomainCookies: { 33 | enabled: true, 34 | domain: `.${env.BETTER_AUTH_COOKIE_DOMAIN}`, 35 | }, 36 | }, 37 | 38 | database: drizzleAdapter(drizzle(env.HYPERDRIVE.connectionString), { 39 | provider: "pg", 40 | usePlural: true, 41 | schema, 42 | }), 43 | 44 | emailAndPassword: { 45 | enabled: true, 46 | requireEmailVerification: true, 47 | sendResetPassword: async ({ user, url, token }, request) => { 48 | await sendResetPasswordEmail(env, user, url); 49 | }, 50 | }, 51 | emailVerification: { 52 | autoSignInAfterVerification: true, 53 | sendVerificationEmail: async ({ user, url }) => { 54 | await sendVerificationEmail(env, user, url); 55 | }, 56 | }, 57 | socialProviders: { 58 | google: { 59 | clientId: env.GOOGLE_CLIENT_ID, 60 | clientSecret: env.GOOGLE_CLIENT_SECRET, 61 | }, 62 | }, 63 | plugins: [ 64 | stripe({ 65 | stripeClient: new Stripe(env.STRIPE_SECRET_KEY), 66 | stripeWebhookSecret: env.STRIPE_WEBHOOK_SECRET, 67 | createCustomerOnSignUp: true, 68 | onCustomerCreate: async ( 69 | { customer, stripeCustomer, user }, 70 | request 71 | ) => { 72 | console.log(`Customer ${customer.id} created for user ${user.id}`); 73 | }, 74 | subscription: { 75 | enabled: true, 76 | plans: [], 77 | }, 78 | }), 79 | openAPI(), 80 | haveIBeenPwned(), 81 | admin(), 82 | organization(), 83 | ], 84 | }); 85 | }; 86 | 87 | export const auth: any = betterAuth({ 88 | database: drizzleAdapter( 89 | drizzle("postgres://john:doe@localhost:5432/postgres"), 90 | { 91 | provider: "pg", 92 | usePlural: true, 93 | schema, 94 | } 95 | ), 96 | emailAndPassword: { 97 | enabled: true, 98 | }, 99 | 100 | plugins: [ 101 | stripe({ 102 | stripeClient: new Stripe("dummy-key"), 103 | stripeWebhookSecret: "dummy-key", 104 | createCustomerOnSignUp: true, 105 | 106 | subscription: { 107 | enabled: true, 108 | plans: [], 109 | }, 110 | }), 111 | openAPI(), 112 | haveIBeenPwned(), 113 | oneTimeToken(), 114 | admin(), 115 | organization(), 116 | ], 117 | }); 118 | -------------------------------------------------------------------------------- /apps/api/src/lib/auth/hooks/emails.tsx: -------------------------------------------------------------------------------- 1 | import { AppEnv } from "@/types/AppEnv"; 2 | import { appInfo } from "@neostack/constants"; 3 | import ResetPasswordEmail from "@neostack/email/ResetPassword"; 4 | import VerificationEmail from "@neostack/email/VerificationEmail"; 5 | import { Resend } from "resend"; 6 | import { BetterAuthInstance } from "../../../client"; 7 | 8 | export const sendVerificationEmail = async ( 9 | env: AppEnv, 10 | user: BetterAuthInstance["$Infer"]["Session"]["user"], 11 | url: string 12 | ) => { 13 | const resend = new Resend(env.RESEND_API_KEY); 14 | 15 | await resend.emails.send({ 16 | from: appInfo.email, 17 | to: user.email, 18 | subject: "Verification Email", 19 | text: "test", 20 | react: , 21 | }); 22 | }; 23 | export const sendResetPasswordEmail = async ( 24 | env: AppEnv, 25 | user: BetterAuthInstance["$Infer"]["Session"]["user"], 26 | url: string 27 | ) => { 28 | const resend = new Resend(env.RESEND_API_KEY); 29 | 30 | await resend.emails.send({ 31 | from: appInfo.email, 32 | to: user.email, 33 | subject: "Reset Password", 34 | text: "test", 35 | react: , 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /apps/api/src/lib/database.ts: -------------------------------------------------------------------------------- 1 | import { AppEnv } from "@/types/AppEnv"; 2 | import { drizzle, PostgresJsDatabase } from "@neostack/database"; 3 | 4 | export const db = (env: AppEnv): PostgresJsDatabase => 5 | drizzle(env.HYPERDRIVE.connectionString); 6 | -------------------------------------------------------------------------------- /apps/api/src/middleware/allowBrowser.ts: -------------------------------------------------------------------------------- 1 | import { AppContext } from "@/types/AppEnv"; 2 | import { Context, Next } from "hono"; 3 | import { env } from "hono/adapter"; 4 | import { cors } from "hono/cors"; 5 | 6 | export async function allowBrowser(c: Context, next: Next) { 7 | if (c.req.header("Upgrade") === "websocket") { 8 | return await next(); 9 | } 10 | const corsMiddleware = cors({ 11 | origin: c.env.TRUSTED_ORIGINS.split(","), 12 | allowHeaders: ["Content-Type", "Authorization", "Cookie"], 13 | allowMethods: ["POST", "GET", "PUT", "DELETE", "OPTIONS"], 14 | exposeHeaders: ["Content-Length"], 15 | maxAge: 600, 16 | credentials: true, 17 | }); 18 | return await corsMiddleware(c, next); 19 | } 20 | -------------------------------------------------------------------------------- /apps/api/src/routes/notifications.ts: -------------------------------------------------------------------------------- 1 | import { allowBrowser } from "@/middleware/allowBrowser"; 2 | import { AppContext } from "@/types/AppEnv"; 3 | import { Hono } from "hono"; 4 | 5 | export const notificationRouter = new Hono() 6 | .basePath("/notifications") 7 | .post("/clear/:id", allowBrowser, async (c) => { 8 | const user = c.get("user"); 9 | if (!user?.id) { 10 | return c.json({ message: "Unauthorized" }, 401); 11 | } 12 | const notificationId = c.req.param("id"); 13 | if (!notificationId) { 14 | return c.json({ message: "Notification ID is required" }, 400); 15 | } 16 | 17 | const id = c.env.NOTIFICATION_WEBSOCKET_SERVER.idFromName(user.id); 18 | const pollerStub = c.env.NOTIFICATION_WEBSOCKET_SERVER.get(id); 19 | const response = await pollerStub.fetch( 20 | `http://do/clear/${notificationId}`, 21 | { 22 | method: "POST", 23 | } 24 | ); 25 | 26 | if (!response.ok) { 27 | return c.json({ message: "Failed to clear notification" }, 500); 28 | } 29 | 30 | return c.json({ message: "Notification cleared" }); 31 | }) 32 | 33 | .post("/clear", allowBrowser, async (c) => { 34 | const user = c.get("user"); 35 | if (!user?.id) { 36 | return c.json({ message: "Unauthorized" }, 401); 37 | } 38 | 39 | const id = c.env.NOTIFICATION_WEBSOCKET_SERVER.idFromName(user.id); 40 | const pollerStub = c.env.NOTIFICATION_WEBSOCKET_SERVER.get(id); 41 | const response = await pollerStub.fetch("http://do/clear", { 42 | method: "POST", 43 | }); 44 | 45 | if (!response.ok) { 46 | return c.json({ message: "Failed to clear all notifications" }, 500); 47 | } 48 | 49 | return c.json({ message: "All notifications cleared" }); 50 | }) 51 | .get("/ws", async (c) => { 52 | const user = c.get("user"); 53 | if (!user?.id) { 54 | return c.json({ message: "Unauthorized" }, 401); 55 | } 56 | const upgradeHeader = c.req.header("Upgrade"); 57 | if (upgradeHeader?.toLowerCase() !== "websocket") { 58 | return new Response("Expected WebSocket upgrade", { status: 426 }); 59 | } 60 | const id = c.env.NOTIFICATION_WEBSOCKET_SERVER.idFromName(user.id); 61 | const pollerStub = c.env.NOTIFICATION_WEBSOCKET_SERVER.get(id); 62 | return pollerStub.fetch(c.req.raw); 63 | }); 64 | -------------------------------------------------------------------------------- /apps/api/src/types/AppEnv.ts: -------------------------------------------------------------------------------- 1 | import { BetterAuthInstance } from "@/client"; 2 | import { NotificationWebsocketServer } from "@/durables/NotificationWebsocketServer"; 3 | 4 | export interface AppEnv { 5 | BUCKET: R2Bucket; 6 | HYPERDRIVE: Hyperdrive; 7 | AI: Ai; 8 | 9 | GOOGLE_CLIENT_ID: string; 10 | GOOGLE_CLIENT_SECRET: string; 11 | 12 | STRIPE_SECRET_KEY: string; 13 | STRIPE_WEBHOOK_SECRET: string; 14 | 15 | BETTER_AUTH_URL: string; 16 | BETTER_AUTH_COOKIE_DOMAIN: string; 17 | TRUSTED_ORIGINS: string; 18 | 19 | RESEND_API_KEY: string; 20 | 21 | TRANSCRIPTION_QUEUE: Queue; 22 | NOTIFICATION_WEBSOCKET_SERVER: DurableObjectNamespace; 23 | } 24 | 25 | export type QueueStatus = "enqueued" | "processing" | "completed" | "failed"; 26 | 27 | export interface TranscriptionData { 28 | transcriptId: string; 29 | audioPath: string; 30 | userId: string; 31 | timestamp: number; 32 | status: QueueStatus; 33 | } 34 | 35 | export interface AppContext { 36 | Bindings: AppEnv; 37 | Variables: { 38 | user: BetterAuthInstance["$Infer"]["Session"]["user"] | null; 39 | session: BetterAuthInstance["$Infer"]["Session"]["session"] | null; 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /apps/api/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "declaration": true, 6 | "declarationMap": true 7 | }, 8 | "include": ["src/client.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "strict": true, 7 | "skipLibCheck": true, 8 | "lib": ["ESNext"], 9 | "types": ["@cloudflare/workers-types/2023-07-01"], 10 | "paths": { 11 | "@/*": ["./src/*"] 12 | }, 13 | "jsx": "react-jsx", 14 | "jsxImportSource": "react" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/api/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/wrangler/config-schema.json", 3 | "name": "neostack-api", 4 | "main": "src/index.ts", 5 | "compatibility_date": "2025-04-14", 6 | "compatibility_flags": ["nodejs_compat"], 7 | "keep_vars": true, 8 | "env": { 9 | "local": { 10 | "ai": { 11 | "binding": "AI", 12 | }, 13 | 14 | "hyperdrive": [ 15 | { 16 | "binding": "HYPERDRIVE", 17 | "id": "fa57d5af4d834aba938051235f0d7484", 18 | "localConnectionString": "postgresql://postgres:mysecretpassword@localhost:5432/postgres", 19 | }, 20 | ], 21 | "r2_buckets": [ 22 | { 23 | "binding": "BUCKET", 24 | "bucket_name": "neostack-audio-bucket", 25 | }, 26 | ], 27 | "queues": { 28 | "consumers": [ 29 | { 30 | "queue": "NeoStack-Queue", 31 | }, 32 | ], 33 | "producers": [ 34 | { 35 | "binding": "TRANSCRIPTION_QUEUE", 36 | "queue": "NeoStack-Queue", 37 | }, 38 | ], 39 | }, 40 | "durable_objects": { 41 | "bindings": [ 42 | { 43 | "name": "NOTIFICATION_WEBSOCKET_SERVER", 44 | "class_name": "NotificationWebsocketServer", 45 | }, 46 | ], 47 | }, 48 | }, 49 | "prod": { 50 | "ai": { 51 | "binding": "AI", 52 | }, 53 | "hyperdrive": [ 54 | { 55 | "binding": "HYPERDRIVE", 56 | "id": "fa57d5af4d834aba938051235f0d7484", 57 | }, 58 | ], 59 | "r2_buckets": [ 60 | { 61 | "binding": "BUCKET", 62 | "bucket_name": "neostack-audio-bucket", 63 | }, 64 | ], 65 | "queues": { 66 | "consumers": [ 67 | { 68 | "queue": "neostack-queue", 69 | }, 70 | ], 71 | "producers": [ 72 | { 73 | "binding": "TRANSCRIPTION_QUEUE", 74 | "queue": "neostack-queue", 75 | }, 76 | ], 77 | }, 78 | "durable_objects": { 79 | "bindings": [ 80 | { 81 | "name": "NOTIFICATION_WEBSOCKET_SERVER", 82 | "class_name": "NotificationWebsocketServer", 83 | 84 | }, 85 | ], 86 | 87 | }, 88 | "migrations": [ 89 | { 90 | "tag": "v1", 91 | "new_classes": ["NotificationWebsocketServer"], 92 | } 93 | ], 94 | "routes": [ 95 | { 96 | "pattern": "prod-api.stack.neoprint3d.dev", 97 | "custom_domain": true, 98 | }, 99 | ], 100 | }, 101 | }, 102 | "observability": { 103 | "enabled": true, 104 | "head_sampling_rate": 1, 105 | }, 106 | "dev": { 107 | "port": 8080, 108 | }, 109 | } 110 | -------------------------------------------------------------------------------- /apps/web/.env.example: -------------------------------------------------------------------------------- 1 | PUBLIC_API_URL=http://localhost:8080 2 | PUBLIC_SITE_URL=http://localhost:4321 -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # generated types 5 | .astro/ 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # logs 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # environment variables 17 | .env 18 | .env.* 19 | 20 | !.env.example 21 | 22 | # macOS-specific files 23 | .DS_Store 24 | 25 | # jetbrains setting folder 26 | .idea/ 27 | 28 | # wrangler files 29 | .wrangler 30 | .dev.vars* 31 | -------------------------------------------------------------------------------- /apps/web/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /apps/web/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "wrangler.json": "jsonc" 4 | } 5 | } -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | # Astro Starter Kit: Basics 2 | 3 | ```sh 4 | pnpm create astro@latest -- --template basics 5 | ``` 6 | 7 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) 8 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) 9 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) 10 | 11 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 12 | 13 | ![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) 14 | 15 | ## 🚀 Project Structure 16 | 17 | Inside of your Astro project, you'll see the following folders and files: 18 | 19 | ```text 20 | / 21 | ├── public/ 22 | │ └── favicon.svg 23 | ├── src/ 24 | │ ├── layouts/ 25 | │ │ └── Layout.astro 26 | │ └── pages/ 27 | │ └── index.astro 28 | └── package.json 29 | ``` 30 | 31 | To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/). 32 | 33 | ## 🧞 Commands 34 | 35 | All commands are run from the root of the project, from a terminal: 36 | 37 | | Command | Action | 38 | | :------------------------ | :----------------------------------------------- | 39 | | `pnpm install` | Installs dependencies | 40 | | `pnpm dev` | Starts local dev server at `localhost:4321` | 41 | | `pnpm build` | Build your production site to `./dist/` | 42 | | `pnpm preview` | Preview your build locally, before deploying | 43 | | `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` | 44 | | `pnpm astro -- --help` | Get help using the Astro CLI | 45 | 46 | ## 👀 Want to learn more? 47 | 48 | Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). 49 | -------------------------------------------------------------------------------- /apps/web/astro.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig, envField } from "astro/config"; 3 | 4 | import cloudflare from "@astrojs/cloudflare"; 5 | 6 | import react from "@astrojs/react"; 7 | import tailwindcss from "@tailwindcss/vite"; 8 | 9 | // https://astro.build/config 10 | export default defineConfig({ 11 | site: import.meta.env.PUBLIC_SITE_URL, 12 | devToolbar: { enabled: false }, 13 | adapter: cloudflare({ 14 | imageService: "compile", 15 | platformProxy: { 16 | enabled: true, 17 | environment: "local", 18 | persist: { 19 | path: "../api/.wrangler/state/v3", 20 | }, 21 | }, 22 | }), 23 | 24 | output: "server", 25 | 26 | vite: { 27 | ssr: { 28 | external: ["node:buffer", "node:events", "node:stream"], 29 | }, 30 | resolve: { 31 | // @ts-ignore 32 | alias: import.meta.env.PROD && { 33 | "react-dom/server": "react-dom/server.edge", 34 | }, 35 | }, 36 | plugins: [tailwindcss()], 37 | }, 38 | integrations: [react()], 39 | }); 40 | -------------------------------------------------------------------------------- /apps/web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "../../packages/ui/src/styles/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true 11 | }, 12 | "iconLibrary": "lucide", 13 | "aliases": { 14 | "components": "@/components", 15 | "hooks": "@/hooks", 16 | "lib": "@/lib", 17 | "utils": "@neostack/ui/lib/utils", 18 | "ui": "@neostack/ui/components" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neostack/web", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "dotenv -e .env.local -- astro dev", 7 | "build": "astro build", 8 | "preview": "astro build && wrangler dev --env prod", 9 | "astro": "astro", 10 | "deploy": "astro build --mode prod && wrangler deploy --env prod", 11 | "cf-typegen": "wrangler types --env local" 12 | }, 13 | "dependencies": { 14 | "@astrojs/cloudflare": "^12.4.1", 15 | "@astrojs/react": "^4.2.4", 16 | "@hookform/resolvers": "^5.0.1", 17 | "@neostack/api": "workspace:*", 18 | "@neostack/constants": "workspace:*", 19 | "@neostack/database": "workspace:*", 20 | "@neostack/ui": "workspace:*", 21 | "@tailwindcss/vite": "^4.1.3", 22 | "@tanstack/react-query": "^5.74.4", 23 | "@types/react": "^19.1.1", 24 | "@types/react-dom": "^19.1.2", 25 | "@uidotdev/usehooks": "^2.4.1", 26 | "astro": "^5.6.2", 27 | "better-auth": "^1.2.7", 28 | "hono": "^4.7.6", 29 | "lucide-react": "^0.475.0", 30 | "motion": "^12.7.4", 31 | "nuqs": "^2.4.2", 32 | "react": "^19.1.0", 33 | "react-dom": "^19.1.0", 34 | "react-hook-form": "^7.55.0", 35 | "react-intersection-observer": "^9.16.0", 36 | "sonner": "^2.0.3", 37 | "tailwindcss": "^4.1.3", 38 | "zod": "^3.24.2" 39 | }, 40 | "devDependencies": { 41 | "@cloudflare/workers-types": "^4.20250414.0", 42 | "@neostack/typescript-config": "workspace:*", 43 | "@types/node": "^20", 44 | "dotenv-cli": "^8.0.0", 45 | "wrangler": "^4.11.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /apps/web/public/.assetsignore: -------------------------------------------------------------------------------- 1 | _worker.js 2 | _routes.json -------------------------------------------------------------------------------- /apps/web/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /apps/web/src/assets/astro.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/web/src/assets/background.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/web/src/components/DashboardHeader.tsx: -------------------------------------------------------------------------------- 1 | import { SidebarTrigger } from "@neostack/ui/components/sidebar"; 2 | import { NotificationBell } from "./NotificationBell"; 3 | 4 | interface DashboardHeaderProps { 5 | pageName: string; 6 | } 7 | export default function DashboardHeader({ pageName }: DashboardHeaderProps) { 8 | return ( 9 |
10 | 11 |

12 | {pageName} 13 |

14 |
15 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /apps/web/src/components/PagesDashboard.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardHeader, 4 | CardTitle, 5 | CardDescription, 6 | } from "@neostack/ui/components/card"; 7 | 8 | export function PagesDashboard() { 9 | return ( 10 |
11 |

12 | Transcription Dashboard 13 |

14 |
15 | {/* Form Card */} 16 | 17 | 18 | Queue New Transcription 19 | 20 | Submit an audio URL to start processing. 21 | 22 | 23 | 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /apps/web/src/components/PagesDashboardTranscripts.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { Button } from "@neostack/ui/components/button"; 3 | import { 4 | Card, 5 | CardHeader, 6 | CardTitle, 7 | CardDescription, 8 | CardContent, 9 | } from "@neostack/ui/components/card"; 10 | import { 11 | Table, 12 | TableBody, 13 | TableCell, 14 | TableHead, 15 | TableHeader, 16 | TableRow, 17 | } from "@neostack/ui/components/table"; 18 | import { toast } from "sonner"; 19 | import { apiClient } from "@/lib/apiClient"; 20 | import { Loader2 } from "lucide-react"; 21 | import type { InferResponseType } from "hono/client"; 22 | 23 | interface Pagination { 24 | page: number; 25 | limit: number; 26 | totalCount: number; 27 | totalPages: number; 28 | hasNextPage: boolean; 29 | hasPreviousPage: boolean; 30 | } 31 | 32 | export function PagesDashboardTranscripts() { 33 | const [transcripts, setTranscripts] = useState< 34 | InferResponseType["data"] 35 | >([]); 36 | const [pagination, setPagination] = useState({ 37 | page: 1, 38 | limit: 10, 39 | totalCount: 0, 40 | totalPages: 1, 41 | hasNextPage: false, 42 | hasPreviousPage: false, 43 | }); 44 | const [loading, setLoading] = useState(false); 45 | const [error, setError] = useState(null); 46 | 47 | // Fetch transcripts from the API 48 | const fetchTranscripts = async (page: number, limit: number) => { 49 | setLoading(true); 50 | setError(null); 51 | try { 52 | const response = await apiClient.v1.transcripts.$get({ 53 | query: { page: page.toString(), limit: limit.toString() }, 54 | }); 55 | if (!response.ok) { 56 | const data = await response.json(); 57 | throw new Error(data.message || "Failed to fetch transcripts"); 58 | } 59 | const data = await response.json(); 60 | setTranscripts(data.data); 61 | setPagination(data.pagination); 62 | } catch (err) { 63 | const errorMessage = 64 | err instanceof Error ? err.message : "An unexpected error occurred"; 65 | setError(errorMessage); 66 | toast.error(errorMessage); 67 | } finally { 68 | setLoading(false); 69 | } 70 | }; 71 | 72 | // Initial fetch and refetch on page/limit change 73 | useEffect(() => { 74 | fetchTranscripts(pagination.page, pagination.limit); 75 | }, []); 76 | 77 | // Handle page change 78 | const handlePageChange = (newPage: number) => { 79 | if (newPage >= 1 && newPage <= pagination.totalPages) { 80 | fetchTranscripts(newPage, pagination.limit); 81 | } 82 | }; 83 | 84 | // Format date for display 85 | const formatDate = (dateString: string) => { 86 | return new Date(dateString).toLocaleDateString("en-US", { 87 | year: "numeric", 88 | month: "short", 89 | day: "numeric", 90 | hour: "2-digit", 91 | minute: "2-digit", 92 | }); 93 | }; 94 | 95 | return ( 96 |
97 |

98 | Transcripts Dashboard 99 |

100 |
101 | 102 | 103 | Your Transcripts 104 | 105 | View and manage your transcription jobs. 106 | 107 | 108 | 109 | {loading ? ( 110 |
111 | 112 | Loading transcripts... 113 |
114 | ) : error ? ( 115 |
{error}
116 | ) : transcripts.length === 0 ? ( 117 |
118 | No transcripts found. Upload an audio file to get started. 119 |
120 | ) : ( 121 | <> 122 | 123 | 124 | 125 | Title 126 | Created At 127 | 128 | 129 | 130 | {transcripts.map((transcript) => ( 131 | 132 | 133 | 138 | 139 | 140 | {formatDate(transcript.createdAt)} 141 | 142 | 143 | ))} 144 | 145 |
146 | {/* Pagination Controls */} 147 |
148 |
149 | Showing {transcripts.length} of {pagination.totalCount}{" "} 150 | transcripts 151 |
152 |
153 | 161 |
162 | Page {pagination.page} of {pagination.totalPages} 163 |
164 | 172 |
173 |
174 | 175 | )} 176 |
177 |
178 |
179 |
180 | ); 181 | } 182 | -------------------------------------------------------------------------------- /apps/web/src/components/PagesHome.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@neostack/ui/components/button"; 2 | import { 3 | Card, 4 | CardContent, 5 | CardHeader, 6 | CardTitle, 7 | } from "@neostack/ui/components/card"; 8 | import { ArrowRight } from "lucide-react"; 9 | export function PagesHome() { 10 | return ( 11 |
12 | {/* Hero Section */} 13 |
14 |
15 |
16 |

17 | Build the Future 18 |

19 |

20 | Unleash your creativity with our cutting-edge platform. Fast, 21 | intuitive, and built for innovators. 22 |

23 |
24 | 34 | 41 |
42 |
43 |
44 | 45 | {/* Features Section */} 46 |
47 |
48 |

49 | Why We're Different 50 |

51 |
52 | {/* Feature 1 */} 53 | 54 | 55 | 56 | Lightning Fast 57 | 58 | 59 | 60 |

61 | Optimized for speed, our platform delivers instant results 62 | without compromise. 63 |

64 |
65 |
66 | {/* Feature 2 */} 67 | 68 | 69 | 70 | Seamless Design 71 | 72 | 73 | 74 |

75 | Intuitive interfaces that make complex tasks feel effortless 76 | and natural. 77 |

78 |
79 |
80 | {/* Feature 3 */} 81 | 82 | 83 | 84 | Always On Support 85 | 86 | 87 | 88 |

89 | Our team is here 24/7 to ensure you succeed at every step. 90 |

91 |
92 |
93 |
94 |
95 |
96 | 97 | {/* CTA Section */} 98 |
99 |
100 |

101 | Ready to Elevate Your Game? 102 |

103 |

104 | Join thousands of creators and start building something 105 | extraordinary today. 106 |

107 | 114 |
115 |
116 |
117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /apps/web/src/components/PagesResetPassword.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { zodResolver } from "@hookform/resolvers/zod"; 3 | import { useForm } from "react-hook-form"; 4 | import { z } from "zod"; 5 | import { Button } from "@neostack/ui/components/button"; 6 | import { 7 | Card, 8 | CardHeader, 9 | CardTitle, 10 | CardDescription, 11 | CardContent, 12 | CardFooter, 13 | } from "@neostack/ui/components/card"; 14 | import { 15 | Form, 16 | FormControl, 17 | FormField, 18 | FormItem, 19 | FormLabel, 20 | FormMessage, 21 | } from "@neostack/ui/components/form"; 22 | import { Input } from "@neostack/ui/components/input"; 23 | import { authClient } from "@/lib/authClient"; 24 | import { toast } from "sonner"; 25 | import { navigate } from "astro/virtual-modules/transitions-router.js"; 26 | 27 | // Form schema 28 | const resetPasswordSchema = z 29 | .object({ 30 | password: z.string().min(8, "Password must be at least 8 characters"), 31 | confirmPassword: z.string().min(8, "Please confirm your password"), 32 | }) 33 | .refine((data) => data.password === data.confirmPassword, { 34 | message: "Passwords do not match", 35 | path: ["confirmPassword"], 36 | }); 37 | 38 | type ResetPasswordForm = z.infer; 39 | 40 | export function PagesResetPassword() { 41 | const [isSubmitting, setIsSubmitting] = useState(false); 42 | 43 | // Form setup 44 | const resetForm = useForm({ 45 | resolver: zodResolver(resetPasswordSchema), 46 | defaultValues: { 47 | password: "", 48 | confirmPassword: "", 49 | }, 50 | }); 51 | 52 | // Form submission handler 53 | const onSubmit = async (values: ResetPasswordForm) => { 54 | setIsSubmitting(true); 55 | const token = new URLSearchParams(window.location.search).get("token"); 56 | if (!token) { 57 | toast.error("Invalid token. Please try again."); 58 | setIsSubmitting(false); 59 | return; 60 | } 61 | await authClient.resetPassword({ 62 | newPassword: values.password, 63 | token, 64 | fetchOptions: { 65 | onSuccess: async () => { 66 | toast.success("Password reset successfully. You can now sign in."); 67 | resetForm.reset(); 68 | setIsSubmitting(false); 69 | window.location.href = "/login"; 70 | }, 71 | onError: (res) => { 72 | const errorMessage = res.error.message || "An error occurred"; 73 | toast.error(errorMessage); 74 | resetForm.setError("root", { 75 | message: errorMessage, 76 | }); 77 | setIsSubmitting(false); 78 | }, 79 | }, 80 | }); 81 | }; 82 | 83 | return ( 84 |
85 | 86 | 87 | Reset your password 88 | 89 | Enter a new password for your account 90 | 91 | 92 | 93 |
94 | 98 | ( 102 | 103 | New Password 104 | 105 | 110 | 111 | 112 | 113 | )} 114 | /> 115 | ( 119 | 120 | Confirm Password 121 | 122 | 127 | 128 | 129 | 130 | )} 131 | /> 132 | 133 | {resetForm.formState.errors.root && ( 134 |

135 | {resetForm.formState.errors.root.message} 136 |

137 | )} 138 | 139 | 142 | 143 | 144 |
145 | 146 |
147 | Return to{" "} 148 | 152 | Sign in 153 | 154 |
155 |
156 |
157 |
158 | ); 159 | } 160 | -------------------------------------------------------------------------------- /apps/web/src/components/PagesVerifyEmail.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { zodResolver } from "@hookform/resolvers/zod"; 3 | import { useForm } from "react-hook-form"; 4 | import { z } from "zod"; 5 | import { Button } from "@neostack/ui/components/button"; 6 | import { 7 | Card, 8 | CardHeader, 9 | CardTitle, 10 | CardDescription, 11 | CardContent, 12 | CardFooter, 13 | } from "@neostack/ui/components/card"; 14 | import { 15 | Form, 16 | FormControl, 17 | FormField, 18 | FormItem, 19 | FormLabel, 20 | FormMessage, 21 | } from "@neostack/ui/components/form"; 22 | import { Input } from "@neostack/ui/components/input"; 23 | import { authClient } from "@/lib/authClient"; 24 | import { useQueryState } from "nuqs"; 25 | import { NuqsAdapter } from "nuqs/adapters/react"; 26 | import { toast } from "sonner"; 27 | 28 | // Form schema 29 | const verifyEmailSchema = z.object({ 30 | email: z.string().email("Invalid email address"), 31 | }); 32 | 33 | type VerifyEmailForm = z.infer; 34 | 35 | export function PagesVerifyEmail() { 36 | return ( 37 | 38 | 39 | 40 | ); 41 | } 42 | 43 | function Page() { 44 | const [isSending, setIsSending] = useState(false); 45 | 46 | // Get email from URL query using nuqs 47 | const [emailQuery] = useQueryState("email"); 48 | 49 | // Initialize form with email from query if available 50 | const verifyForm = useForm({ 51 | resolver: zodResolver(verifyEmailSchema), 52 | defaultValues: { 53 | email: emailQuery || "", 54 | }, 55 | }); 56 | 57 | // Update form email and send link if query changes 58 | useEffect(() => { 59 | if (emailQuery) { 60 | verifyForm.setValue("email", emailQuery); 61 | handleSendLink(emailQuery); 62 | } 63 | }, [emailQuery]); 64 | 65 | // Handler for sending/resending verification link 66 | const handleSendLink = (email: string) => { 67 | if (!email) { 68 | verifyForm.setError("root", { 69 | message: "Please enter an email address.", 70 | }); 71 | return; 72 | } 73 | 74 | setIsSending(true); 75 | authClient.sendVerificationEmail({ 76 | email, 77 | callbackURL: `${import.meta.env.PUBLIC_SITE_URL}`, 78 | fetchOptions: { 79 | onSuccess: () => { 80 | toast.success("A verification link has been sent to your email."); 81 | verifyForm.clearErrors("root"); 82 | setIsSending(false); 83 | }, 84 | onError: (error) => { 85 | console.error("Send verification email error:", error); 86 | verifyForm.setError("root", { 87 | message: "Failed to send verification link. Please try again.", 88 | }); 89 | setIsSending(false); 90 | }, 91 | }, 92 | }); 93 | }; 94 | 95 | // Form submission handler 96 | const onSubmit = (values: VerifyEmailForm) => { 97 | handleSendLink(values.email); 98 | }; 99 | 100 | return ( 101 |
102 | 103 | 104 | Verify your email 105 | 106 | Enter your email address to receive a verification link 107 | 108 | 109 | 110 |
111 | 115 | ( 119 | 120 | Email 121 | 122 | 127 | 128 | 129 | 130 | )} 131 | /> 132 | 133 | {verifyForm.formState.errors.root && ( 134 |

135 | {verifyForm.formState.errors.root.message} 136 |

137 | )} 138 | 139 | 142 | 143 | 144 |
145 | 146 |
147 | Didn't receive a link?{" "} 148 | 157 |
158 |
159 | Already verified?{" "} 160 | 164 | Sign in 165 | 166 |
167 |
168 |
169 |
170 | ); 171 | } 172 | -------------------------------------------------------------------------------- /apps/web/src/components/PagesVerifyPhone.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeoPrint3D/neostack/ce5ead93abea7ac0f90193eaabd4252cbc6f244c/apps/web/src/components/PagesVerifyPhone.tsx -------------------------------------------------------------------------------- /apps/web/src/env.d.ts: -------------------------------------------------------------------------------- 1 | type Runtime = import("@astrojs/cloudflare").Runtime; 2 | 3 | interface Auth { 4 | user: import("better-auth").User; 5 | session: import("better-auth").Session; 6 | } 7 | declare namespace App { 8 | interface Locals extends Runtime { 9 | auth: Auth | null; 10 | } 11 | } 12 | 13 | interface ImportMetaEnv { 14 | PUBLIC_API_URL: string; 15 | PUBLIC_SITE_URL: string; 16 | } 17 | 18 | interface ImportMeta { 19 | readonly env: ImportMetaEnv; 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { AppHeader } from "@/components/AppHeader"; 3 | import LayoutBase from "./LayoutBase.astro"; 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /apps/web/src/layouts/LayoutBase.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { AppHeader } from "@/components/AppHeader"; 3 | import { ClientRouter } from "astro:transitions"; 4 | import { Toaster } from "@neostack/ui/components/sonner"; 5 | import "@neostack/ui/globals.css"; 6 | --- 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Astro Basics 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 32 | -------------------------------------------------------------------------------- /apps/web/src/layouts/LayoutDashboard.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { LayoutDashboard as LayoutDashboardReact } from "@/components/LayoutDashboard"; 3 | import { Toaster } from "@neostack/ui/components/sonner"; 4 | import LayoutBase from "./LayoutBase.astro"; 5 | const { pageName, customPadding } = Astro.props; 6 | --- 7 | 8 | 9 | 17 |
18 | 19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /apps/web/src/lib/apiClient.ts: -------------------------------------------------------------------------------- 1 | import { api } from "@neostack/api/client"; 2 | 3 | export const apiClient = api(import.meta.env.PUBLIC_API_URL, { 4 | init: { 5 | credentials: "include", 6 | }, 7 | }); 8 | 9 | export type ExtractedApiData< 10 | T extends (...args: any[]) => Promise<{ json: () => Promise }>, 11 | K extends string, 12 | > = Awaited>["json"]>>[K]; 13 | -------------------------------------------------------------------------------- /apps/web/src/lib/authClient.ts: -------------------------------------------------------------------------------- 1 | import { createAuthClient } from "better-auth/react"; 2 | import { organizationClient, adminClient } from "better-auth/client/plugins"; 3 | import { toast } from "sonner"; 4 | import { navigate } from "astro:transitions/client"; 5 | export const authClient = createAuthClient({ 6 | baseURL: `${import.meta.env.PUBLIC_API_URL}/v1/auth`, 7 | plugins: [organizationClient(), adminClient()], 8 | }); 9 | 10 | export const handleSignOut = async () => { 11 | await authClient.signOut({ 12 | fetchOptions: { 13 | onSuccess: async () => { 14 | toast.success("Signed out successfully!"); 15 | await navigate("/login"); 16 | }, 17 | }, 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /apps/web/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { authClient } from "@/lib/authClient"; 2 | import { defineMiddleware } from "astro:middleware"; 3 | 4 | export const onRequest = defineMiddleware(async (context, next) => { 5 | const { data, error } = await authClient.getSession({ 6 | fetchOptions: { 7 | headers: context.request.headers, 8 | }, 9 | }); 10 | console.log("fetching user"); 11 | if (data && !error) { 12 | context.locals.auth = data; 13 | } else { 14 | context.locals.auth = null; 15 | } 16 | 17 | return next(); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/web/src/pages/dashboard/file-upload.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import LayoutDashboard from "@/layouts/LayoutDashboard.astro"; 3 | import { PagesDashboardAudioFileUpload } from "@/components/PagesDashboardFileUpload"; 4 | if (!Astro.locals.auth) { 5 | return Astro.redirect("/login", 302); 6 | } 7 | --- 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /apps/web/src/pages/dashboard/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { PagesDashboard } from "@/components/PagesDashboard"; 3 | import LayoutDashboard from "@/layouts/LayoutDashboard.astro"; 4 | 5 | if (!Astro.locals.auth) { 6 | return Astro.redirect("/login", 302); 7 | } 8 | --- 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /apps/web/src/pages/dashboard/record.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import LayoutDashboard from "@/layouts/LayoutDashboard.astro"; 3 | --- 4 | 5 | test 6 | -------------------------------------------------------------------------------- /apps/web/src/pages/dashboard/transcripts/[id].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { PagesDashboardTranscript } from "@/components/PagesDashboardTranscript"; 3 | import LayoutDashboard from "@/layouts/LayoutDashboard.astro"; 4 | import { drizzle, transcriptions, eq } from "@neostack/database"; 5 | 6 | // Extract parameters and environment variables 7 | const { id } = Astro.params; 8 | const { HYPERDRIVE, BUCKET } = Astro.locals.runtime.env; 9 | 10 | const db = drizzle(HYPERDRIVE.connectionString); 11 | 12 | const transcript = ( 13 | await db.select().from(transcriptions).where(eq(transcriptions.id, id!)) 14 | )[0]; 15 | 16 | if (!transcript) { 17 | throw new Response("Transcript Not Found", { status: 404 }); 18 | } 19 | --- 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /apps/web/src/pages/dashboard/transcripts/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import LayoutDashboard from "@/layouts/LayoutDashboard.astro"; 3 | import { PagesDashboardTranscripts } from "@/components/PagesDashboardTranscripts"; 4 | if (!Astro.locals.auth) { 5 | return Astro.redirect("/login", 302); 6 | } 7 | --- 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /apps/web/src/pages/forgot-password.astro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeoPrint3D/neostack/ce5ead93abea7ac0f90193eaabd4252cbc6f244c/apps/web/src/pages/forgot-password.astro -------------------------------------------------------------------------------- /apps/web/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { PagesHome } from "@/components/PagesHome"; 3 | import Layout from "../layouts/Layout.astro"; 4 | --- 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /apps/web/src/pages/login.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { PagesLogin } from "@/components/PagesLogin"; 3 | import Layout from "@/layouts/Layout.astro"; 4 | 5 | if (Astro.locals.auth) { 6 | return Astro.redirect("/dashboard"); 7 | } 8 | --- 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /apps/web/src/pages/register.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { PagesRegister } from "@/components/PagesRegister"; 3 | import Layout from "@/layouts/Layout.astro"; 4 | 5 | if (Astro.locals.auth) { 6 | return Astro.redirect("/dashboard", 302); 7 | } 8 | --- 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /apps/web/src/pages/reset-password.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { PagesResetPassword } from "@/components/PagesResetPassword"; 3 | import Layout from "@/layouts/Layout.astro"; 4 | --- 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /apps/web/src/pages/settings/billing/index.astro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeoPrint3D/neostack/ce5ead93abea7ac0f90193eaabd4252cbc6f244c/apps/web/src/pages/settings/billing/index.astro -------------------------------------------------------------------------------- /apps/web/src/pages/settings/security.astro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeoPrint3D/neostack/ce5ead93abea7ac0f90193eaabd4252cbc6f244c/apps/web/src/pages/settings/security.astro -------------------------------------------------------------------------------- /apps/web/src/pages/settings/sessions.astro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeoPrint3D/neostack/ce5ead93abea7ac0f90193eaabd4252cbc6f244c/apps/web/src/pages/settings/sessions.astro -------------------------------------------------------------------------------- /apps/web/src/pages/settings/user.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // pages/settings/user.astro 3 | import { PagesSettingsUser } from "@/components/PagesSettingsUser"; 4 | 5 | if (!Astro.locals.auth) { 6 | return Astro.redirect("/login", 302); 7 | } 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /apps/web/src/pages/verify-email.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { PagesVerifyEmail } from "@/components/PagesVerifyEmail"; 3 | import Layout from "@/layouts/Layout.astro"; 4 | --- 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /apps/web/src/utils/deriveUserData.ts: -------------------------------------------------------------------------------- 1 | export function deriveUserData(initialAuth: Auth | null) { 2 | if (!initialAuth?.user) return null; 3 | 4 | const name = 5 | initialAuth.user.name || initialAuth.user.email?.split("@")[0] || "User"; 6 | 7 | return { 8 | name, 9 | email: initialAuth.user.email || "no-email@example.com", 10 | avatar: initialAuth.user.image, 11 | initials: name 12 | .split(" ") 13 | .map((n) => n[0]) 14 | .join("") 15 | .slice(0, 2) 16 | .toUpperCase(), 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "include": [ 4 | ".astro/types.d.ts", 5 | "**/*" 6 | ], 7 | "exclude": [ 8 | "dist" 9 | ], 10 | "compilerOptions": { 11 | "strict": true, 12 | "types": [ 13 | "@cloudflare/workers-types/2023-07-01" 14 | ], 15 | "baseUrl": "./", 16 | "paths": { 17 | "@/*": [ 18 | "./src/*" 19 | ], 20 | "@neostack/ui/*": [ 21 | "../../packages/ui/src/*" 22 | ] 23 | }, 24 | "jsx": "react-jsx", 25 | "jsxImportSource": "react" 26 | } 27 | } -------------------------------------------------------------------------------- /apps/web/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/wrangler/config-schema.json", 3 | "name": "neostack-web", 4 | "main": "./dist/_worker.js/index.js", 5 | "compatibility_date": "2025-03-25", 6 | "compatibility_flags": ["nodejs_compat"], 7 | "keep_vars": true, 8 | "workers_dev": false, 9 | "preview_urls": false, 10 | "assets": { 11 | "binding": "ASSETS", 12 | "directory": "./dist", 13 | }, 14 | "observability": { 15 | "enabled": true, 16 | "logs": { 17 | "enabled": true, 18 | }, 19 | }, 20 | 21 | "env": { 22 | "local": { 23 | "hyperdrive": [ 24 | { 25 | "binding": "HYPERDRIVE", 26 | "id": "fa57d5af4d834aba938051235f0d7484", 27 | "localConnectionString": "postgresql://postgres:mysecretpassword@localhost:5432/postgres", 28 | }, 29 | 30 | ], 31 | "r2_buckets": [ 32 | { 33 | "binding": "BUCKET", 34 | "bucket_name": "neostack-audio-bucket", 35 | }, 36 | ], 37 | }, 38 | "prod": { 39 | "hyperdrive": [ 40 | { 41 | "binding": "HYPERDRIVE", 42 | "id": "fa57d5af4d834aba938051235f0d7484", 43 | }, 44 | ], 45 | "r2_buckets": [ 46 | { 47 | "binding": "BUCKET", 48 | "bucket_name": "neostack-audio-bucket", 49 | }, 50 | ], 51 | "routes": [ 52 | { 53 | "pattern": "www.stack.neoprint3d.dev", 54 | "custom_domain": true, 55 | }, 56 | ], 57 | }, 58 | }, 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neostack", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "build": "turbo build", 7 | "dev": "turbo dev", 8 | "lint": "turbo lint", 9 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 10 | }, 11 | "devDependencies": { 12 | "@neostack/typescript-config": "workspace:*", 13 | "prettier": "^3.5.1", 14 | "turbo": "^2.4.2", 15 | "typescript": "5.7.3" 16 | }, 17 | "packageManager": "pnpm@10.8.1", 18 | "engines": { 19 | "node": ">=20" 20 | }, 21 | "pnpm": { 22 | "ignoredBuiltDependencies": [ 23 | "esbuild", 24 | "workerd" 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/constants/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neostack/constants", 3 | "version": "0.0.0", 4 | "exports": { 5 | ".": { 6 | "import": "./src/index.ts" 7 | } 8 | }, 9 | "scripts": {}, 10 | "packageManager": "pnpm@10.8.1", 11 | "dependencies": { 12 | "colorizr": "^3.0.7", 13 | "@neostack/typescript-config": "workspace:*" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/constants/src/appInfo.ts: -------------------------------------------------------------------------------- 1 | export const appInfo = { 2 | name: "NeoStack", 3 | email: "no-reply@neoprint3d.dev", 4 | }; 5 | -------------------------------------------------------------------------------- /packages/constants/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./appInfo"; 2 | export * from "./tailwindConfig"; 3 | -------------------------------------------------------------------------------- /packages/constants/src/tailwindConfig.ts: -------------------------------------------------------------------------------- 1 | export const tailwindConfig = { 2 | theme: { 3 | extend: { 4 | colors: { 5 | background: "#F7F5E6", 6 | foreground: "#4A4E42", 7 | card: "#F7F5E6", 8 | "card-foreground": "#2E2F2B", 9 | popover: "#FFFFFF", 10 | "popover-foreground": "#3C3F36", 11 | primary: "#C75000", 12 | "primary-foreground": "#FFFFFF", 13 | secondary: "#E8E4CF", 14 | "secondary-foreground": "#6B695C", 15 | muted: "#EBE7C6", 16 | "muted-foreground": "#9C9986", 17 | accent: "#E8E4CF", 18 | "accent-foreground": "#3C3F36", 19 | destructive: "#8B1D07", 20 | "destructive-foreground": "#FFDBD2", 21 | border: "#DEDAC3", 22 | input: "#BDBA9E", 23 | ring: "#4E5AD4", 24 | "chart-1": "#A94408", 25 | "chart-2": "#A135BF", 26 | "chart-3": "#DED9B8", 27 | "chart-4": "#D9D2E0", 28 | "chart-5": "#A64509", 29 | sidebar: "#F5F3DD", 30 | "sidebar-foreground": "#56584D", 31 | "sidebar-primary": "#C75000", 32 | "sidebar-primary-foreground": "#FCFCFC", 33 | "sidebar-accent": "#E8E4CF", 34 | "sidebar-accent-foreground": "#535353", 35 | "sidebar-border": "#EFEFEF", 36 | "sidebar-ring": "#C5C5C5", 37 | "shadow-color": "#000000", 38 | }, 39 | fontFamily: { 40 | sans: [ 41 | "ui-sans-serif", 42 | "system-ui", 43 | "-apple-system", 44 | "BlinkMacSystemFont", 45 | '"Segoe UI"', 46 | "Roboto", 47 | '"Helvetica Neue"', 48 | "Arial", 49 | '"Noto Sans"', 50 | "sans-serif", 51 | '"Apple Color Emoji"', 52 | '"Segoe UI Emoji"', 53 | '"Segoe UI Symbol"', 54 | '"Noto Color Emoji"', 55 | ], 56 | serif: [ 57 | "ui-serif", 58 | "Georgia", 59 | "Cambria", 60 | '"Times New Roman"', 61 | "Times", 62 | "serif", 63 | ], 64 | mono: [ 65 | "ui-monospace", 66 | "SFMono-Regular", 67 | "Menlo", 68 | "Monaco", 69 | "Consolas", 70 | '"Liberation Mono"', 71 | '"Courier New"', 72 | "monospace", 73 | ], 74 | }, 75 | borderRadius: { 76 | DEFAULT: "0.5rem", 77 | sm: "0.25rem", 78 | md: "0.375rem", 79 | lg: "0.5rem", 80 | xl: "0.75rem", 81 | }, 82 | spacing: { 83 | DEFAULT: "0.25rem", 84 | }, 85 | letterSpacing: { 86 | normal: "0em", 87 | tighter: "-0.05em", 88 | tight: "-0.025em", 89 | wide: "0.025em", 90 | wider: "0.05em", 91 | widest: "0.1em", 92 | }, 93 | boxShadow: { 94 | "2xs": "0 1px 3px 0 rgba(0, 0, 0, 0.05)", 95 | xs: "0 1px 3px 0 rgba(0, 0, 0, 0.05)", 96 | sm: "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)", 97 | DEFAULT: "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)", 98 | md: "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.1)", 99 | lg: "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 4px 6px -1px rgba(0, 0, 0, 0.1)", 100 | xl: "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 8px 10px -1px rgba(0, 0, 0, 0.1)", 101 | "2xl": "0 1px 3px 0 rgba(0, 0, 0, 0.25)", 102 | }, 103 | }, 104 | }, 105 | }; -------------------------------------------------------------------------------- /packages/constants/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": false, 4 | "skipLibCheck": true, 5 | "strict": true, 6 | "noEmit": true, 7 | "strictNullChecks": true 8 | }, 9 | "include": ["src"], 10 | "exclude": ["node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/database/.gitignore: -------------------------------------------------------------------------------- 1 | .env.* -------------------------------------------------------------------------------- /packages/database/db-local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker run --name postgres-local -e POSTGRES_PASSWORD=mysecretpassword -d -p 5432:5432 --restart always -v postgres-data:/var/lib/postgresql/data supabase/postgres:15.8.1.024 4 | 5 | 6 | # pgadmin web interface make sure to include the actual database host and port 7 | docker run --name pgadmin-local -p 5050:80 -e PGADMIN_DEFAULT_EMAIL="user@domain.com" -e PGADMIN_DEFAULT_PASSWORD="admin" -v pgadmin-data:/var/lib/pgadmin --restart always -d dpage/pgadmin4:8.14 8 | # cli mode 9 | psql -U postgres 10 | CREATE EXTENSION vector; 11 | CREATE EXTENSION postgis; 12 | 13 | -------------------------------------------------------------------------------- /packages/database/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit"; 2 | 3 | export default defineConfig({ 4 | schema: "./src/schema", 5 | dialect: "postgresql", 6 | out: "./migrations", 7 | dbCredentials: { 8 | url: process.env.DATABASE_URL!, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/database/migrations/0000_daffy_ego.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "accounts" ( 2 | "id" text PRIMARY KEY NOT NULL, 3 | "account_id" text NOT NULL, 4 | "provider_id" text NOT NULL, 5 | "user_id" text NOT NULL, 6 | "access_token" text, 7 | "refresh_token" text, 8 | "id_token" text, 9 | "access_token_expires_at" timestamp, 10 | "refresh_token_expires_at" timestamp, 11 | "scope" text, 12 | "password" text, 13 | "created_at" timestamp NOT NULL, 14 | "updated_at" timestamp NOT NULL 15 | ); 16 | --> statement-breakpoint 17 | CREATE TABLE "invitations" ( 18 | "id" text PRIMARY KEY NOT NULL, 19 | "organization_id" text NOT NULL, 20 | "email" text NOT NULL, 21 | "role" text, 22 | "status" text NOT NULL, 23 | "expires_at" timestamp NOT NULL, 24 | "inviter_id" text NOT NULL 25 | ); 26 | --> statement-breakpoint 27 | CREATE TABLE "members" ( 28 | "id" text PRIMARY KEY NOT NULL, 29 | "organization_id" text NOT NULL, 30 | "user_id" text NOT NULL, 31 | "role" text NOT NULL, 32 | "created_at" timestamp NOT NULL 33 | ); 34 | --> statement-breakpoint 35 | CREATE TABLE "organizations" ( 36 | "id" text PRIMARY KEY NOT NULL, 37 | "name" text NOT NULL, 38 | "slug" text, 39 | "logo" text, 40 | "created_at" timestamp NOT NULL, 41 | "metadata" text, 42 | CONSTRAINT "organizations_slug_unique" UNIQUE("slug") 43 | ); 44 | --> statement-breakpoint 45 | CREATE TABLE "sessions" ( 46 | "id" text PRIMARY KEY NOT NULL, 47 | "expires_at" timestamp NOT NULL, 48 | "token" text NOT NULL, 49 | "created_at" timestamp NOT NULL, 50 | "updated_at" timestamp NOT NULL, 51 | "ip_address" text, 52 | "user_agent" text, 53 | "user_id" text NOT NULL, 54 | "impersonated_by" text, 55 | "active_organization_id" text, 56 | CONSTRAINT "sessions_token_unique" UNIQUE("token") 57 | ); 58 | --> statement-breakpoint 59 | CREATE TABLE "subscriptions" ( 60 | "id" text PRIMARY KEY NOT NULL, 61 | "plan" text NOT NULL, 62 | "reference_id" text NOT NULL, 63 | "stripe_customer_id" text, 64 | "stripe_subscription_id" text, 65 | "status" text, 66 | "period_start" timestamp, 67 | "period_end" timestamp, 68 | "cancel_at_period_end" boolean, 69 | "seats" integer 70 | ); 71 | --> statement-breakpoint 72 | CREATE TABLE "users" ( 73 | "id" text PRIMARY KEY NOT NULL, 74 | "name" text NOT NULL, 75 | "email" text NOT NULL, 76 | "email_verified" boolean NOT NULL, 77 | "image" text, 78 | "created_at" timestamp NOT NULL, 79 | "updated_at" timestamp NOT NULL, 80 | "stripe_customer_id" text, 81 | "role" text, 82 | "banned" boolean, 83 | "ban_reason" text, 84 | "ban_expires" timestamp, 85 | CONSTRAINT "users_email_unique" UNIQUE("email") 86 | ); 87 | --> statement-breakpoint 88 | CREATE TABLE "verifications" ( 89 | "id" text PRIMARY KEY NOT NULL, 90 | "identifier" text NOT NULL, 91 | "value" text NOT NULL, 92 | "expires_at" timestamp NOT NULL, 93 | "created_at" timestamp, 94 | "updated_at" timestamp 95 | ); 96 | --> statement-breakpoint 97 | CREATE TABLE "transcript_chunks" ( 98 | "id" text PRIMARY KEY NOT NULL, 99 | "transcription_id" text NOT NULL, 100 | "chunk_index" integer NOT NULL, 101 | "chunk_text" text NOT NULL, 102 | "embedding" vector(1024), 103 | "created_at" timestamp DEFAULT now() NOT NULL 104 | ); 105 | --> statement-breakpoint 106 | CREATE TABLE "transcriptions" ( 107 | "id" text PRIMARY KEY NOT NULL, 108 | "title" text NOT NULL, 109 | "summary" text NOT NULL, 110 | "audio_path" text NOT NULL, 111 | "srt_path" text, 112 | "transcript_path" text, 113 | "user_id" text NOT NULL, 114 | "created_at" timestamp DEFAULT now() NOT NULL, 115 | "updated_at" timestamp DEFAULT now() NOT NULL 116 | ); 117 | --> statement-breakpoint 118 | ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint 119 | ALTER TABLE "invitations" ADD CONSTRAINT "invitations_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint 120 | ALTER TABLE "invitations" ADD CONSTRAINT "invitations_inviter_id_users_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint 121 | ALTER TABLE "members" ADD CONSTRAINT "members_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint 122 | ALTER TABLE "members" ADD CONSTRAINT "members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint 123 | ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint 124 | ALTER TABLE "transcript_chunks" ADD CONSTRAINT "transcript_chunks_transcription_id_transcriptions_id_fk" FOREIGN KEY ("transcription_id") REFERENCES "public"."transcriptions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint 125 | ALTER TABLE "transcriptions" ADD CONSTRAINT "transcriptions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; -------------------------------------------------------------------------------- /packages/database/migrations/0001_watery_satana.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "transcript_chunks" ADD COLUMN "start_time" integer NOT NULL;--> statement-breakpoint 2 | ALTER TABLE "transcript_chunks" ADD COLUMN "end_time" integer NOT NULL; -------------------------------------------------------------------------------- /packages/database/migrations/0002_dusty_mephistopheles.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "transcriptions" RENAME COLUMN "srt_path" TO "subtitle_path"; -------------------------------------------------------------------------------- /packages/database/migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1744791396792, 9 | "tag": "0000_daffy_ego", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "7", 15 | "when": 1744833008439, 16 | "tag": "0001_watery_satana", 17 | "breakpoints": true 18 | }, 19 | { 20 | "idx": 2, 21 | "version": "7", 22 | "when": 1744842490014, 23 | "tag": "0002_dusty_mephistopheles", 24 | "breakpoints": true 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /packages/database/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neostack/database", 3 | "version": "1.0.0", 4 | "description": "", 5 | "exports": { 6 | ".": { 7 | "import": "./src/index.ts" 8 | } 9 | }, 10 | "scripts": { 11 | "build": "tsc -p tsconfig.build.json", 12 | "db:generate": "drizzle-kit generate", 13 | "local:db:migrate": "dotenv -e .env.local -- drizzle-kit migrate", 14 | "local:db:studio": "dotenv -e .env.local -- drizzle-kit studio", 15 | "prod:db:studio": "dotenv -e .env.prod -- drizzle-kit studio", 16 | "prod:db:migrate": "dotenv -e .env.prod -- drizzle-kit migrate" 17 | }, 18 | "dependencies": { 19 | "dotenv": "^16.5.0", 20 | "drizzle-orm": "^0.38.4", 21 | "drizzle-zod": "^0.6.1", 22 | "postgres": "^3.4.5" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^20", 26 | "dotenv-cli": "^8.0.0", 27 | "drizzle-kit": "^0.30.6", 28 | "typescript": "^5.8.3" 29 | }, 30 | "packageManager": "pnpm@9.12.3", 31 | "engines": { 32 | "node": ">=22.11.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/database/src/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "declaration": true, 6 | "declarationMap": true, 7 | "esModuleInterop": true, 8 | "incremental": false, 9 | "isolatedModules": true, 10 | "lib": ["es2022", "DOM", "DOM.Iterable"], 11 | "module": "NodeNext", 12 | "moduleDetection": "force", 13 | "moduleResolution": "NodeNext", 14 | "noUncheckedIndexedAccess": true, 15 | "resolveJsonModule": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "target": "ES2022" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/database/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "drizzle-orm"; 2 | export * from "./schema"; 3 | export * as schema from "./schema"; 4 | export { drizzle, PostgresJsDatabase } from "drizzle-orm/postgres-js"; 5 | -------------------------------------------------------------------------------- /packages/database/src/schema/auth.ts: -------------------------------------------------------------------------------- 1 | import { 2 | pgTable, 3 | text, 4 | timestamp, 5 | boolean, 6 | integer, 7 | } from "drizzle-orm/pg-core"; 8 | 9 | export const users = pgTable("users", { 10 | id: text("id").primaryKey(), 11 | name: text("name").notNull(), 12 | email: text("email").notNull().unique(), 13 | emailVerified: boolean("email_verified").notNull(), 14 | image: text("image"), 15 | createdAt: timestamp("created_at").notNull(), 16 | updatedAt: timestamp("updated_at").notNull(), 17 | stripeCustomerId: text("stripe_customer_id"), 18 | role: text("role"), 19 | banned: boolean("banned"), 20 | banReason: text("ban_reason"), 21 | banExpires: timestamp("ban_expires"), 22 | }); 23 | 24 | export const sessions = pgTable("sessions", { 25 | id: text("id").primaryKey(), 26 | expiresAt: timestamp("expires_at").notNull(), 27 | token: text("token").notNull().unique(), 28 | createdAt: timestamp("created_at").notNull(), 29 | updatedAt: timestamp("updated_at").notNull(), 30 | ipAddress: text("ip_address"), 31 | userAgent: text("user_agent"), 32 | userId: text("user_id") 33 | .notNull() 34 | .references(() => users.id, { onDelete: "cascade" }), 35 | impersonatedBy: text("impersonated_by"), 36 | activeOrganizationId: text("active_organization_id"), 37 | }); 38 | 39 | export const accounts = pgTable("accounts", { 40 | id: text("id").primaryKey(), 41 | accountId: text("account_id").notNull(), 42 | providerId: text("provider_id").notNull(), 43 | userId: text("user_id") 44 | .notNull() 45 | .references(() => users.id, { onDelete: "cascade" }), 46 | accessToken: text("access_token"), 47 | refreshToken: text("refresh_token"), 48 | idToken: text("id_token"), 49 | accessTokenExpiresAt: timestamp("access_token_expires_at"), 50 | refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), 51 | scope: text("scope"), 52 | password: text("password"), 53 | createdAt: timestamp("created_at").notNull(), 54 | updatedAt: timestamp("updated_at").notNull(), 55 | }); 56 | 57 | export const verifications = pgTable("verifications", { 58 | id: text("id").primaryKey(), 59 | identifier: text("identifier").notNull(), 60 | value: text("value").notNull(), 61 | expiresAt: timestamp("expires_at").notNull(), 62 | createdAt: timestamp("created_at"), 63 | updatedAt: timestamp("updated_at"), 64 | }); 65 | 66 | export const subscriptions = pgTable("subscriptions", { 67 | id: text("id").primaryKey(), 68 | plan: text("plan").notNull(), 69 | referenceId: text("reference_id").notNull(), 70 | stripeCustomerId: text("stripe_customer_id"), 71 | stripeSubscriptionId: text("stripe_subscription_id"), 72 | status: text("status"), 73 | periodStart: timestamp("period_start"), 74 | periodEnd: timestamp("period_end"), 75 | cancelAtPeriodEnd: boolean("cancel_at_period_end"), 76 | seats: integer("seats"), 77 | }); 78 | 79 | export const organizations = pgTable("organizations", { 80 | id: text("id").primaryKey(), 81 | name: text("name").notNull(), 82 | slug: text("slug").unique(), 83 | logo: text("logo"), 84 | createdAt: timestamp("created_at").notNull(), 85 | metadata: text("metadata"), 86 | }); 87 | 88 | export const members = pgTable("members", { 89 | id: text("id").primaryKey(), 90 | organizationId: text("organization_id") 91 | .notNull() 92 | .references(() => organizations.id, { onDelete: "cascade" }), 93 | userId: text("user_id") 94 | .notNull() 95 | .references(() => users.id, { onDelete: "cascade" }), 96 | role: text("role").notNull(), 97 | createdAt: timestamp("created_at").notNull(), 98 | }); 99 | 100 | export const invitations = pgTable("invitations", { 101 | id: text("id").primaryKey(), 102 | organizationId: text("organization_id") 103 | .notNull() 104 | .references(() => organizations.id, { onDelete: "cascade" }), 105 | email: text("email").notNull(), 106 | role: text("role"), 107 | status: text("status").notNull(), 108 | expiresAt: timestamp("expires_at").notNull(), 109 | inviterId: text("inviter_id") 110 | .notNull() 111 | .references(() => users.id, { onDelete: "cascade" }), 112 | }); 113 | -------------------------------------------------------------------------------- /packages/database/src/schema/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth"; 2 | export * from "./main"; 3 | -------------------------------------------------------------------------------- /packages/database/src/schema/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | pgTable, 3 | uuid, 4 | text, 5 | timestamp, 6 | boolean, 7 | integer, 8 | vector, 9 | } from "drizzle-orm/pg-core"; 10 | import { users } from "./auth"; 11 | 12 | export const transcriptions = pgTable("transcriptions", { 13 | id: text("id").primaryKey(), 14 | title: text("title").notNull(), 15 | summary: text("summary").notNull(), 16 | audioPath: text("audio_path").notNull(), 17 | subtitlePath: text("subtitle_path"), 18 | transcriptPath: text("transcript_path"), 19 | userId: text("user_id") 20 | .notNull() 21 | .references(() => users.id, { onDelete: "cascade" }), 22 | createdAt: timestamp("created_at").defaultNow().notNull(), 23 | updatedAt: timestamp("updated_at").defaultNow().notNull(), 24 | }); 25 | 26 | export const transcriptionChunks = pgTable("transcript_chunks", { 27 | id: text("id").primaryKey(), 28 | transcriptionId: text("transcription_id") 29 | .notNull() 30 | .references(() => transcriptions.id, { onDelete: "cascade" }), 31 | chunkIndex: integer("chunk_index").notNull(), 32 | chunkText: text("chunk_text").notNull(), 33 | startTime: integer("start_time").notNull(), // Store time in seconds 34 | endTime: integer("end_time").notNull(), // Store time in seconds 35 | embedding: vector("embedding", { dimensions: 1024 }), // adjust dimensions as needed 36 | createdAt: timestamp("created_at").defaultNow().notNull(), 37 | }); 38 | -------------------------------------------------------------------------------- /packages/database/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "strict": true, 7 | "skipLibCheck": true, 8 | "lib": ["ESNext"], 9 | "baseUrl": ".", 10 | "paths": { 11 | "@/*": ["./src/*"] 12 | } 13 | }, 14 | "include": ["src/**/*"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/email/emails/ResetPassword.tsx: -------------------------------------------------------------------------------- 1 | // emails/ResetPasswordEmail.tsx 2 | import { 3 | Body, 4 | Button, 5 | Container, 6 | Head, 7 | Heading, 8 | Html, 9 | Link, 10 | Preview, 11 | Section, 12 | Text, 13 | Tailwind, 14 | } from "@react-email/components"; 15 | import { appInfo, tailwindConfig } from "@neostack/constants"; 16 | 17 | interface ResetPasswordEmailProps { 18 | username?: string; 19 | resetLink?: string; 20 | } 21 | 22 | export const ResetPasswordEmail = ({ 23 | username = "User", 24 | resetLink = "https://example.com/reset-password?token=abc123", 25 | }: ResetPasswordEmailProps) => ( 26 | 27 | 28 | Reset your password 29 | 30 | 31 | 32 |
33 | 34 | Hello, {username}! 35 | 36 | 37 | We received a request to reset your password. Click the button 38 | below to set a new password. 39 | 40 |
41 | 47 |
48 | 49 | If the button above doesn’t work, copy and paste the following 50 | link into your browser: 51 | 52 | 56 | {resetLink} 57 | 58 | 59 | If you didn’t request a password reset, you can safely ignore this 60 | email. 61 | 62 |
63 |
64 | 65 | © {new Date().getFullYear()} {appInfo.name}. All rights reserved. 66 | 67 |
68 |
69 | 70 |
71 | 72 | ); 73 | 74 | export default ResetPasswordEmail; 75 | -------------------------------------------------------------------------------- /packages/email/emails/VerificationEmail.tsx: -------------------------------------------------------------------------------- 1 | // emails/VerificationEmail.tsx 2 | import { 3 | Body, 4 | Button, 5 | Container, 6 | Head, 7 | Heading, 8 | Html, 9 | Link, 10 | Preview, 11 | Section, 12 | Text, 13 | Tailwind, 14 | } from "@react-email/components"; 15 | import { appInfo, tailwindConfig } from "@neostack/constants"; 16 | interface VerificationEmailProps { 17 | username?: string; 18 | verificationLink?: string; 19 | } 20 | 21 | export const VerificationEmail = ({ 22 | username = "User", 23 | verificationLink = "https://example.com/verify?token=abc123", 24 | }: VerificationEmailProps) => ( 25 | 26 | 27 | Verify your account 28 | 29 | 30 | 31 |
32 | 33 | Welcome, {username}! 34 | 35 | 36 | Thank you for signing up. Please verify your email address to 37 | activate your account. 38 | 39 |
40 | 46 |
47 | 48 | If the button above doesn’t work, copy and paste the following 49 | link into your browser: 50 | 51 | 55 | {verificationLink} 56 | 57 | 58 | If you didn’t create this account, you can safely ignore this 59 | email. 60 | 61 |
62 |
63 | 64 | © {new Date().getFullYear()} {appInfo.name}. All rights reserved. 65 | 66 |
67 |
68 | 69 |
70 | 71 | ); 72 | 73 | export default VerificationEmail; 74 | -------------------------------------------------------------------------------- /packages/email/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neostack/email", 3 | "version": "1.1.0", 4 | "scripts": { 5 | "build": "email build", 6 | "dev": "email dev", 7 | "export": "email export" 8 | }, 9 | "exports": { 10 | "./*": "./emails/*.tsx" 11 | }, 12 | "workspaces": [ 13 | ".react-email" 14 | ], 15 | "dependencies": { 16 | "@neostack/constants": "workspace:*", 17 | "@react-email/components": "^0.0.36", 18 | "react-dom": "19.1.0", 19 | "react": "19.1.0" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "19.1.1", 23 | "@types/react-dom": "19.1.2", 24 | "react-email": "^4.0.7" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/email/readme.md: -------------------------------------------------------------------------------- 1 | # React Email Starter 2 | 3 | A live preview right in your browser so you don't need to keep sending real emails during development. 4 | 5 | ## Getting Started 6 | 7 | First, install the dependencies: 8 | 9 | ```sh 10 | npm install 11 | # or 12 | yarn 13 | ``` 14 | 15 | Then, run the development server: 16 | 17 | ```sh 18 | npm run dev 19 | # or 20 | yarn dev 21 | ``` 22 | 23 | Open [localhost:3000](http://localhost:3000) with your browser to see the result. 24 | 25 | ## License 26 | 27 | MIT License 28 | -------------------------------------------------------------------------------- /packages/email/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": false, 4 | "jsx": "react-jsx", 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "strictNullChecks": true, 9 | "paths": { 10 | "@/*": [ 11 | "./src/*" 12 | ], 13 | "@neostack/constants": [ 14 | "../../packages/constants/src/index.ts" 15 | ] 16 | }, 17 | }, 18 | "include": ["**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules", ".react-email"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/typescript-config/README.md: -------------------------------------------------------------------------------- 1 | # `@workspace/typescript-config` 2 | 3 | Shared typescript configuration for the workspace. 4 | -------------------------------------------------------------------------------- /packages/typescript-config/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | 5 | "compilerOptions": { 6 | "target": "ESNext", 7 | "module": "ESNext", 8 | "moduleResolution": "Bundler", 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "lib": ["ESNext"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/typescript-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neostack/typescript-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "PROPRIETARY", 6 | "publishConfig": { 7 | "access": "public" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/typescript-config/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "React Library", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "jsx": "react-jsx", 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/ui/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true 11 | }, 12 | "iconLibrary": "lucide", 13 | "aliases": { 14 | "components": "@neostack/ui/components", 15 | "utils": "@neostack/ui/lib/utils", 16 | "hooks": "@neostack/ui/hooks", 17 | "lib": "@neostack/ui/lib", 18 | "ui": "@neostack/ui/components" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@neostack/ui", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "private": true, 6 | "exports": { 7 | "./globals.css": "./src/styles/globals.css", 8 | "./lib/*": "./src/lib/*.ts", 9 | "./components/*": "./src/components/*.tsx", 10 | "./hooks/*": "./src/hooks/*.ts" 11 | }, 12 | "scripts": {}, 13 | "dependencies": { 14 | "@hookform/resolvers": "^5.0.1", 15 | "@radix-ui/react-accordion": "^1.2.4", 16 | "@radix-ui/react-alert-dialog": "^1.1.7", 17 | "@radix-ui/react-aspect-ratio": "^1.1.3", 18 | "@radix-ui/react-avatar": "^1.1.4", 19 | "@radix-ui/react-checkbox": "^1.1.5", 20 | "@radix-ui/react-collapsible": "^1.1.4", 21 | "@radix-ui/react-context-menu": "^2.2.7", 22 | "@radix-ui/react-dialog": "^1.1.7", 23 | "@radix-ui/react-dropdown-menu": "^2.1.7", 24 | "@radix-ui/react-hover-card": "^1.1.7", 25 | "@radix-ui/react-label": "^2.1.3", 26 | "@radix-ui/react-menubar": "^1.1.7", 27 | "@radix-ui/react-navigation-menu": "^1.2.6", 28 | "@radix-ui/react-popover": "^1.1.7", 29 | "@radix-ui/react-progress": "^1.1.3", 30 | "@radix-ui/react-radio-group": "^1.2.4", 31 | "@radix-ui/react-scroll-area": "^1.2.4", 32 | "@radix-ui/react-select": "^2.1.7", 33 | "@radix-ui/react-separator": "^1.1.3", 34 | "@radix-ui/react-slider": "^1.2.4", 35 | "@radix-ui/react-slot": "^1.1.2", 36 | "@radix-ui/react-switch": "^1.1.4", 37 | "@radix-ui/react-tabs": "^1.1.4", 38 | "@radix-ui/react-toggle": "^1.1.3", 39 | "@radix-ui/react-toggle-group": "^1.1.3", 40 | "@radix-ui/react-tooltip": "^1.2.0", 41 | "class-variance-authority": "^0.7.1", 42 | "clsx": "^2.1.1", 43 | "cmdk": "^1.1.1", 44 | "date-fns": "^4.1.0", 45 | "embla-carousel-react": "^8.6.0", 46 | "input-otp": "^1.4.2", 47 | "lucide-react": "^0.475.0", 48 | "motion": "^12.7.4", 49 | "next-themes": "^0.4.4", 50 | "react": "^19.1.0", 51 | "react-day-picker": "8.10.1", 52 | "react-dom": "^19.1.0", 53 | "react-hook-form": "^7.55.0", 54 | "react-resizable-panels": "^2.1.7", 55 | "recharts": "^2.15.2", 56 | "sonner": "^2.0.3", 57 | "tailwind-merge": "^3.0.1", 58 | "tw-animate-css": "^1.2.4", 59 | "vaul": "^1.1.2", 60 | "zod": "^3.24.2" 61 | }, 62 | "devDependencies": { 63 | "@neostack/typescript-config": "workspace:*", 64 | "@tailwindcss/postcss": "^4.0.8", 65 | "@turbo/gen": "^2.4.2", 66 | "@types/node": "^20", 67 | "@types/react": "^19.1.1", 68 | "@types/react-dom": "^19.1.2", 69 | "tailwindcss": "^4.0.8", 70 | "typescript": "^5.7.3" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/ui/src/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeoPrint3D/neostack/ce5ead93abea7ac0f90193eaabd4252cbc6f244c/packages/ui/src/components/.gitkeep -------------------------------------------------------------------------------- /packages/ui/src/components/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"; 5 | import { ChevronDownIcon } from "lucide-react"; 6 | 7 | import { cn } from "@neostack/ui/lib/utils"; 8 | 9 | function Accordion({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return ; 13 | } 14 | 15 | function AccordionItem({ 16 | className, 17 | ...props 18 | }: React.ComponentProps) { 19 | return ( 20 | 25 | ); 26 | } 27 | 28 | function AccordionTrigger({ 29 | className, 30 | children, 31 | ...props 32 | }: React.ComponentProps) { 33 | return ( 34 | 35 | svg]:rotate-180", 39 | className 40 | )} 41 | {...props} 42 | > 43 | {children} 44 | 45 | 46 | 47 | ); 48 | } 49 | 50 | function AccordionContent({ 51 | className, 52 | children, 53 | ...props 54 | }: React.ComponentProps) { 55 | return ( 56 | 61 |
{children}
62 |
63 | ); 64 | } 65 | 66 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; 67 | -------------------------------------------------------------------------------- /packages/ui/src/components/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; 5 | 6 | import { cn } from "@neostack/ui/lib/utils"; 7 | import { buttonVariants } from "@neostack/ui/components/button"; 8 | 9 | function AlertDialog({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return ; 13 | } 14 | 15 | function AlertDialogTrigger({ 16 | ...props 17 | }: React.ComponentProps) { 18 | return ( 19 | 20 | ); 21 | } 22 | 23 | function AlertDialogPortal({ 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 28 | ); 29 | } 30 | 31 | function AlertDialogOverlay({ 32 | className, 33 | ...props 34 | }: React.ComponentProps) { 35 | return ( 36 | 44 | ); 45 | } 46 | 47 | function AlertDialogContent({ 48 | className, 49 | ...props 50 | }: React.ComponentProps) { 51 | return ( 52 | 53 | 54 | 62 | 63 | ); 64 | } 65 | 66 | function AlertDialogHeader({ 67 | className, 68 | ...props 69 | }: React.ComponentProps<"div">) { 70 | return ( 71 |
76 | ); 77 | } 78 | 79 | function AlertDialogFooter({ 80 | className, 81 | ...props 82 | }: React.ComponentProps<"div">) { 83 | return ( 84 |
92 | ); 93 | } 94 | 95 | function AlertDialogTitle({ 96 | className, 97 | ...props 98 | }: React.ComponentProps) { 99 | return ( 100 | 105 | ); 106 | } 107 | 108 | function AlertDialogDescription({ 109 | className, 110 | ...props 111 | }: React.ComponentProps) { 112 | return ( 113 | 118 | ); 119 | } 120 | 121 | function AlertDialogAction({ 122 | className, 123 | ...props 124 | }: React.ComponentProps) { 125 | return ( 126 | 130 | ); 131 | } 132 | 133 | function AlertDialogCancel({ 134 | className, 135 | ...props 136 | }: React.ComponentProps) { 137 | return ( 138 | 142 | ); 143 | } 144 | 145 | export { 146 | AlertDialog, 147 | AlertDialogPortal, 148 | AlertDialogOverlay, 149 | AlertDialogTrigger, 150 | AlertDialogContent, 151 | AlertDialogHeader, 152 | AlertDialogFooter, 153 | AlertDialogTitle, 154 | AlertDialogDescription, 155 | AlertDialogAction, 156 | AlertDialogCancel, 157 | }; 158 | -------------------------------------------------------------------------------- /packages/ui/src/components/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "@neostack/ui/lib/utils"; 5 | 6 | const alertVariants = cva( 7 | "relative items-start gap-y-0.5 has-[>svg]:gap-x-3 grid grid-cols-[0_1fr] has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] px-4 py-3 border rounded-lg w-full [&>svg]:size-4 [&>svg]:text-current text-sm [&>svg]:translate-y-0.5", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-card text-card-foreground", 12 | destructive: 13 | "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ); 21 | 22 | function Alert({ 23 | className, 24 | variant, 25 | ...props 26 | }: React.ComponentProps<"div"> & VariantProps) { 27 | return ( 28 |
34 | ); 35 | } 36 | 37 | function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { 38 | return ( 39 |
47 | ); 48 | } 49 | 50 | function AlertDescription({ 51 | className, 52 | ...props 53 | }: React.ComponentProps<"div">) { 54 | return ( 55 |
63 | ); 64 | } 65 | 66 | export { Alert, AlertTitle, AlertDescription }; 67 | -------------------------------------------------------------------------------- /packages/ui/src/components/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | function AspectRatio({ 6 | ...props 7 | }: React.ComponentProps) { 8 | return 9 | } 10 | 11 | export { AspectRatio } 12 | -------------------------------------------------------------------------------- /packages/ui/src/components/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 5 | 6 | import { cn } from "@neostack/ui/lib/utils"; 7 | 8 | function Avatar({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ); 22 | } 23 | 24 | function AvatarImage({ 25 | className, 26 | ...props 27 | }: React.ComponentProps) { 28 | return ( 29 | 34 | ); 35 | } 36 | 37 | function AvatarFallback({ 38 | className, 39 | ...props 40 | }: React.ComponentProps) { 41 | return ( 42 | 50 | ); 51 | } 52 | 53 | export { Avatar, AvatarImage, AvatarFallback }; 54 | -------------------------------------------------------------------------------- /packages/ui/src/components/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@neostack/ui/lib/utils"; 6 | 7 | const badgeVariants = cva( 8 | "inline-flex justify-center items-center gap-1 px-2 py-0.5 border aria-invalid:border-destructive focus-visible:border-ring rounded-md aria-invalid:ring-destructive/20 focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:aria-invalid:ring-destructive/40 w-fit [&>svg]:size-3 overflow-hidden font-medium text-xs whitespace-nowrap transition-[color,box-shadow] [&>svg]:pointer-events-none shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", 14 | secondary: 15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", 16 | destructive: 17 | "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 18 | outline: 19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", 20 | }, 21 | }, 22 | defaultVariants: { 23 | variant: "default", 24 | }, 25 | } 26 | ); 27 | 28 | function Badge({ 29 | className, 30 | variant, 31 | asChild = false, 32 | ...props 33 | }: React.ComponentProps<"span"> & 34 | VariantProps & { asChild?: boolean }) { 35 | const Comp = asChild ? Slot : "span"; 36 | 37 | return ( 38 | 43 | ); 44 | } 45 | 46 | export { Badge, badgeVariants }; 47 | -------------------------------------------------------------------------------- /packages/ui/src/components/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { ChevronRight, MoreHorizontal } from "lucide-react"; 4 | 5 | import { cn } from "@neostack/ui/lib/utils"; 6 | 7 | function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { 8 | return