enh
This commit is contained in:
parent
dc19233ab0
commit
7c9fb6b060
67 changed files with 10072 additions and 0 deletions
13
README.md
13
README.md
|
|
@ -1,2 +1,15 @@
|
|||
# health-petal
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.2.10. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
||||
|
|
|
|||
34
apps/backend/.gitignore
vendored
Normal file
34
apps/backend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
15
apps/backend/README.md
Normal file
15
apps/backend/README.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# backend
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.2.10. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
||||
BIN
apps/backend/dev.db
Normal file
BIN
apps/backend/dev.db
Normal file
Binary file not shown.
21
apps/backend/package.json
Normal file
21
apps/backend/package.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "backend",
|
||||
"module": "index.ts",
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "bun --watch src/index.ts"
|
||||
},
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@trpc/server": "^11.6.0",
|
||||
"data-manager-sqlite": "*",
|
||||
"hono": "^4.12.18",
|
||||
"zod": "^3.25.0"
|
||||
}
|
||||
}
|
||||
43
apps/backend/src/index.ts
Normal file
43
apps/backend/src/index.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { Hono } from 'hono'
|
||||
import { cors } from 'hono/cors'
|
||||
import { env } from './lib/env-exporter'
|
||||
import { appRouter } from './trpc/router'
|
||||
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.use(
|
||||
'*',
|
||||
cors({
|
||||
origin: (origin) => {
|
||||
// Allow local dev UIs.
|
||||
if (origin === 'http://localhost:3000') return origin
|
||||
if (origin === 'http://localhost:3001') return origin
|
||||
// Non-browser clients (no Origin header)
|
||||
if (!origin) return '*'
|
||||
return null
|
||||
},
|
||||
allowHeaders: ['content-type'],
|
||||
allowMethods: ['GET', 'POST', 'OPTIONS'],
|
||||
}),
|
||||
)
|
||||
|
||||
app.get('/health', (c) => c.json({ ok: true }))
|
||||
|
||||
app.all('/trpc/*', (c) =>
|
||||
fetchRequestHandler({
|
||||
endpoint: '/trpc',
|
||||
req: c.req.raw,
|
||||
router: appRouter,
|
||||
}),
|
||||
)
|
||||
|
||||
export { app }
|
||||
|
||||
const port = Number(env.PORT || 4004)
|
||||
Bun.serve({
|
||||
port,
|
||||
fetch: app.fetch,
|
||||
})
|
||||
|
||||
console.log(`Backend listening on http://localhost:${port}`)
|
||||
5
apps/backend/src/lib/data-manager-instance.ts
Normal file
5
apps/backend/src/lib/data-manager-instance.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { env } from './env-exporter'
|
||||
import { DataManager } from './data-manager'
|
||||
|
||||
// Singleton container for all backend data access.
|
||||
export const dataManager = new DataManager(env.SQLITE_PATH || 'dev.db')
|
||||
28
apps/backend/src/lib/data-manager.ts
Normal file
28
apps/backend/src/lib/data-manager.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import {
|
||||
createStorageSpacesRepo,
|
||||
runMigrations,
|
||||
type StorageSpacesRepo,
|
||||
} from 'data-manager-sqlite'
|
||||
|
||||
import type { StorageSpacesService } from '../trpc/router'
|
||||
|
||||
export class DataManager {
|
||||
readonly storageSpaces: StorageSpacesService
|
||||
readonly close: () => void
|
||||
|
||||
constructor(sqlitePath: string) {
|
||||
const { repo, sqlite, close } = createStorageSpacesRepo({ sqlitePath })
|
||||
runMigrations(sqlite)
|
||||
|
||||
this.close = close
|
||||
|
||||
// Keep the service surface stable for the router.
|
||||
this.storageSpaces = {
|
||||
getStorageSpaces: () => repo.getStorageSpaces(),
|
||||
getStorageSpaceById: (id) => repo.getStorageSpaceById(id),
|
||||
createStorageSpace: (input) => repo.createStorageSpace(input),
|
||||
updateStorageSpace: (id, patch) => repo.updateStorageSpace(id, patch),
|
||||
deleteStorageSpace: (id) => repo.deleteStorageSpace(id),
|
||||
}
|
||||
}
|
||||
}
|
||||
4
apps/backend/src/lib/env-exporter.ts
Normal file
4
apps/backend/src/lib/env-exporter.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export const env = {
|
||||
PORT: process.env.PORT || '4004',
|
||||
SQLITE_PATH: process.env.SQLITE_PATH,
|
||||
} as const
|
||||
92
apps/backend/src/trpc/router.ts
Normal file
92
apps/backend/src/trpc/router.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { initTRPC } from '@trpc/server'
|
||||
import { z } from 'zod'
|
||||
import { dataManager } from '../lib/data-manager-instance'
|
||||
|
||||
export const StorageSpaceSchema = z.object({
|
||||
id: z.number().int(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
image_urls: z.array(z.string()),
|
||||
})
|
||||
|
||||
export type StorageSpace = z.infer<typeof StorageSpaceSchema>
|
||||
|
||||
export type StorageSpacesService = {
|
||||
getStorageSpaces: () => Promise<StorageSpace[]>
|
||||
getStorageSpaceById: (id: number) => Promise<StorageSpace | null>
|
||||
createStorageSpace: (input: {
|
||||
name: string
|
||||
description?: string | null
|
||||
image_urls: string[]
|
||||
}) => Promise<StorageSpace>
|
||||
updateStorageSpace: (
|
||||
id: number,
|
||||
patch: {
|
||||
name?: string
|
||||
description?: string | null
|
||||
image_urls?: string[]
|
||||
},
|
||||
) => Promise<StorageSpace | null>
|
||||
deleteStorageSpace: (id: number) => Promise<boolean>
|
||||
}
|
||||
|
||||
const t = initTRPC.create()
|
||||
|
||||
const storageSpacesRouter = t.router({
|
||||
getStorageSpaces: t.procedure.output(z.array(StorageSpaceSchema)).query(() => {
|
||||
return dataManager.storageSpaces.getStorageSpaces()
|
||||
}),
|
||||
|
||||
getStorageSpaceById: t.procedure
|
||||
.input(z.object({ id: z.number().int() }))
|
||||
.output(StorageSpaceSchema.nullable())
|
||||
.query(({ input }) => {
|
||||
return dataManager.storageSpaces.getStorageSpaceById(input.id)
|
||||
}),
|
||||
|
||||
addStorageSpace: t.procedure
|
||||
.input(
|
||||
z.object({
|
||||
name: z.string().min(1),
|
||||
description: z.string().nullable().optional(),
|
||||
image_urls: z.array(z.string()).default([]),
|
||||
}),
|
||||
)
|
||||
.output(StorageSpaceSchema)
|
||||
.mutation(({ input }) => {
|
||||
return dataManager.storageSpaces.createStorageSpace({
|
||||
name: input.name,
|
||||
description: input.description ?? null,
|
||||
image_urls: input.image_urls,
|
||||
})
|
||||
}),
|
||||
|
||||
updateStorageSpace: t.procedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.number().int(),
|
||||
name: z.string().min(1).optional(),
|
||||
description: z.string().nullable().optional(),
|
||||
image_urls: z.array(z.string()).optional(),
|
||||
}),
|
||||
)
|
||||
.output(StorageSpaceSchema.nullable())
|
||||
.mutation(({ input }) => {
|
||||
const { id, ...patch } = input
|
||||
return dataManager.storageSpaces.updateStorageSpace(id, patch)
|
||||
}),
|
||||
|
||||
deleteStorageSpace: t.procedure
|
||||
.input(z.object({ id: z.number().int() }))
|
||||
.output(z.object({ ok: z.boolean() }))
|
||||
.mutation(async ({ input }) => {
|
||||
const ok = await dataManager.storageSpaces.deleteStorageSpace(input.id)
|
||||
return { ok }
|
||||
}),
|
||||
})
|
||||
|
||||
export const appRouter = t.router({
|
||||
storageSpaces: storageSpacesRouter,
|
||||
})
|
||||
|
||||
export type AppRouter = typeof appRouter
|
||||
29
apps/backend/tsconfig.json
Normal file
29
apps/backend/tsconfig.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"types": ["bun"]
|
||||
}
|
||||
}
|
||||
18
apps/pharmanager/.cta.json
Normal file
18
apps/pharmanager/.cta.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"projectName": "pharmanager",
|
||||
"mode": "file-router",
|
||||
"typescript": true,
|
||||
"packageManager": "bun",
|
||||
"includeExamples": false,
|
||||
"tailwind": true,
|
||||
"addOnOptions": {},
|
||||
"envVarValues": {},
|
||||
"git": false,
|
||||
"install": true,
|
||||
"routerOnly": true,
|
||||
"version": 1,
|
||||
"framework": "react",
|
||||
"chosenAddOns": [
|
||||
"biome"
|
||||
]
|
||||
}
|
||||
13
apps/pharmanager/.gitignore
vendored
Normal file
13
apps/pharmanager/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.env
|
||||
.nitro
|
||||
.tanstack
|
||||
.wrangler
|
||||
.output
|
||||
.vinxi
|
||||
__unconfig*
|
||||
todos.json
|
||||
35
apps/pharmanager/.vscode/settings.json
vendored
Normal file
35
apps/pharmanager/.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
204
apps/pharmanager/README.md
Normal file
204
apps/pharmanager/README.md
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
Welcome to your new TanStack Start app!
|
||||
|
||||
# Getting Started
|
||||
|
||||
To run this application:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
bun --bun run dev
|
||||
```
|
||||
|
||||
# Building For Production
|
||||
|
||||
To build this application for production:
|
||||
|
||||
```bash
|
||||
bun --bun run build
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with:
|
||||
|
||||
```bash
|
||||
bun --bun 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: `bun install @tailwindcss/vite tailwindcss -D`
|
||||
|
||||
## Linting & Formatting
|
||||
|
||||
This project uses [Biome](https://biomejs.dev/) for linting and formatting. The following scripts are available:
|
||||
|
||||
|
||||
```bash
|
||||
bun --bun run lint
|
||||
bun --bun run format
|
||||
bun --bun run check
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 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).
|
||||
36
apps/pharmanager/biome.json
Normal file
36
apps/pharmanager/biome.json
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"$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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
apps/pharmanager/index.html
Normal file
13
apps/pharmanager/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>pharmanager</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
52
apps/pharmanager/package.json
Normal file
52
apps/pharmanager/package.json
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"name": "pharmanager",
|
||||
"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": "latest",
|
||||
"@tanstack/react-router": "latest",
|
||||
"@tanstack/react-router-devtools": "latest",
|
||||
"@tanstack/router-plugin": "^1.132.0",
|
||||
"lucide-react": "^0.545.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"shared-react": "*",
|
||||
"tailwindcss": "^4.1.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.4.5",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/devtools-vite": "latest",
|
||||
"@tanstack/router-plugin": "latest",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"jsdom": "^28.1.0",
|
||||
"typescript": "^6.0.2",
|
||||
"vite": "^8.0.0",
|
||||
"vitest": "^4.1.5",
|
||||
"@repo/shared": "*"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild",
|
||||
"lightningcss"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
apps/pharmanager/public/favicon.ico
Normal file
BIN
apps/pharmanager/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
BIN
apps/pharmanager/public/logo192.png
Normal file
BIN
apps/pharmanager/public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
apps/pharmanager/public/logo512.png
Normal file
BIN
apps/pharmanager/public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
25
apps/pharmanager/public/manifest.json
Normal file
25
apps/pharmanager/public/manifest.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
3
apps/pharmanager/public/robots.txt
Normal file
3
apps/pharmanager/public/robots.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
27
apps/pharmanager/src/main.tsx
Normal file
27
apps/pharmanager/src/main.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import ReactDOM from 'react-dom/client'
|
||||
import { RouterProvider, createRouter } from '@tanstack/react-router'
|
||||
import { routeTree } from './routeTree.gen'
|
||||
import { TrpcProvider } from 'shared-react'
|
||||
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
defaultPreload: 'intent',
|
||||
scrollRestoration: true,
|
||||
})
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: typeof router
|
||||
}
|
||||
}
|
||||
|
||||
const rootElement = document.getElementById('app')!
|
||||
|
||||
if (!rootElement.innerHTML) {
|
||||
const root = ReactDOM.createRoot(rootElement)
|
||||
root.render(
|
||||
<TrpcProvider baseUrl="http://localhost:3001">
|
||||
<RouterProvider router={router} />
|
||||
</TrpcProvider>,
|
||||
)
|
||||
}
|
||||
59
apps/pharmanager/src/routeTree.gen.ts
Normal file
59
apps/pharmanager/src/routeTree.gen.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/* 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'
|
||||
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/'
|
||||
id: '__root__' | '/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
19
apps/pharmanager/src/router.tsx
Normal file
19
apps/pharmanager/src/router.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
|
||||
import { routeTree } from './routeTree.gen'
|
||||
|
||||
export function getRouter() {
|
||||
const router = createTanStackRouter({
|
||||
routeTree,
|
||||
scrollRestoration: true,
|
||||
defaultPreload: 'intent',
|
||||
defaultPreloadStaleTime: 0,
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: ReturnType<typeof getRouter>
|
||||
}
|
||||
}
|
||||
28
apps/pharmanager/src/routes/__root.tsx
Normal file
28
apps/pharmanager/src/routes/__root.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { Outlet, createRootRoute } from '@tanstack/react-router'
|
||||
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
|
||||
import { TanStackDevtools } from '@tanstack/react-devtools'
|
||||
|
||||
import '../styles.css'
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: RootComponent,
|
||||
})
|
||||
|
||||
function RootComponent() {
|
||||
return (
|
||||
<>
|
||||
<Outlet />
|
||||
<TanStackDevtools
|
||||
config={{
|
||||
position: 'bottom-right',
|
||||
}}
|
||||
plugins={[
|
||||
{
|
||||
name: 'TanStack Router',
|
||||
render: <TanStackRouterDevtoolsPanel />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
45
apps/pharmanager/src/routes/index.tsx
Normal file
45
apps/pharmanager/src/routes/index.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import type { Person } from '@repo/shared'
|
||||
import { useCounterStore, useGetStorageSpaces } from 'shared-react'
|
||||
|
||||
export const Route = createFileRoute('/')({ component: Home })
|
||||
|
||||
function Home() {
|
||||
const shafi:Person = {age: 32, name: 'Shafi'}
|
||||
const count = useCounterStore((s) => s.count)
|
||||
const inc = useCounterStore((s) => s.inc)
|
||||
const spaces = useGetStorageSpaces()
|
||||
return (
|
||||
<div className="p-8">
|
||||
<h1 className="text-4xl font-bold">Welcome to TanStack Start</h1>
|
||||
<p className="mt-4 text-lg">
|
||||
{shafi.name} is {shafi.age} years old.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-6 rounded bg-black px-4 py-2 text-white"
|
||||
onClick={inc}
|
||||
>
|
||||
Count: {count}
|
||||
</button>
|
||||
|
||||
<div className="mt-8">
|
||||
<h2 className="text-2xl font-semibold">Storage Spaces</h2>
|
||||
{spaces.isLoading ? (
|
||||
<p className="mt-2">Loading…</p>
|
||||
) : spaces.error ? (
|
||||
<p className="mt-2 text-red-600">Failed to load storage spaces</p>
|
||||
) : (
|
||||
<ul className="mt-2 list-disc pl-5">
|
||||
{spaces.data?.map((s) => (
|
||||
<li key={s.id}>
|
||||
{s.name}
|
||||
{s.description ? `: ${s.description}` : ''}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
17
apps/pharmanager/src/styles.css
Normal file
17
apps/pharmanager/src/styles.css
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
@import "tailwindcss";
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
28
apps/pharmanager/tsconfig.json
Normal file
28
apps/pharmanager/tsconfig.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"jsx": "react-jsx",
|
||||
"module": "ESNext",
|
||||
"paths": {
|
||||
"#/*": ["./src/*"],
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"types": ["vite/client", "bun"],
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
}
|
||||
}
|
||||
19
apps/pharmanager/vite.config.ts
Normal file
19
apps/pharmanager/vite.config.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import { devtools } from '@tanstack/devtools-vite'
|
||||
|
||||
import { tanstackRouter } from '@tanstack/router-plugin/vite'
|
||||
|
||||
import viteReact from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
const config = defineConfig({
|
||||
resolve: { tsconfigPaths: true },
|
||||
plugins: [
|
||||
devtools(),
|
||||
tailwindcss(),
|
||||
tanstackRouter({ target: 'react', autoCodeSplitting: true }),
|
||||
viteReact(),
|
||||
],
|
||||
})
|
||||
|
||||
export default config
|
||||
1
apps/user-ui
Submodule
1
apps/user-ui
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 4b1f0916c80f839aaf6523cf72fd7e36355887a1
|
||||
3
bunfig.toml
Normal file
3
bunfig.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[install]
|
||||
# Hoisted installs help avoid multiple React copies across workspaces.
|
||||
linker = "hoisted"
|
||||
1
index.ts
Normal file
1
index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
console.log("Hello via Bun!");
|
||||
16
package.json
Normal file
16
package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "health-petal",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
12
packages/api/package.json
Normal file
12
packages/api/package.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "api",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"dependencies": {
|
||||
"@trpc/server": "^11.6.0",
|
||||
"zod": "^3.25.0"
|
||||
}
|
||||
}
|
||||
4
packages/api/src/index.ts
Normal file
4
packages/api/src/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export { appRouter } from './router'
|
||||
export type { AppRouter } from './router'
|
||||
export type { TrpcContext } from './router'
|
||||
export type { StorageSpace, StorageSpacesService } from './router/storageSpaces'
|
||||
11
packages/api/src/router/index.ts
Normal file
11
packages/api/src/router/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { initTRPC } from '@trpc/server'
|
||||
import { storageSpacesRouter, type TrpcContext } from './storageSpaces'
|
||||
|
||||
const t = initTRPC.context<TrpcContext>().create()
|
||||
|
||||
export const appRouter = t.router({
|
||||
storageSpaces: storageSpacesRouter,
|
||||
})
|
||||
|
||||
export type AppRouter = typeof appRouter
|
||||
export type { TrpcContext } from './storageSpaces'
|
||||
86
packages/api/src/router/storageSpaces.ts
Normal file
86
packages/api/src/router/storageSpaces.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { initTRPC } from '@trpc/server'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const StorageSpaceSchema = z.object({
|
||||
id: z.number().int(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
image_urls: z.array(z.string()),
|
||||
})
|
||||
|
||||
export type StorageSpace = z.infer<typeof StorageSpaceSchema>
|
||||
|
||||
export type StorageSpacesService = {
|
||||
getStorageSpaces: () => Promise<StorageSpace[]>
|
||||
getStorageSpaceById: (id: number) => Promise<StorageSpace | null>
|
||||
createStorageSpace: (input: {
|
||||
name: string
|
||||
description?: string | null
|
||||
image_urls: string[]
|
||||
}) => Promise<StorageSpace>
|
||||
updateStorageSpace: (id: number, patch: {
|
||||
name?: string
|
||||
description?: string | null
|
||||
image_urls?: string[]
|
||||
}) => Promise<StorageSpace | null>
|
||||
deleteStorageSpace: (id: number) => Promise<boolean>
|
||||
}
|
||||
|
||||
export type TrpcContext = {
|
||||
storageSpaces: StorageSpacesService
|
||||
}
|
||||
|
||||
const t = initTRPC.context<TrpcContext>().create()
|
||||
|
||||
export const storageSpacesRouter = t.router({
|
||||
getStorageSpaces: t.procedure.output(z.array(StorageSpaceSchema)).query(({ ctx }) => {
|
||||
return ctx.storageSpaces.getStorageSpaces()
|
||||
}),
|
||||
|
||||
getStorageSpaceById: t.procedure
|
||||
.input(z.object({ id: z.number().int() }))
|
||||
.output(StorageSpaceSchema.nullable())
|
||||
.query(({ ctx, input }) => {
|
||||
return ctx.storageSpaces.getStorageSpaceById(input.id)
|
||||
}),
|
||||
|
||||
addStorageSpace: t.procedure
|
||||
.input(
|
||||
z.object({
|
||||
name: z.string().min(1),
|
||||
description: z.string().nullable().optional(),
|
||||
image_urls: z.array(z.string()).default([]),
|
||||
}),
|
||||
)
|
||||
.output(StorageSpaceSchema)
|
||||
.mutation(({ ctx, input }) => {
|
||||
return ctx.storageSpaces.createStorageSpace({
|
||||
name: input.name,
|
||||
description: input.description ?? null,
|
||||
image_urls: input.image_urls,
|
||||
})
|
||||
}),
|
||||
|
||||
updateStorageSpace: t.procedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.number().int(),
|
||||
name: z.string().min(1).optional(),
|
||||
description: z.string().nullable().optional(),
|
||||
image_urls: z.array(z.string()).optional(),
|
||||
}),
|
||||
)
|
||||
.output(StorageSpaceSchema.nullable())
|
||||
.mutation(({ ctx, input }) => {
|
||||
const { id, ...patch } = input
|
||||
return ctx.storageSpaces.updateStorageSpace(id, patch)
|
||||
}),
|
||||
|
||||
deleteStorageSpace: t.procedure
|
||||
.input(z.object({ id: z.number().int() }))
|
||||
.output(z.object({ ok: z.boolean() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const ok = await ctx.storageSpaces.deleteStorageSpace(input.id)
|
||||
return { ok }
|
||||
}),
|
||||
})
|
||||
4
packages/api/tsconfig.json
Normal file
4
packages/api/tsconfig.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"]
|
||||
}
|
||||
16
packages/data-manager-sqlite/drizzle.config.ts
Normal file
16
packages/data-manager-sqlite/drizzle.config.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { Config } from 'drizzle-kit'
|
||||
import path from 'node:path'
|
||||
import { env } from './src/lib/env-exporter'
|
||||
|
||||
// Default to the backend dev database in the monorepo.
|
||||
const defaultDbPath = path.resolve(import.meta.dir, '../../apps/backend/dev.db')
|
||||
const dbPath = env.SQLITE_PATH || defaultDbPath
|
||||
|
||||
export default {
|
||||
schema: './src/schema/index.ts',
|
||||
out: './drizzle',
|
||||
dialect: 'sqlite',
|
||||
dbCredentials: {
|
||||
url: dbPath,
|
||||
},
|
||||
} satisfies Config
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
CREATE TABLE `storage_spaces` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`description` text,
|
||||
`image_urls` text DEFAULT '[]' NOT NULL
|
||||
);
|
||||
57
packages/data-manager-sqlite/drizzle/meta/0000_snapshot.json
Normal file
57
packages/data-manager-sqlite/drizzle/meta/0000_snapshot.json
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "1fe82ac3-40f3-4846-8d99-eddabf4580a2",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"storage_spaces": {
|
||||
"name": "storage_spaces",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"image_urls": {
|
||||
"name": "image_urls",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
13
packages/data-manager-sqlite/drizzle/meta/_journal.json
Normal file
13
packages/data-manager-sqlite/drizzle/meta/_journal.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1778999618092,
|
||||
"tag": "0000_clumsy_morgan_stark",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
22
packages/data-manager-sqlite/package.json
Normal file
22
packages/data-manager-sqlite/package.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "data-manager-sqlite",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"migrate:generate": "drizzle-kit generate",
|
||||
"migrate:push": "drizzle-kit push",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"drizzle-orm": "^0.44.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/bun": "^1.3.14",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
24
packages/data-manager-sqlite/src/db.ts
Normal file
24
packages/data-manager-sqlite/src/db.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { Database } from 'bun:sqlite'
|
||||
import { drizzle } from 'drizzle-orm/bun-sqlite'
|
||||
import path from 'node:path'
|
||||
import { mkdirSync } from 'node:fs'
|
||||
import { env } from './lib/env-exporter'
|
||||
|
||||
export type DbOptions = {
|
||||
sqlitePath?: string
|
||||
}
|
||||
|
||||
export function createDb(options: DbOptions = {}) {
|
||||
const configured = options.sqlitePath || env.SQLITE_PATH || 'dev.db'
|
||||
const sqlitePath = path.isAbsolute(configured)
|
||||
? configured
|
||||
: path.resolve(process.cwd(), configured)
|
||||
|
||||
// Ensure parent directory exists (eg. when using ./data/dev.db)
|
||||
mkdirSync(path.dirname(sqlitePath), { recursive: true })
|
||||
|
||||
const sqlite = new Database(sqlitePath)
|
||||
const db = drizzle(sqlite)
|
||||
|
||||
return { db, sqlite, sqlitePath }
|
||||
}
|
||||
8
packages/data-manager-sqlite/src/index.ts
Normal file
8
packages/data-manager-sqlite/src/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export { createDb } from './db'
|
||||
export { runMigrations } from './migrate'
|
||||
export {
|
||||
createStorageSpacesRepo,
|
||||
type StorageSpace,
|
||||
type StorageSpacesRepo,
|
||||
} from './storageSpaces'
|
||||
export { storageSpaces } from './schema/storageSpaces'
|
||||
3
packages/data-manager-sqlite/src/lib/env-exporter.ts
Normal file
3
packages/data-manager-sqlite/src/lib/env-exporter.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export const env = {
|
||||
SQLITE_PATH: process.env.SQLITE_PATH,
|
||||
} as const
|
||||
12
packages/data-manager-sqlite/src/migrate.ts
Normal file
12
packages/data-manager-sqlite/src/migrate.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import path from 'node:path'
|
||||
import { drizzle } from 'drizzle-orm/bun-sqlite'
|
||||
import { migrate } from 'drizzle-orm/bun-sqlite/migrator'
|
||||
import type { Database } from 'bun:sqlite'
|
||||
|
||||
export function runMigrations(sqlite: Database, opts?: { migrationsFolder?: string }) {
|
||||
const migrationsFolder =
|
||||
opts?.migrationsFolder || path.resolve(import.meta.dir, '../drizzle')
|
||||
|
||||
const db = drizzle(sqlite)
|
||||
migrate(db, { migrationsFolder })
|
||||
}
|
||||
1
packages/data-manager-sqlite/src/schema/index.ts
Normal file
1
packages/data-manager-sqlite/src/schema/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './storageSpaces'
|
||||
9
packages/data-manager-sqlite/src/schema/storageSpaces.ts
Normal file
9
packages/data-manager-sqlite/src/schema/storageSpaces.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
export const storageSpaces = sqliteTable('storage_spaces', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
// JSON array string of image URLs
|
||||
imageUrls: text('image_urls').notNull().default('[]'),
|
||||
})
|
||||
125
packages/data-manager-sqlite/src/storageSpaces.ts
Normal file
125
packages/data-manager-sqlite/src/storageSpaces.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import { eq } from 'drizzle-orm'
|
||||
import type { Database } from 'bun:sqlite'
|
||||
|
||||
import { createDb } from './db'
|
||||
import { storageSpaces } from './schema/storageSpaces'
|
||||
|
||||
export type StorageSpace = {
|
||||
id: number
|
||||
name: string
|
||||
description: string | null
|
||||
image_urls: string[]
|
||||
}
|
||||
|
||||
function parseImageUrls(raw: string): string[] {
|
||||
try {
|
||||
const v = JSON.parse(raw)
|
||||
return Array.isArray(v) && v.every((x) => typeof x === 'string') ? v : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function serializeImageUrls(urls: string[]): string {
|
||||
return JSON.stringify(urls)
|
||||
}
|
||||
|
||||
function toStorageSpace(row: {
|
||||
id: number
|
||||
name: string
|
||||
description: string | null
|
||||
imageUrls: string
|
||||
}): StorageSpace {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
image_urls: parseImageUrls(row.imageUrls),
|
||||
}
|
||||
}
|
||||
|
||||
export type StorageSpacesRepo = {
|
||||
getStorageSpaces: () => Promise<StorageSpace[]>
|
||||
getStorageSpaceById: (id: number) => Promise<StorageSpace | null>
|
||||
createStorageSpace: (input: {
|
||||
name: string
|
||||
description?: string | null
|
||||
image_urls: string[]
|
||||
}) => Promise<StorageSpace>
|
||||
updateStorageSpace: (id: number, patch: {
|
||||
name?: string
|
||||
description?: string | null
|
||||
image_urls?: string[]
|
||||
}) => Promise<StorageSpace | null>
|
||||
deleteStorageSpace: (id: number) => Promise<boolean>
|
||||
}
|
||||
|
||||
export function createStorageSpacesRepo(opts?: { sqlitePath?: string }): {
|
||||
repo: StorageSpacesRepo
|
||||
sqlite: Database
|
||||
close: () => void
|
||||
} {
|
||||
const { db, sqlite } = createDb({ sqlitePath: opts?.sqlitePath })
|
||||
|
||||
const repo: StorageSpacesRepo = {
|
||||
async getStorageSpaces() {
|
||||
const rows = await db.select().from(storageSpaces).all()
|
||||
return rows.map(toStorageSpace)
|
||||
},
|
||||
|
||||
async getStorageSpaceById(id) {
|
||||
const row = await db
|
||||
.select()
|
||||
.from(storageSpaces)
|
||||
.where(eq(storageSpaces.id, id))
|
||||
.get()
|
||||
return row ? toStorageSpace(row) : null
|
||||
},
|
||||
|
||||
async createStorageSpace(input) {
|
||||
const created = await db
|
||||
.insert(storageSpaces)
|
||||
.values({
|
||||
name: input.name,
|
||||
description: input.description ?? null,
|
||||
imageUrls: serializeImageUrls(input.image_urls),
|
||||
})
|
||||
.returning()
|
||||
.get()
|
||||
|
||||
return toStorageSpace(created)
|
||||
},
|
||||
|
||||
async updateStorageSpace(id, patch) {
|
||||
const updated = await db
|
||||
.update(storageSpaces)
|
||||
.set({
|
||||
...(patch.name !== undefined ? { name: patch.name } : {}),
|
||||
...(patch.description !== undefined ? { description: patch.description } : {}),
|
||||
...(patch.image_urls !== undefined
|
||||
? { imageUrls: serializeImageUrls(patch.image_urls) }
|
||||
: {}),
|
||||
})
|
||||
.where(eq(storageSpaces.id, id))
|
||||
.returning()
|
||||
.get()
|
||||
|
||||
return updated ? toStorageSpace(updated) : null
|
||||
},
|
||||
|
||||
async deleteStorageSpace(id) {
|
||||
const deleted = await db
|
||||
.delete(storageSpaces)
|
||||
.where(eq(storageSpaces.id, id))
|
||||
.returning({ id: storageSpaces.id })
|
||||
.get()
|
||||
return Boolean(deleted)
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
repo,
|
||||
sqlite,
|
||||
close: () => sqlite.close(),
|
||||
}
|
||||
}
|
||||
7
packages/data-manager-sqlite/tsconfig.json
Normal file
7
packages/data-manager-sqlite/tsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "bun"]
|
||||
},
|
||||
"include": ["src", "drizzle.config.ts"]
|
||||
}
|
||||
18
packages/shared-react/package.json
Normal file
18
packages/shared-react/package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "shared-react",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"peerDependencies": {
|
||||
"react": "19.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.87.4",
|
||||
"@trpc/client": "^11.6.0",
|
||||
"@trpc/react-query": "^11.6.0",
|
||||
"@repo/shared": "*",
|
||||
"zustand": "^5.0.8"
|
||||
}
|
||||
}
|
||||
21
packages/shared-react/src/hooks/storageSpaces.ts
Normal file
21
packages/shared-react/src/hooks/storageSpaces.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { trpc } from '../trpc'
|
||||
|
||||
export function useGetStorageSpaces() {
|
||||
return trpc.storageSpaces.getStorageSpaces.useQuery()
|
||||
}
|
||||
|
||||
export function useGetStorageSpaceById(id: number) {
|
||||
return trpc.storageSpaces.getStorageSpaceById.useQuery({ id })
|
||||
}
|
||||
|
||||
export function useAddStorageSpace() {
|
||||
return trpc.storageSpaces.addStorageSpace.useMutation()
|
||||
}
|
||||
|
||||
export function useUpdateStorageSpace() {
|
||||
return trpc.storageSpaces.updateStorageSpace.useMutation()
|
||||
}
|
||||
|
||||
export function useDeleteStorageSpace() {
|
||||
return trpc.storageSpaces.deleteStorageSpace.useMutation()
|
||||
}
|
||||
4
packages/shared-react/src/index.ts
Normal file
4
packages/shared-react/src/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export * from './query'
|
||||
export * from './store'
|
||||
export * from './provider'
|
||||
export * from './hooks/storageSpaces'
|
||||
20
packages/shared-react/src/provider.tsx
Normal file
20
packages/shared-react/src/provider.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import * as React from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { createTrpcClient, trpc } from './trpc'
|
||||
|
||||
const apiUrl = 'http://192.168.100.113:4004'
|
||||
|
||||
export function TrpcProvider(props: {
|
||||
baseUrl: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [queryClient] = React.useState(() => new QueryClient())
|
||||
// const [client] = React.useState(() => createTrpcClient(props.baseUrl))
|
||||
const [client] = React.useState(() => createTrpcClient(apiUrl))
|
||||
|
||||
return (
|
||||
<trpc.Provider client={client} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>{props.children}</QueryClientProvider>
|
||||
</trpc.Provider>
|
||||
)
|
||||
}
|
||||
12
packages/shared-react/src/query.ts
Normal file
12
packages/shared-react/src/query.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { QueryClient } from '@tanstack/react-query'
|
||||
|
||||
export function createQueryClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
15
packages/shared-react/src/store.ts
Normal file
15
packages/shared-react/src/store.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { create } from 'zustand'
|
||||
|
||||
export type CounterStore = {
|
||||
count: number
|
||||
inc: () => void
|
||||
dec: () => void
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
export const useCounterStore = create<CounterStore>((set) => ({
|
||||
count: 0,
|
||||
inc: () => set((s) => ({ count: s.count + 1 })),
|
||||
dec: () => set((s) => ({ count: s.count - 1 })),
|
||||
reset: () => set({ count: 0 }),
|
||||
}))
|
||||
15
packages/shared-react/src/trpc.ts
Normal file
15
packages/shared-react/src/trpc.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { createTRPCReact } from '@trpc/react-query'
|
||||
import { httpBatchLink } from '@trpc/client'
|
||||
import type { AppRouter } from '@repo/shared'
|
||||
|
||||
export const trpc = createTRPCReact<AppRouter>()
|
||||
|
||||
export function createTrpcClient(baseUrl: string) {
|
||||
return trpc.createClient({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: `${baseUrl}/trpc`,
|
||||
}),
|
||||
],
|
||||
})
|
||||
}
|
||||
4
packages/shared-react/tsconfig.json
Normal file
4
packages/shared-react/tsconfig.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"]
|
||||
}
|
||||
6
packages/shared/package.json
Normal file
6
packages/shared/package.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@repo/shared",
|
||||
"version": "0.0.1",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts"
|
||||
}
|
||||
18
packages/shared/src/index.ts
Normal file
18
packages/shared/src/index.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
export type Person = {
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
export const person:Person = {
|
||||
name: 'Shafi',
|
||||
age: 26
|
||||
}
|
||||
|
||||
export function greetPerson(person: Person) {
|
||||
return `hello ${person.name}`
|
||||
}
|
||||
|
||||
// tRPC contract types (router lives in apps/backend)
|
||||
export type {
|
||||
AppRouter,
|
||||
} from '../../../apps/backend/src/trpc/router'
|
||||
7
packages/shared/tsconfig.json
Normal file
7
packages/shared/tsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["bun"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
5655
session-ses_1d40.md
Normal file
5655
session-ses_1d40.md
Normal file
File diff suppressed because one or more lines are too long
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue