Compare commits

..

10 Commits

Author SHA1 Message Date
8b4051e473 refactor upload image endpoint 2026-01-13 12:03:33 +13:00
3f61caf04f endpoint to get one entry by id 2026-01-13 11:50:15 +13:00
e28b6a053e fix type error 2026-01-13 11:36:29 +13:00
ef72a95372 uploading works! storing images separately and all 2026-01-13 11:33:01 +13:00
03d8c2ff74 starting upload functions 2026-01-13 10:27:33 +13:00
cf5801689f return full image path on upload 2026-01-12 19:29:41 +13:00
6bde837330 added fetch image by id endpoint 2026-01-12 18:59:02 +13:00
760b2feb16 decent image storage api 2026-01-12 18:48:58 +13:00
64a4baf38c still hate docker tho 2026-01-12 18:23:23 +13:00
ea173dcc52 yippee C and R from db works well 2026-01-12 18:16:32 +13:00
16 changed files with 258 additions and 201 deletions

View File

@@ -1,13 +1,16 @@
FROM node:lts AS runtime FROM node:lts AS runtime
WORKDIR /app WORKDIR /app
VOLUME [ "/data" ]
COPY . . COPY . .
ENV ASTRO_DB_REMOTE_URL=libsql ENV DATABASE_URL=postgresql://memento:test@localhost:5432/memento
ENV UPLOAD_DIR=/data/images
RUN npm install RUN npm install
RUN npx astro db push --remote RUN npx drizzle-kit push
RUN npx astro build --remote RUN npx astro build
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0
ENV PORT=4321 ENV PORT=4321

View File

@@ -3,8 +3,6 @@ import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite'; import tailwindcss from '@tailwindcss/vite';
import db from '@astrojs/db';
import node from '@astrojs/node'; import node from '@astrojs/node';
// https://astro.build/config // https://astro.build/config
@@ -14,8 +12,6 @@ export default defineConfig({
plugins: [tailwindcss()] plugins: [tailwindcss()]
}, },
integrations: [db()],
adapter: node({ adapter: node({
mode: 'standalone' mode: 'standalone'
}) })

View File

@@ -1,9 +1,4 @@
services: services:
memento:
build: .
ports:
- 4321:4321
db: db:
image: postgres:17 image: postgres:17
ports: ports:
@@ -14,3 +9,8 @@ services:
POSTGRES_PASSWORD: test POSTGRES_PASSWORD: test
POSTGRES_USER: memento POSTGRES_USER: memento
memento:
build: .
ports:
- 4321:4321

11
drizzle.config.ts Normal file
View File

@@ -0,0 +1,11 @@
import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
out: './drizzle',
schema: './src/db/schema.ts',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});

View File

@@ -9,7 +9,6 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"@astrojs/db": "^0.18.3",
"@astrojs/node": "^9.5.1", "@astrojs/node": "^9.5.1",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"astro": "^5.16.8", "astro": "^5.16.8",

191
pnpm-lock.yaml generated
View File

@@ -8,9 +8,6 @@ importers:
.: .:
dependencies: dependencies:
'@astrojs/db':
specifier: ^0.18.3
version: 0.18.3(@types/pg@8.16.0)(pg@8.16.3)
'@astrojs/node': '@astrojs/node':
specifier: ^9.5.1 specifier: ^9.5.1
version: 9.5.1(astro@5.16.8(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.55.1)(tsx@4.21.0)(typescript@5.9.3)) version: 9.5.1(astro@5.16.8(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.55.1)(tsx@4.21.0)(typescript@5.9.3))
@@ -51,9 +48,6 @@ packages:
'@astrojs/compiler@2.13.0': '@astrojs/compiler@2.13.0':
resolution: {integrity: sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw==} resolution: {integrity: sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw==}
'@astrojs/db@0.18.3':
resolution: {integrity: sha512-iTK50jUgyj25oa/JiXSN1/IVp5kTmPuioLlve06LE8/HzWGv3JpVgCKIV9HHf3kOVi1HV/uauXnyWzkB+yHLSQ==}
'@astrojs/internal-helpers@0.7.5': '@astrojs/internal-helpers@0.7.5':
resolution: {integrity: sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA==} resolution: {integrity: sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA==}
@@ -1205,10 +1199,6 @@ packages:
decode-named-character-reference@1.2.0: decode-named-character-reference@1.2.0:
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
deep-diff@1.0.2:
resolution: {integrity: sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
defu@6.1.4: defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
@@ -1269,95 +1259,6 @@ packages:
resolution: {integrity: sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg==} resolution: {integrity: sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg==}
hasBin: true hasBin: true
drizzle-orm@0.42.0:
resolution: {integrity: sha512-pS8nNJm2kBNZwrOjTHJfdKkaU+KuUQmV/vk5D57NojDq4FG+0uAYGMulXtYT///HfgsMF0hnFFvu1ezI3OwOkg==}
peerDependencies:
'@aws-sdk/client-rds-data': '>=3'
'@cloudflare/workers-types': '>=4'
'@electric-sql/pglite': '>=0.2.0'
'@libsql/client': '>=0.10.0'
'@libsql/client-wasm': '>=0.10.0'
'@neondatabase/serverless': '>=0.10.0'
'@op-engineering/op-sqlite': '>=2'
'@opentelemetry/api': ^1.4.1
'@planetscale/database': '>=1.13'
'@prisma/client': '*'
'@tidbcloud/serverless': '*'
'@types/better-sqlite3': '*'
'@types/pg': '*'
'@types/sql.js': '*'
'@vercel/postgres': '>=0.8.0'
'@xata.io/client': '*'
better-sqlite3: '>=7'
bun-types: '*'
expo-sqlite: '>=14.0.0'
gel: '>=2'
knex: '*'
kysely: '*'
mysql2: '>=2'
pg: '>=8'
postgres: '>=3'
prisma: '*'
sql.js: '>=1'
sqlite3: '>=5'
peerDependenciesMeta:
'@aws-sdk/client-rds-data':
optional: true
'@cloudflare/workers-types':
optional: true
'@electric-sql/pglite':
optional: true
'@libsql/client':
optional: true
'@libsql/client-wasm':
optional: true
'@neondatabase/serverless':
optional: true
'@op-engineering/op-sqlite':
optional: true
'@opentelemetry/api':
optional: true
'@planetscale/database':
optional: true
'@prisma/client':
optional: true
'@tidbcloud/serverless':
optional: true
'@types/better-sqlite3':
optional: true
'@types/pg':
optional: true
'@types/sql.js':
optional: true
'@vercel/postgres':
optional: true
'@xata.io/client':
optional: true
better-sqlite3:
optional: true
bun-types:
optional: true
expo-sqlite:
optional: true
gel:
optional: true
knex:
optional: true
kysely:
optional: true
mysql2:
optional: true
pg:
optional: true
postgres:
optional: true
prisma:
optional: true
sql.js:
optional: true
sqlite3:
optional: true
drizzle-orm@0.45.1: drizzle-orm@0.45.1:
resolution: {integrity: sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==} resolution: {integrity: sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==}
peerDependencies: peerDependencies:
@@ -1919,11 +1820,6 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
nanoid@5.1.6:
resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==}
engines: {node: ^18 || >=20}
hasBin: true
neotraverse@0.6.18: neotraverse@0.6.18:
resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
@@ -2524,47 +2420,6 @@ snapshots:
'@astrojs/compiler@2.13.0': {} '@astrojs/compiler@2.13.0': {}
'@astrojs/db@0.18.3(@types/pg@8.16.0)(pg@8.16.3)':
dependencies:
'@libsql/client': 0.15.15
deep-diff: 1.0.2
drizzle-orm: 0.42.0(@libsql/client@0.15.15)(@types/pg@8.16.0)(pg@8.16.3)
nanoid: 5.1.6
piccolore: 0.1.3
prompts: 2.4.2
yargs-parser: 21.1.1
zod: 3.25.76
transitivePeerDependencies:
- '@aws-sdk/client-rds-data'
- '@cloudflare/workers-types'
- '@electric-sql/pglite'
- '@libsql/client-wasm'
- '@neondatabase/serverless'
- '@op-engineering/op-sqlite'
- '@opentelemetry/api'
- '@planetscale/database'
- '@prisma/client'
- '@tidbcloud/serverless'
- '@types/better-sqlite3'
- '@types/pg'
- '@types/sql.js'
- '@vercel/postgres'
- '@xata.io/client'
- better-sqlite3
- bufferutil
- bun-types
- expo-sqlite
- gel
- knex
- kysely
- mysql2
- pg
- postgres
- prisma
- sql.js
- sqlite3
- utf-8-validate
'@astrojs/internal-helpers@0.7.5': {} '@astrojs/internal-helpers@0.7.5': {}
'@astrojs/markdown-remark@6.3.10': '@astrojs/markdown-remark@6.3.10':
@@ -3000,10 +2855,12 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
- utf-8-validate - utf-8-validate
optional: true
'@libsql/core@0.15.15': '@libsql/core@0.15.15':
dependencies: dependencies:
js-base64: 3.7.8 js-base64: 3.7.8
optional: true
'@libsql/darwin-arm64@0.5.22': '@libsql/darwin-arm64@0.5.22':
optional: true optional: true
@@ -3020,8 +2877,10 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
- utf-8-validate - utf-8-validate
optional: true
'@libsql/isomorphic-fetch@0.3.1': {} '@libsql/isomorphic-fetch@0.3.1':
optional: true
'@libsql/isomorphic-ws@0.1.5': '@libsql/isomorphic-ws@0.1.5':
dependencies: dependencies:
@@ -3030,6 +2889,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
- utf-8-validate - utf-8-validate
optional: true
'@libsql/linux-arm-gnueabihf@0.5.22': '@libsql/linux-arm-gnueabihf@0.5.22':
optional: true optional: true
@@ -3052,7 +2912,8 @@ snapshots:
'@libsql/win32-x64-msvc@0.5.22': '@libsql/win32-x64-msvc@0.5.22':
optional: true optional: true
'@neon-rs/load@0.0.4': {} '@neon-rs/load@0.0.4':
optional: true
'@oslojs/encoding@1.1.0': {} '@oslojs/encoding@1.1.0': {}
@@ -3275,6 +3136,7 @@ snapshots:
'@types/ws@8.18.1': '@types/ws@8.18.1':
dependencies: dependencies:
'@types/node': 25.0.6 '@types/node': 25.0.6
optional: true
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
@@ -3486,7 +3348,8 @@ snapshots:
dependencies: dependencies:
css-tree: 2.2.1 css-tree: 2.2.1
data-uri-to-buffer@4.0.1: {} data-uri-to-buffer@4.0.1:
optional: true
debug@4.4.3: debug@4.4.3:
dependencies: dependencies:
@@ -3496,8 +3359,6 @@ snapshots:
dependencies: dependencies:
character-entities: 2.0.2 character-entities: 2.0.2
deep-diff@1.0.2: {}
defu@6.1.4: {} defu@6.1.4: {}
depd@2.0.0: {} depd@2.0.0: {}
@@ -3506,7 +3367,8 @@ snapshots:
destr@2.0.5: {} destr@2.0.5: {}
detect-libc@2.0.2: {} detect-libc@2.0.2:
optional: true
detect-libc@2.1.2: {} detect-libc@2.1.2: {}
@@ -3553,12 +3415,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
drizzle-orm@0.42.0(@libsql/client@0.15.15)(@types/pg@8.16.0)(pg@8.16.3):
optionalDependencies:
'@libsql/client': 0.15.15
'@types/pg': 8.16.0
pg: 8.16.3
drizzle-orm@0.45.1(@libsql/client@0.15.15)(@types/pg@8.16.0)(pg@8.16.3): drizzle-orm@0.45.1(@libsql/client@0.15.15)(@types/pg@8.16.0)(pg@8.16.3):
optionalDependencies: optionalDependencies:
'@libsql/client': 0.15.15 '@libsql/client': 0.15.15
@@ -3702,6 +3558,7 @@ snapshots:
dependencies: dependencies:
node-domexception: 1.0.0 node-domexception: 1.0.0
web-streams-polyfill: 3.3.3 web-streams-polyfill: 3.3.3
optional: true
flattie@1.1.1: {} flattie@1.1.1: {}
@@ -3716,6 +3573,7 @@ snapshots:
formdata-polyfill@4.0.10: formdata-polyfill@4.0.10:
dependencies: dependencies:
fetch-blob: 3.2.0 fetch-blob: 3.2.0
optional: true
fresh@2.0.0: {} fresh@2.0.0: {}
@@ -3867,7 +3725,8 @@ snapshots:
jiti@2.6.1: {} jiti@2.6.1: {}
js-base64@3.7.8: {} js-base64@3.7.8:
optional: true
js-yaml@4.1.1: js-yaml@4.1.1:
dependencies: dependencies:
@@ -3889,6 +3748,7 @@ snapshots:
'@libsql/linux-x64-gnu': 0.5.22 '@libsql/linux-x64-gnu': 0.5.22
'@libsql/linux-x64-musl': 0.5.22 '@libsql/linux-x64-musl': 0.5.22
'@libsql/win32-x64-msvc': 0.5.22 '@libsql/win32-x64-msvc': 0.5.22
optional: true
lightningcss-android-arm64@1.30.2: lightningcss-android-arm64@1.30.2:
optional: true optional: true
@@ -4288,15 +4148,14 @@ snapshots:
nanoid@3.3.11: {} nanoid@3.3.11: {}
nanoid@5.1.6: {}
neotraverse@0.6.18: {} neotraverse@0.6.18: {}
nlcst-to-string@4.0.0: nlcst-to-string@4.0.0:
dependencies: dependencies:
'@types/nlcst': 2.0.3 '@types/nlcst': 2.0.3
node-domexception@1.0.0: {} node-domexception@1.0.0:
optional: true
node-fetch-native@1.6.7: {} node-fetch-native@1.6.7: {}
@@ -4305,6 +4164,7 @@ snapshots:
data-uri-to-buffer: 4.0.1 data-uri-to-buffer: 4.0.1
fetch-blob: 3.2.0 fetch-blob: 3.2.0
formdata-polyfill: 4.0.10 formdata-polyfill: 4.0.10
optional: true
node-mock-http@1.0.4: {} node-mock-http@1.0.4: {}
@@ -4423,7 +4283,8 @@ snapshots:
prismjs@1.30.0: {} prismjs@1.30.0: {}
promise-limit@2.7.0: {} promise-limit@2.7.0:
optional: true
prompts@2.4.2: prompts@2.4.2:
dependencies: dependencies:
@@ -4856,7 +4717,8 @@ snapshots:
web-namespaces@2.0.1: {} web-namespaces@2.0.1: {}
web-streams-polyfill@3.3.3: {} web-streams-polyfill@3.3.3:
optional: true
which-pm-runs@1.1.0: {} which-pm-runs@1.1.0: {}
@@ -4870,7 +4732,8 @@ snapshots:
string-width: 7.2.0 string-width: 7.2.0
strip-ansi: 7.1.2 strip-ansi: 7.1.2
ws@8.19.0: {} ws@8.19.0:
optional: true
xtend@4.0.2: {} xtend@4.0.2: {}

View File

@@ -1,8 +1,8 @@
import { integer, pgTable, varchar, date, json } from "drizzle-orm/pg-core"; import { integer, pgTable, varchar, date, json } from "drizzle-orm/pg-core";
export const usersTable = pgTable("users", { export const entryTable = pgTable("entries", {
id: integer().primaryKey().generatedAlwaysAsIdentity(), id: integer().primaryKey().generatedAlwaysAsIdentity(),
data: date().defaultNow(), date: varchar().notNull(),
location: json(), location: json(),
content: json(), content: json(),
}); });

View File

@@ -0,0 +1,30 @@
import type { APIContext } from 'astro';
import { eq } from 'drizzle-orm';
import { db } from '../../../utils/db';
import { entryTable } from '../../../db/schema';
import { httpResponse } from '../../../utils/response';
export async function GET({ params }: APIContext) {
try {
const { id } = params
if (!id) {
return httpResponse({'error': 'no id provided'}, 400)
}
return getEntry(id)
} catch (error) {
return httpResponse({ error: `Failed to retrieve entry: ${error}` }, 500);
}
}
async function getEntry(id) {
try {
const entry = await db.select().from(entryTable).where(eq(entryTable.id, id))
if (entry.length == 0) {
return httpResponse({'error': 'entry not found'}, 404)
}
return httpResponse(entry[0], 200)
} catch {
return httpResponse({'error': 'bad request'}, 400)
}
}

View File

@@ -1,7 +1,8 @@
import { db, Entry } from "astro:db"; import { db } from "../../../utils/db"
import { entryTable } from "../../../db/schema"
export async function GET() { export async function GET() {
const entries = await db.select().from(Entry) const entries = await db.select().from(entryTable)
return new Response(JSON.stringify(entries)) return new Response(JSON.stringify(entries))
} }

View File

@@ -1,23 +1,23 @@
import { db, Entry } from "astro:db"; import { db } from "../../../utils/db"
import { entryTable } from "../../../db/schema"
import { httpResponse } from "../../../utils/response";
import type { APIContext } from "astro";
export async function POST({ request }) { export async function POST({ request }: APIContext) {
if (request.headers.get("Content-Type") === "application/json") { if (request.headers.get("Content-Type") === "application/json") {
try { try {
const body = await request.json(); const body = await request.json();
const entry: typeof entryTable.$inferInsert = body
await db.insert(Entry).values(body) await db.insert(entryTable).values(entry)
} catch(e) { } catch(e) {
return new Response( return httpResponse({ "error": `Malformed JSON (${e})` }, 400)
JSON.stringify({
"error": `Malformed JSON (${e})`
}),{ status: 400 }
)
} }
return new Response(null, { status: 200 }); return httpResponse(null, 200);
} }
return new Response(null, { status: 400 }); return httpResponse(null, 400);
} }

View File

@@ -0,0 +1,37 @@
import type { APIContext } from 'astro';
import { promises as fs } from 'fs';
import { join } from 'path';
import 'dotenv/config'
import { httpResponse } from '../../../utils/response';
const uploadDir = process.env.UPLOAD_DIR!;
export async function GET({ params }: APIContext) {
try {
const { id } = params
return readImage(id!)
} catch (error) {
return httpResponse({ error: `Failed to retrieve image: ${error}` }, 500);
}
}
async function readImage(id: string) {
const filepath = join(uploadDir, id);
try {
await fs.access(filepath);
} catch {
return httpResponse({ error: 'Image not found' }, 404);
}
const image = await fs.readFile(filepath);
const ext = id.split('.').pop()?.toLowerCase();
const contentType = ext === 'png' ? 'image/png' : 'image/jpeg';
return new Response(image, {
status: 200,
headers: {
'Content-Type': contentType,
}
});
}

View File

@@ -0,0 +1,41 @@
import type { APIContext } from 'astro';
import { promises as fs } from 'fs';
import { join } from 'path';
import { randomBytes } from 'crypto';
import { httpResponse } from '../../../utils/response';
import 'dotenv/config'
const uploadDir = process.env.UPLOAD_DIR!;
export async function POST({ request }: APIContext) {
try {
const formData = await request.formData();
const file = formData.get('image');
return await writeImage(file)
} catch (error) {
return httpResponse({ error: `Failed to upload image, ${error}` }, 500);
}
}
async function writeImage(file) {
if (!file || !(file instanceof File)) {
return httpResponse({ error: 'No image provided' }, 400);
}
if (!file.type.match(/^image\/(jpeg|jpg|png)$/)) {
return httpResponse({ error: 'Only JPG and PNG allowed' }, 400);
}
const ext = file.name.split('.').pop();
const filename = `${randomBytes(16).toString('hex')}.${ext}`;
const filepath = join(uploadDir, filename);
// Save file
const buffer = Buffer.from(await file.arrayBuffer());
await fs.writeFile(filepath, buffer);
return httpResponse({ url: `/api/image/${filename}`}, 201);
}

View File

@@ -4,25 +4,30 @@ import "../styles/global.css"
<script> <script>
import Quill from "quill"; import Quill from "quill";
import { uploadEntry } from '../utils/quill'
import type { Entry } from "../utils/quill";
const quill = new Quill('#editor', { const quill = new Quill('#editor', {
modules: { modules: {
toolbar: [ toolbar: [
// [{ header: [1, 2, false] }],
['bold', 'italic', 'underline'], ['bold', 'italic', 'underline'],
['image'], ['image']
], ],
}, },
placeholder: 'Compose an epic...', placeholder: 'Compose an epic...',
theme: 'snow', // or 'bubble' theme: 'snow', // or 'bubble'
}); });
document.querySelector("#render")?.addEventListener('click', () => { document.querySelector("#upload")?.addEventListener('click', async () => {
const contents = JSON.stringify(quill.getContents()) const contents = quill.getContents()
const el = document.getElementById("result") let entry: Entry = {
content: contents,
date: '2026-01-13T10:49:43Z',
location: null
}
el!.innerText = contents await uploadEntry(entry)
}) })
@@ -45,8 +50,8 @@ import "../styles/global.css"
<p><br /></p> <p><br /></p>
</div> </div>
<button id="render" class="mt-2"> <button id="upload" class="mt-2">
render upload
</button> </button>
<div id="result"> <div id="result">

View File

@@ -1,4 +1,4 @@
import 'dotenv/config'; import 'dotenv/config';
import { drizzle } from 'drizzle-orm/node-postgres'; import { drizzle } from 'drizzle-orm/node-postgres';
const db = drizzle(process.env.DATABASE_URL!); export const db = drizzle(process.env.DATABASE_URL!);

66
src/utils/quill.ts Normal file
View File

@@ -0,0 +1,66 @@
import type { Delta } from "quill";
export interface Entry {
date: string,
location: { lat: number, long: number } | null,
content: Delta
}
// ty https://stackoverflow.com/questions/35940290
function dataURLtoFile(dataurl: string, filename: string) {
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[arr.length - 1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while(n--){
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, {type:mime});
}
async function uploadImage(b64: string) {
const data = new FormData()
// unable to tell from the b64 whether it's a jpg or png but defaulting to jpg seems to work fine enough
const file = dataURLtoFile(b64, 'image.jpg')
data.append('image', file)
const r = await fetch('/api/image', {
method: 'POST',
body: data
})
const url = (await r.json()).url
return url
}
async function uploadAllImages(delta: Delta) {
let newDelta = delta
for (const val of newDelta.ops) {
if (val.insert?.image != null) {
const imgUrl = await uploadImage(val.insert!.image)
val.insert!.image = imgUrl
}
}
return newDelta
}
export async function uploadEntry(entry: Entry) {
// first upload all the images seperately
const delta = await uploadAllImages(entry.content)
const finalEntry: Entry = {
date: entry.date,
location: entry.location,
content: delta
}
const r = await fetch('/api/entry/new', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify(finalEntry)
})
}

5
src/utils/response.ts Normal file
View File

@@ -0,0 +1,5 @@
export function httpResponse(data: object | null, code: number) {
return new Response(JSON.stringify(data), {
status: code
})
}