switch from next to vue

This commit is contained in:
2026-02-06 01:11:09 +01:00
parent 1193ca3552
commit 43463bbe8f
71 changed files with 5380 additions and 5164 deletions
+158
View File
@@ -0,0 +1,158 @@
import initSqlJs, { type Database } from 'sql.js'
import wasmUrl from 'sql.js/dist/sql-wasm.wasm?url'
import type { Entry } from './types'
const DB_NAME = 'sanasto-sqlite'
const STORE_NAME = 'sqlite'
const DB_KEY = 'db'
let sqlInit: ReturnType<typeof initSqlJs> | null = null
function initSql(): ReturnType<typeof initSqlJs> {
if (!sqlInit) {
sqlInit = initSqlJs({ locateFile: () => wasmUrl })
}
return sqlInit
}
export async function initDb(): Promise<Database> {
const SQL = await initSql()
const stored = await loadDbFromIdb()
const db = stored ? new SQL.Database(new Uint8Array(stored)) : new SQL.Database()
ensureSchema(db)
return db
}
function ensureSchema(db: Database) {
db.exec(`
CREATE TABLE IF NOT EXISTS entries (
id INTEGER PRIMARY KEY,
category TEXT,
fi TEXT,
en TEXT,
sv TEXT,
no TEXT,
ru TEXT,
de TEXT,
updated_at TEXT
);
`)
db.exec(`
CREATE TABLE IF NOT EXISTS meta (
key TEXT PRIMARY KEY,
value TEXT
);
`)
}
export function listEntries(db: Database): Entry[] {
const result = db.exec(
`SELECT id, category, fi, en, sv, no, ru, de, updated_at
FROM entries
ORDER BY COALESCE(fi, en, no, sv, de, ru, '') ASC`
)
const [first] = result
if (!first) {
return []
}
const { columns, values } = first
return values.map((row: unknown[]) => {
const entry: Record<string, unknown> = {}
columns.forEach((column: string, index: number) => {
entry[column] = row[index] ?? null
})
return entry as Entry
})
}
export function upsertEntries(db: Database, entries: Entry[]) {
const statement = db.prepare(
`INSERT INTO entries (id, category, fi, en, sv, no, ru, de, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
category=excluded.category,
fi=excluded.fi,
en=excluded.en,
sv=excluded.sv,
no=excluded.no,
ru=excluded.ru,
de=excluded.de,
updated_at=excluded.updated_at`
)
entries.forEach((entry) => {
statement.run([
entry.id,
entry.category ?? null,
entry.fi ?? null,
entry.en ?? null,
entry.sv ?? null,
entry.no ?? null,
entry.ru ?? null,
entry.de ?? null,
entry.updated_at ?? null,
])
})
statement.free()
}
export function getMeta(db: Database, key: string): string | null {
const result = db.exec(`SELECT value FROM meta WHERE key = ?`, [key])
const first = result[0]
if (!first || !first.values.length || !first.values[0]?.length) {
return null
}
const value = first.values[0][0]
return typeof value === 'string' ? value : null
}
export function setMeta(db: Database, key: string, value: string) {
db.exec(`INSERT INTO meta (key, value) VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value=excluded.value`, [key, value])
}
export async function persistDb(db: Database) {
const data = db.export()
await saveDbToIdb(data)
}
function openIdb(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1)
request.onupgradeneeded = () => {
const db = request.result
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME)
}
}
request.onsuccess = () => resolve(request.result)
request.onerror = () => reject(request.error)
})
}
function loadDbFromIdb(): Promise<ArrayBuffer | null> {
return openIdb().then(
(db) =>
new Promise((resolve, reject) => {
const transaction = db.transaction(STORE_NAME, 'readonly')
const store = transaction.objectStore(STORE_NAME)
const request = store.get(DB_KEY)
request.onsuccess = () => {
resolve((request.result as ArrayBuffer | undefined) ?? null)
}
request.onerror = () => reject(request.error)
})
)
}
function saveDbToIdb(data: Uint8Array): Promise<void> {
return openIdb().then(
(db) =>
new Promise((resolve, reject) => {
const transaction = db.transaction(STORE_NAME, 'readwrite')
const store = transaction.objectStore(STORE_NAME)
const request = store.put(data, DB_KEY)
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
)
}
+48
View File
@@ -0,0 +1,48 @@
import type { Database } from 'sql.js'
import type { Entry } from './types'
import { getMeta, persistDb, setMeta, upsertEntries } from './db'
const LAST_SYNC_KEY = 'last_sync_at'
export async function syncEntries(db: Database) {
const lastSyncAt = getMeta(db, LAST_SYNC_KEY)
const url = new URL('https://sanasto.rin.no/api/entries')
if (lastSyncAt) {
url.searchParams.set('since', lastSyncAt)
}
const response = await fetch(url.toString(), {
headers: {
'X-Sanasto-App': 'app.sanasto',
},
})
if (!response.ok) {
throw new Error(`Sync failed with ${response.status}`)
}
const entries = (await response.json()) as Entry[]
if (entries.length === 0) {
return { updated: 0, lastSyncAt }
}
upsertEntries(db, entries)
const newest = entries.reduce<Date | null>((current, entry) => {
if (!entry.updated_at) {
return current
}
const date = new Date(entry.updated_at)
if (!current || date > current) {
return date
}
return current
}, lastSyncAt ? new Date(lastSyncAt) : null)
if (newest) {
setMeta(db, LAST_SYNC_KEY, newest.toISOString())
}
await persistDb(db)
return { updated: entries.length, lastSyncAt: newest?.toISOString() ?? lastSyncAt }
}
+16
View File
@@ -0,0 +1,16 @@
export type Entry = {
id: number
category: string | null
fi: string | null
en: string | null
sv: string | null
no: string | null
ru: string | null
de: string | null
updated_at: string | null
}
export type Language = {
code: keyof Pick<Entry, 'fi' | 'en' | 'sv' | 'no' | 'ru' | 'de'>
name: string
}