socket io stuff entered

This commit is contained in:
2026-03-12 15:05:37 -05:00
parent bf7d765989
commit 81dc575b4f
44 changed files with 234 additions and 6895 deletions

View File

@@ -10,6 +10,7 @@
"\tmessage: \"${5:Failed to connect to the prod sql server.}\",", "\tmessage: \"${5:Failed to connect to the prod sql server.}\",",
"\tdata: ${6:[]},", "\tdata: ${6:[]},",
"\tnotify: ${7:false},", "\tnotify: ${7:false},",
"\troom: ${8:''},",
"});" "});"
], ],
"description": "Insert a returnFunc template" "description": "Insert a returnFunc template"

View File

@@ -62,6 +62,7 @@
"opendock", "opendock",
"opendocks", "opendocks",
"ppoo", "ppoo",
"preseed",
"prodlabels", "prodlabels",
"prolink", "prolink",
"trycatch" "trycatch"

View File

@@ -1,7 +1,9 @@
import { Writable } from "node:stream"; import { Writable } from "node:stream";
import pino, { type Logger } from "pino"; import pino, { type Logger } from "pino";
import { db } from "../db/db.controller.js"; import { db } from "../db/db.controller.js";
import { logs } from "../db/schema/logs.schema.js"; import { logs } from "../db/schema/logs.schema.js";
import { emitToRoom } from "../socket.io/roomEmitter.socket.js";
import { tryCatch } from "../utils/trycatch.utils.js"; import { tryCatch } from "../utils/trycatch.utils.js";
//import build from "pino-abstract-transport"; //import build from "pino-abstract-transport";
@@ -40,6 +42,10 @@ const dbStream = new Writable({
console.error(res.error); console.error(res.error);
} }
if (obj.room) {
emitToRoom(obj.room, obj);
}
emitToRoom("logs", obj);
callback(); callback();
} catch (err) { } catch (err) {
console.error("DB log insert error:", err); console.error("DB log insert error:", err);
@@ -48,9 +54,14 @@ const dbStream = new Writable({
}, },
}); });
// ✅ Multistream setup const rootLogger: Logger = pino(
const streams = [
{ {
level: logLevel,
redact: { paths: ["email", "password"], remove: true },
},
pino.multistream([
{
level: logLevel,
stream: pino.transport({ stream: pino.transport({
target: "pino-pretty", target: "pino-pretty",
options: { options: {
@@ -60,19 +71,17 @@ const streams = [
}), }),
}, },
{ {
level: "info", level: logLevel,
stream: dbStream, stream: dbStream,
}, },
]; ]),
const rootLogger: Logger = pino(
{
level: logLevel,
redact: { paths: ["email", "password"], remove: true },
},
pino.multistream(streams),
); );
/**
*
*
* example data to put in as a reference
* rooms logs | labels | etc
*/
export const createLogger = (bindings: Record<string, unknown>): Logger => { export const createLogger = (bindings: Record<string, unknown>): Logger => {
return rootLogger.child(bindings); return rootLogger.child(bindings);
}; };

View File

@@ -84,7 +84,7 @@ const postRelease = async (release: Releases) => {
loadTypeId: process.env.DEFAULT_LOAD_TYPE, // well get this and make it a default one loadTypeId: process.env.DEFAULT_LOAD_TYPE, // well get this and make it a default one
dockId: process.env.DEFAULT_DOCK, // this the warehouse we want it in to start out dockId: process.env.DEFAULT_DOCK, // this the warehouse we want it in to start out
refNumbers: [release.ReleaseNumber], refNumbers: [release.ReleaseNumber],
refNumber: release.ReleaseNumber, //refNumber: release.ReleaseNumber,
start: release.DeliveryDate, start: release.DeliveryDate,
end: addHours(release.DeliveryDate, 1), end: addHours(release.DeliveryDate, 1),
notes: "", notes: "",
@@ -202,14 +202,17 @@ const postRelease = async (release: Releases) => {
log.info({}, `${release.ReleaseNumber} was updated`); log.info({}, `${release.ReleaseNumber} was updated`);
} catch (e) { } catch (e) {
log.error({ error: e }, "Error updating the release"); log.error(
{ error: e },
`Error updating the release: ${release.ReleaseNumber}`,
);
} }
// biome-ignore lint/suspicious/noExplicitAny: to many possibilities // biome-ignore lint/suspicious/noExplicitAny: to many possibilities
} catch (e: any) { } catch (e: any) {
//console.info(newDockApt); //console.info(newDockApt);
log.error( log.error(
{ error: e.response.data }, { error: e.response.data },
"An error has occurred during patching of the release", `An error has occurred during patching of the release: ${release.ReleaseNumber}`,
); );
return; return;

View File

@@ -15,6 +15,12 @@ import { createCronJob } from "./utils/croner.utils.js";
const port = Number(process.env.PORT) || 3000; const port = Number(process.env.PORT) || 3000;
export let systemSettings: Setting[] = []; export let systemSettings: Setting[] = [];
const start = async () => { const start = async () => {
const { app, baseUrl } = await createApp();
const server = createServer(app);
setupSocketIORoutes(baseUrl, server);
const log = createLogger({ module: "system", subModule: "main start" }); const log = createLogger({ module: "system", subModule: "main start" });
// triggering long lived processes // triggering long lived processes
@@ -25,6 +31,7 @@ const start = async () => {
systemSettings = await db.select().from(settings); systemSettings = await db.select().from(settings);
//when starting up long lived features the name must match the setting name. //when starting up long lived features the name must match the setting name.
// also we always want to have long lived processes inside a setting check.
setTimeout(() => { setTimeout(() => {
if (systemSettings.filter((n) => n.name === "opendock_sync")[0]?.active) { if (systemSettings.filter((n) => n.name === "opendock_sync")[0]?.active) {
log.info({}, "Opendock is not active"); log.info({}, "Opendock is not active");
@@ -35,19 +42,13 @@ const start = async () => {
); );
} }
// cleanup sql jobs // these jobs below are system jobs and should run no matter what.
createCronJob("JobAuditLogCleanUp", "0 0 5 * * *", () => createCronJob("JobAuditLogCleanUp", "0 0 5 * * *", () =>
dbCleanup("jobs", 30), dbCleanup("jobs", 30),
); );
createCronJob("logsCleanup", "0 15 5 * * *", () => dbCleanup("logs", 120)); createCronJob("logsCleanup", "0 15 5 * * *", () => dbCleanup("logs", 120));
}, 5 * 1000); }, 5 * 1000);
const { app, baseUrl } = await createApp();
const server = createServer(app);
setupSocketIORoutes(baseUrl, server);
server.listen(port, async () => { server.listen(port, async () => {
log.info( log.info(
`Listening on http://${os.hostname()}:${port}${baseUrl}, logging in ${process.env.LOG_LEVEL}, current ENV ${process.env.NODE_ENV ? process.env.NODE_ENV : "development"}`, `Listening on http://${os.hostname()}:${port}${baseUrl}, logging in ${process.env.LOG_LEVEL}, current ENV ${process.env.NODE_ENV ? process.env.NODE_ENV : "development"}`,

View File

@@ -0,0 +1,8 @@
import type { RoomId } from "./types.socket.js";
export const MAX_HISTORY = 20;
export const FLUSH_INTERVAL = 100; // 50ms change higher if needed
export const roomHistory = new Map<RoomId, unknown[]>();
export const roomBuffers = new Map<RoomId, any[]>();
export const roomFlushTimers = new Map<RoomId, NodeJS.Timeout>();

View File

@@ -0,0 +1,33 @@
import { logs } from "backend/db/schema/logs.schema.js";
import { desc } from "drizzle-orm";
import { db } from "../db/db.controller.js";
import type { RoomId } from "./types.socket.js";
type RoomDefinition<T = unknown> = {
seed: (limit: number) => Promise<T[]>;
};
export const roomDefinition: Record<RoomId, RoomDefinition> = {
logs: {
seed: async (limit) => {
try {
const rows = await db
.select()
.from(logs)
.orderBy(desc(logs.createdAt))
.limit(limit);
return rows.reverse();
} catch (e) {
console.error("Failed to seed logs:", e);
return [];
}
},
},
labels: {
seed: async (limit) => {
return [];
},
},
};

View File

@@ -0,0 +1,27 @@
// the emitter setup
import type { RoomId } from "./types.socket.js";
let addDataToRoom: ((roomId: RoomId, payload: unknown[]) => void) | null = null;
export const registerEmitter = (
fn: (roomId: RoomId, payload: unknown[]) => void,
) => {
addDataToRoom = fn;
};
export const emitToRoom = (roomId: RoomId, payload: unknown[]) => {
if (!addDataToRoom) {
console.error("Socket emitter not initialized");
return;
}
addDataToRoom(roomId, payload);
};
/*
import { emitToRoom } from "../socket/socketEmitter.js";
// room name
// its payload
emitToRoom("logs", newLogRow);
*/

View File

@@ -0,0 +1,73 @@
import type { Server } from "socket.io";
import { createLogger } from "../logger/logger.controller.js";
import {
FLUSH_INTERVAL,
MAX_HISTORY,
roomBuffers,
roomFlushTimers,
roomHistory,
} from "./roomCache.socket.js";
import { roomDefinition } from "./roomDefinitions.socket.js";
import type { RoomId } from "./types.socket.js";
// get the db data if not exiting already
const log = createLogger({ module: "socket.io", subModule: "roomService" });
export const preseedRoom = async (roomId: RoomId) => {
if (roomHistory.has(roomId)) {
return roomHistory.get(roomId);
}
const roomDef = roomDefinition[roomId];
if (!roomDef) {
log.error({}, `Room ${roomId} is not defined`);
}
const latestData = await roomDef.seed(MAX_HISTORY);
roomHistory.set(roomId, latestData);
return latestData;
};
export const createRoomEmitter = (io: Server) => {
const addDataToRoom = <T>(roomId: RoomId, payload: T) => {
if (!roomHistory.has(roomId)) {
roomHistory.set(roomId, []);
}
const history = roomHistory.get(roomId)!;
history?.push(payload);
if (history?.length > MAX_HISTORY) {
history?.shift();
}
if (!roomBuffers.has(roomId)) {
roomBuffers.set(roomId, []);
}
roomBuffers.get(roomId)!.push(payload);
if (!roomFlushTimers.has(roomId)) {
const timer = setTimeout(() => {
const buffered = roomBuffers.get(roomId) || [];
if (buffered.length > 0) {
io.to(roomId).emit("room-update", {
roomId,
payloads: buffered, // ✅ array now
});
}
roomBuffers.set(roomId, []);
roomFlushTimers.delete(roomId);
}, FLUSH_INTERVAL);
roomFlushTimers.set(roomId, timer);
}
};
return { addDataToRoom };
};

View File

@@ -3,9 +3,13 @@ import type { Server as HttpServer } from "node:http";
//import { fileURLToPath } from "node:url"; //import { fileURLToPath } from "node:url";
import { instrument } from "@socket.io/admin-ui"; import { instrument } from "@socket.io/admin-ui";
import { Server } from "socket.io"; import { Server } from "socket.io";
import { createLogger } from "../logger/logger.controller.js";
import { registerEmitter } from "./roomEmitter.socket.js";
import { createRoomEmitter, preseedRoom } from "./roomService.socket.js";
//const __filename = fileURLToPath(import.meta.url); //const __filename = fileURLToPath(import.meta.url);
//const __dirname = dirname(__filename); //const __dirname = dirname(__filename);
const log = createLogger({ module: "socket.io", subModule: "setup" });
export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => { export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => {
const io = new Server(server, { const io = new Server(server, {
@@ -16,29 +20,49 @@ export const setupSocketIORoutes = (baseUrl: string, server: HttpServer) => {
}, },
}); });
// ✅ Create emitter instance
const { addDataToRoom } = createRoomEmitter(io);
registerEmitter(addDataToRoom);
io.on("connection", (s) => { io.on("connection", (s) => {
console.info(s.id); log.info({}, `User connected: ${s.id}`);
s.emit("welcome", {
serverTime: Date.now(),
availableRooms: ["logs", "labels"],
version: "1.0.0",
}); });
// admin stuff for socket io s.on("join-room", async (rn) => {
// app.use( s.join(rn);
// express.static(
// join(__dirname, "../../../node_modules/@socket.io/admin-ui/dist"), // get room seeded
// ), const history = await preseedRoom(rn);
// );
// send the intial data
s.emit("room-update", {
roomId: rn,
payloads: history,
initial: true,
});
});
});
io.on("disconnect", (s) => {
log.info({}, "User disconnected:", s.id);
});
// admin stuff
// app.get(baseUrl + "/admindashboard", (_, res) => {
// res.sendFile(
// join(
// __dirname,
// "../../../node_modules/@socket.io/admin-ui/dist/index.js",
// ),
// );
// });
const admin = io.of("/admin"); const admin = io.of("/admin");
admin.on("connection", () => { admin.on("connection", (s) => {
console.info("Connected to admin userspace"); log.info({}, `User connected: ${s.id}`);
}); });
admin.on("disconnect", (s) => {
log.info({}, "User disconnected:", s.id);
});
instrument(io, { instrument(io, {
auth: false, auth: false,
//namespaceName: "/admin", //namespaceName: "/admin",

View File

@@ -0,0 +1 @@
export type RoomId = "logs" | "labels"; //| "alerts" | "metrics";

View File

@@ -24,6 +24,7 @@ export const featureControl = async (data: Setting) => {
stopCronJob(data.name); stopCronJob(data.name);
} }
// specific setting stuff should have handled like below. what needs turned back on or off.
if (data.name === "opendock_sync" && data.active) { if (data.name === "opendock_sync" && data.active) {
opendockSocketMonitor(); opendockSocketMonitor();
monitorReleaseChanges(); monitorReleaseChanges();

View File

@@ -18,6 +18,7 @@ interface Data<T = unknown[]> {
| "settings"; | "settings";
level: "info" | "error" | "debug" | "fatal"; level: "info" | "error" | "debug" | "fatal";
message: string; message: string;
room?: string;
data?: T; data?: T;
notify?: boolean; notify?: boolean;
} }
@@ -38,20 +39,21 @@ interface Data<T = unknown[]> {
*/ */
export const returnFunc = (data: Data) => { export const returnFunc = (data: Data) => {
const notify = data.notify ? data.notify : false; const notify = data.notify ? data.notify : false;
const room = data.room ?? data.room;
const log = createLogger({ module: data.module, subModule: data.subModule }); const log = createLogger({ module: data.module, subModule: data.subModule });
// handle the logging part // handle the logging part
switch (data.level) { switch (data.level) {
case "info": case "info":
log.info({ notify: notify }, data.message); log.info({ notify: notify, room }, data.message);
break; break;
case "error": case "error":
log.error({ notify: notify, error: data.data }, data.message); log.error({ notify: notify, error: data.data, room }, data.message);
break; break;
case "debug": case "debug":
log.debug({ notify: notify }, data.message); log.debug({ notify: notify, room }, data.message);
break; break;
case "fatal": case "fatal":
log.fatal({ notify: notify }, data.message); log.fatal({ notify: notify, room }, data.message);
} }
// api section to return // api section to return

View File

@@ -1,20 +0,0 @@
{
"projectName": "frontend",
"mode": "file-router",
"typescript": true,
"packageManager": "npm",
"includeExamples": false,
"tailwind": true,
"addOnOptions": {},
"envVarValues": {},
"git": false,
"routerOnly": false,
"version": 1,
"framework": "react-cra",
"chosenAddOns": [
"biome",
"shadcn",
"better-auth",
"tanstack-query"
]
}

View File

@@ -1,7 +0,0 @@
# shadcn instructions
Use the latest version of Shadcn to install new components, like this command to add a button component:
```bash
pnpm dlx shadcn@latest add button
```

13
frontend/.gitignore vendored
View File

@@ -1,13 +0,0 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.env
.nitro
.tanstack
.wrangler
.output
.vinxi
__unconfig*
todos.json

View File

@@ -1,35 +0,0 @@
{
"files.watcherExclude": {
"**/routeTree.gen.ts": true
},
"search.exclude": {
"**/routeTree.gen.ts": true
},
"files.readonlyInclude": {
"**/routeTree.gen.ts": true
},
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[javascriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[jsonc]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[css]": {
"editor.defaultFormatter": "biomejs.biome"
},
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit"
}
}

View File

@@ -1,247 +0,0 @@
Welcome to your new TanStack Start app!
# Getting Started
To run this application:
```bash
npm install
npm run dev
```
# Building For Production
To build this application for production:
```bash
npm run build
```
## Testing
This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with:
```bash
npm run test
```
## Styling
This project uses [Tailwind CSS](https://tailwindcss.com/) for styling.
### Removing Tailwind CSS
If you prefer not to use Tailwind CSS:
1. Remove the demo pages in `src/routes/demo/`
2. Replace the Tailwind import in `src/styles.css` with your own styles
3. Remove `tailwindcss()` from the plugins array in `vite.config.ts`
4. Uninstall the packages: `npm install @tailwindcss/vite tailwindcss -D`
## Linting & Formatting
This project uses [Biome](https://biomejs.dev/) for linting and formatting. The following scripts are available:
```bash
npm run lint
npm run format
npm run check
```
## Shadcn
Add components using the latest version of [Shadcn](https://ui.shadcn.com/).
```bash
pnpm dlx shadcn@latest add button
```
## Setting up Better Auth
1. Generate and set the `BETTER_AUTH_SECRET` environment variable in your `.env.local`:
```bash
npx -y @better-auth/cli secret
```
2. Visit the [Better Auth documentation](https://www.better-auth.com) to unlock the full potential of authentication in your app.
### Adding a Database (Optional)
Better Auth can work in stateless mode, but to persist user data, add a database:
```typescript
// src/lib/auth.ts
import { betterAuth } from "better-auth";
import { Pool } from "pg";
export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL,
}),
// ... rest of config
});
```
Then run migrations:
```bash
npx -y @better-auth/cli migrate
```
## Routing
This project uses [TanStack Router](https://tanstack.com/router) with file-based routing. Routes are managed as files in `src/routes`.
### Adding A Route
To add a new route to your application just add a new file in the `./src/routes` directory.
TanStack will automatically generate the content of the route file for you.
Now that you have two routes you can use a `Link` component to navigate between them.
### Adding Links
To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`.
```tsx
import { Link } from "@tanstack/react-router";
```
Then anywhere in your JSX you can use it like so:
```tsx
<Link to="/about">About</Link>
```
This will create a link that will navigate to the `/about` route.
More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent).
### Using A Layout
In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you render `{children}` in the `shellComponent`.
Here is an example layout that includes a header:
```tsx
import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ title: 'My App' },
],
}),
shellComponent: ({ children }) => (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
<header>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
</header>
{children}
<Scripts />
</body>
</html>
),
})
```
More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts).
## Server Functions
TanStack Start provides server functions that allow you to write server-side code that seamlessly integrates with your client components.
```tsx
import { createServerFn } from '@tanstack/react-start'
const getServerTime = createServerFn({
method: 'GET',
}).handler(async () => {
return new Date().toISOString()
})
// Use in a component
function MyComponent() {
const [time, setTime] = useState('')
useEffect(() => {
getServerTime().then(setTime)
}, [])
return <div>Server time: {time}</div>
}
```
## API Routes
You can create API routes by using the `server` property in your route definitions:
```tsx
import { createFileRoute } from '@tanstack/react-router'
import { json } from '@tanstack/react-start'
export const Route = createFileRoute('/api/hello')({
server: {
handlers: {
GET: () => json({ message: 'Hello, World!' }),
},
},
})
```
## Data Fetching
There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered.
For example:
```tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/people')({
loader: async () => {
const response = await fetch('https://swapi.dev/api/people')
return response.json()
},
component: PeopleComponent,
})
function PeopleComponent() {
const data = Route.useLoaderData()
return (
<ul>
{data.results.map((person) => (
<li key={person.name}>{person.name}</li>
))}
</ul>
)
}
```
Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters).
# Demo files
Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed.
# Learn More
You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com).
For TanStack Start specific documentation, visit [TanStack Start](https://tanstack.com/start).

View File

@@ -1,36 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"includes": [
"**/src/**/*",
"**/.vscode/**/*",
"**/index.html",
"**/vite.config.ts",
"!**/src/routeTree.gen.ts",
"!**/src/styles.css"
]
},
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
}
}

View File

@@ -1,21 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "#/components",
"utils": "#/lib/utils",
"ui": "#/components/ui",
"lib": "#/lib",
"hooks": "#/hooks"
},
"iconLibrary": "lucide"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,52 +0,0 @@
{
"name": "frontend",
"private": true,
"type": "module",
"imports": {
"#/*": "./src/*"
},
"scripts": {
"dev": "vite dev --port 3000",
"build": "vite build",
"preview": "vite preview",
"test": "vitest run",
"format": "biome format",
"lint": "biome lint",
"check": "biome check"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-devtools": "^0.7.0",
"@tanstack/react-query": "^5.66.5",
"@tanstack/react-query-devtools": "^5.84.2",
"@tanstack/react-router": "^1.132.0",
"@tanstack/react-router-devtools": "^1.132.0",
"@tanstack/react-router-ssr-query": "^1.131.7",
"@tanstack/react-start": "^1.132.0",
"@tanstack/router-plugin": "^1.132.0",
"better-auth": "^1.4.12",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.561.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.1.18",
"tw-animate-css": "^1.3.6"
},
"devDependencies": {
"@biomejs/biome": "2.2.4",
"@tanstack/devtools-vite": "^0.3.11",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/node": "^22.10.2",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^5.0.4",
"jsdom": "^27.0.0",
"typescript": "^5.7.2",
"vite": "^7.1.7",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.5"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -1,25 +0,0 @@
{
"short_name": "TanStack App",
"name": "Create TanStack App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,72 +0,0 @@
import { Link } from "@tanstack/react-router";
import { Home, Menu, X } from "lucide-react";
import { useState } from "react";
import BetterAuthHeader from "../integrations/better-auth/header-user.tsx";
export default function Header() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<header className="p-4 flex items-center bg-gray-800 text-white shadow-lg">
<button
onClick={() => setIsOpen(true)}
className="p-2 hover:bg-gray-700 rounded-lg transition-colors"
aria-label="Open menu"
>
<Menu size={24} />
</button>
<h1 className="ml-4 text-xl font-semibold">
<Link to="/">
<img
src="/tanstack-word-logo-white.svg"
alt="TanStack Logo"
className="h-10"
/>
</Link>
</h1>
</header>
<aside
className={`fixed top-0 left-0 h-full w-80 bg-gray-900 text-white shadow-2xl z-50 transform transition-transform duration-300 ease-in-out flex flex-col ${
isOpen ? "translate-x-0" : "-translate-x-full"
}`}
>
<div className="flex items-center justify-between p-4 border-b border-gray-700">
<h2 className="text-xl font-bold">Navigation</h2>
<button
onClick={() => setIsOpen(false)}
className="p-2 hover:bg-gray-800 rounded-lg transition-colors"
aria-label="Close menu"
>
<X size={24} />
</button>
</div>
<nav className="flex-1 p-4 overflow-y-auto">
<Link
to="/"
onClick={() => setIsOpen(false)}
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
activeProps={{
className:
"flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2",
}}
>
<Home size={20} />
<span className="font-medium">Home</span>
</Link>
{/* Demo Links Start */}
{/* Demo Links End */}
</nav>
<div className="p-4 border-t border-gray-700 bg-gray-800 flex flex-col gap-2">
<BetterAuthHeader />
</div>
</aside>
</>
);
}

View File

@@ -1,45 +0,0 @@
import { authClient } from '#/lib/auth-client'
import { Link } from '@tanstack/react-router'
export default function BetterAuthHeader() {
const { data: session, isPending } = authClient.useSession()
if (isPending) {
return (
<div className="h-8 w-8 bg-neutral-100 dark:bg-neutral-800 animate-pulse" />
)
}
if (session?.user) {
return (
<div className="flex items-center gap-2">
{session.user.image ? (
<img src={session.user.image} alt="" className="h-8 w-8" />
) : (
<div className="h-8 w-8 bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center">
<span className="text-xs font-medium text-neutral-600 dark:text-neutral-400">
{session.user.name?.charAt(0).toUpperCase() || 'U'}
</span>
</div>
)}
<button
onClick={() => {
void authClient.signOut()
}}
className="flex-1 h-9 px-4 text-sm font-medium bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-50 border border-neutral-300 dark:border-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors"
>
Sign out
</button>
</div>
)
}
return (
<Link
to="/demo/better-auth"
className="h-9 px-4 text-sm font-medium bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-50 border border-neutral-300 dark:border-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors inline-flex items-center"
>
Sign in
</Link>
)
}

View File

@@ -1,6 +0,0 @@
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
export default {
name: 'Tanstack Query',
render: <ReactQueryDevtoolsPanel />,
}

View File

@@ -1,34 +0,0 @@
import type { ReactNode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
let context:
| {
queryClient: QueryClient
}
| undefined
export function getContext() {
if (context) {
return context
}
const queryClient = new QueryClient()
context = {
queryClient,
}
return context
}
export default function TanStackQueryProvider({
children,
}: {
children: ReactNode
}) {
const { queryClient } = getContext()
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}

View File

@@ -1,3 +0,0 @@
import { createAuthClient } from 'better-auth/react'
export const authClient = createAuthClient()

View File

@@ -1,9 +0,0 @@
import { betterAuth } from 'better-auth'
import { tanstackStartCookies } from 'better-auth/tanstack-start'
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
},
plugins: [tanstackStartCookies()],
})

View File

@@ -1,7 +0,0 @@
import type { ClassValue } from 'clsx'
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -1,86 +0,0 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
id: '/api/auth/$',
path: '/api/auth/$',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/api/auth/$': typeof ApiAuthSplatRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/api/auth/$': typeof ApiAuthSplatRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/api/auth/$': typeof ApiAuthSplatRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/api/auth/$'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/api/auth/$'
id: '__root__' | '/' | '/api/auth/$'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/api/auth/$': {
id: '/api/auth/$'
path: '/api/auth/$'
fullPath: '/api/auth/$'
preLoaderRoute: typeof ApiAuthSplatRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
ApiAuthSplatRoute: ApiAuthSplatRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}

View File

@@ -1,23 +0,0 @@
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
import { getContext } from "./integrations/tanstack-query/root-provider";
import { routeTree } from "./routeTree.gen.ts";
export function getRouter() {
const router = createTanStackRouter({
routeTree,
context: getContext(),
scrollRestoration: true,
defaultPreload: "intent",
defaultPreloadStaleTime: 0,
});
return router;
}
declare module "@tanstack/react-router" {
interface Register {
router: ReturnType<typeof getRouter>;
}
}

View File

@@ -1,74 +0,0 @@
import {
HeadContent,
Scripts,
createRootRouteWithContext,
} from '@tanstack/react-router'
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'
import Header from '../components/Header'
import TanStackQueryProvider from '../integrations/tanstack-query/root-provider'
import TanStackQueryDevtools from '../integrations/tanstack-query/devtools'
import appCss from '../styles.css?url'
import type { QueryClient } from '@tanstack/react-query'
interface MyRouterContext {
queryClient: QueryClient
}
export const Route = createRootRouteWithContext<MyRouterContext>()({
head: () => ({
meta: [
{
charSet: 'utf-8',
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
},
{
title: 'TanStack Start Starter',
},
],
links: [
{
rel: 'stylesheet',
href: appCss,
},
],
}),
shellComponent: RootDocument,
})
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
<TanStackQueryProvider>
<Header />
{children}
<TanStackDevtools
config={{
position: 'bottom-right',
}}
plugins={[
{
name: 'Tanstack Router',
render: <TanStackRouterDevtoolsPanel />,
},
TanStackQueryDevtools,
]}
/>
</TanStackQueryProvider>
<Scripts />
</body>
</html>
)
}

View File

@@ -1,11 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
import { auth } from '#/lib/auth'
export const Route = createFileRoute('/api/auth/$')({
server: {
handlers: {
GET: ({ request }) => auth.handler(request),
POST: ({ request }) => auth.handler(request),
},
},
})

View File

@@ -1,58 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import {
Route as RouteIcon,
Server,
Shield,
Sparkles,
Waves,
Zap,
} from "lucide-react";
export const Route = createFileRoute("/")({ component: App });
function App() {
const features = [
{
icon: <Zap className="w-12 h-12 text-cyan-400" />,
title: "Powerful Server Functions",
description:
"Write server-side code that seamlessly integrates with your client components. Type-safe, secure, and simple.",
},
{
icon: <Server className="w-12 h-12 text-cyan-400" />,
title: "Flexible Server Side Rendering",
description:
"Full-document SSR, streaming, and progressive enhancement out of the box. Control exactly what renders where.",
},
{
icon: <RouteIcon className="w-12 h-12 text-cyan-400" />,
title: "API Routes",
description:
"Build type-safe API endpoints alongside your application. No separate backend needed.",
},
{
icon: <Shield className="w-12 h-12 text-cyan-400" />,
title: "Strongly Typed Everything",
description:
"End-to-end type safety from server to client. Catch errors before they reach production.",
},
{
icon: <Waves className="w-12 h-12 text-cyan-400" />,
title: "Full Streaming Support",
description:
"Stream data from server to client progressively. Perfect for AI applications and real-time updates.",
},
{
icon: <Sparkles className="w-12 h-12 text-cyan-400" />,
title: "Next Generation Ready",
description:
"Built from the ground up for modern web applications. Deploy anywhere JavaScript runs.",
},
];
return (
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900">
<h1>Just a place for a guy to hang out</h1>
</div>
);
}

View File

@@ -1,138 +0,0 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
body {
@apply m-0;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family:
source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.871 0.006 286.286);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.871 0.006 286.286);
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.141 0.005 285.823);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.141 0.005 285.823);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.274 0.006 286.033);
--input: oklch(0.274 0.006 286.033);
--ring: oklch(0.442 0.017 285.786);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.274 0.006 286.033);
--sidebar-ring: oklch(0.442 0.017 285.786);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -1,28 +0,0 @@
{
"include": ["**/*.ts", "**/*.tsx"],
"compilerOptions": {
"target": "ES2022",
"jsx": "react-jsx",
"module": "ESNext",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
/* Linting */
"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
}
}

View File

@@ -1,20 +0,0 @@
import { defineConfig } from 'vite'
import { devtools } from '@tanstack/devtools-vite'
import tsconfigPaths from 'vite-tsconfig-paths'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
const config = defineConfig({
plugins: [
devtools(),
tsconfigPaths({ projects: ['./tsconfig.json'] }),
tailwindcss(),
tanstackStart(),
viteReact(),
],
})
export default config