diff --git a/packages/shared-uload/package.json b/packages/shared-uload/package.json new file mode 100644 index 000000000..00c1cdf2d --- /dev/null +++ b/packages/shared-uload/package.json @@ -0,0 +1,31 @@ +{ + "name": "@manacore/shared-uload", + "version": "0.1.0", + "private": true, + "type": "module", + "svelte": "./src/index.ts", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "svelte": "./src/index.ts", + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "scripts": { + "check": "svelte-check --tsconfig ./tsconfig.json" + }, + "dependencies": { + "@manacore/local-store": "workspace:*", + "@manacore/shared-icons": "workspace:*" + }, + "devDependencies": { + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "typescript": "^5.7.3" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } +} diff --git a/packages/shared-uload/src/ShareModal.svelte b/packages/shared-uload/src/ShareModal.svelte new file mode 100644 index 000000000..bc966bf64 --- /dev/null +++ b/packages/shared-uload/src/ShareModal.svelte @@ -0,0 +1,244 @@ + + + + +{#if visible} + + +{/if} diff --git a/packages/shared-uload/src/create-link.ts b/packages/shared-uload/src/create-link.ts new file mode 100644 index 000000000..0604d092f --- /dev/null +++ b/packages/shared-uload/src/create-link.ts @@ -0,0 +1,106 @@ +import { createLocalStore, type LocalCollection } from '@manacore/local-store'; +import type { UloadLink, CreateShortLinkOptions, CreatedLink } from './types'; +import { generateShortCode, getShortUrl, getQrCodeUrl } from './utils'; + +let _linkCollection: LocalCollection | null = null; +let _store: ReturnType | null = null; +let _baseUrl: string | undefined; +let _initPromise: Promise | null = null; + +/** + * Initialize the shared uLoad utility. + * + * Option A: Pass an existing linkCollection (from inside uLoad's own app). + * Option B: Call with no collection — it opens the uLoad IndexedDB directly (from any other app). + */ +export function initSharedUload( + linkCollectionOrOptions?: LocalCollection | { baseUrl?: string }, + options?: { baseUrl?: string } +): void { + if (linkCollectionOrOptions && 'insert' in linkCollectionOrOptions) { + // Option A: Existing collection + _linkCollection = linkCollectionOrOptions; + _baseUrl = options?.baseUrl; + } else { + // Option B: Self-initialize by opening the uLoad database + const opts = linkCollectionOrOptions as { baseUrl?: string } | undefined; + _baseUrl = opts?.baseUrl; + + _store = createLocalStore({ + appId: 'uload', + collections: [ + { + name: 'links', + indexes: [ + 'shortCode', + 'isActive', + 'folderId', + 'order', + 'clickCount', + 'source', + '[folderId+order]', + '[isActive+order]', + ], + }, + ], + }); + + _initPromise = _store.initialize().then(() => { + _linkCollection = _store!.collection('links'); + }); + } +} + +async function ensureReady(): Promise> { + if (_initPromise) { + await _initPromise; + } + if (!_linkCollection) { + throw new Error( + '@manacore/shared-uload not initialized. Call initSharedUload() in your app layout.' + ); + } + return _linkCollection; +} + +/** + * Create a short link from any app. + * The link is inserted into the uLoad local-store and syncs automatically. + */ +export async function createShortLink(options: CreateShortLinkOptions): Promise { + const collection = await ensureReady(); + + const shortCode = options.customCode || generateShortCode(); + const id = crypto.randomUUID(); + const shortUrl = getShortUrl(shortCode, _baseUrl); + const qrCodeUrl = getQrCodeUrl(shortUrl); + + await collection.insert({ + id, + shortCode, + customCode: options.customCode || null, + originalUrl: options.url, + title: options.title || null, + description: options.description || null, + isActive: true, + clickCount: 0, + folderId: null, + order: 0, + source: options.source, + expiresAt: options.expiresAt || null, + qrCodeUrl, + } as UloadLink); + + return { id, shortCode, shortUrl, qrCodeUrl }; +} + +/** + * Check if shared-uload has been initialized. + */ +export function isSharedUloadReady(): boolean { + return _linkCollection !== null || _initPromise !== null; +} + +export function getBaseUrl(): string | undefined { + return _baseUrl; +} diff --git a/packages/shared-uload/src/index.ts b/packages/shared-uload/src/index.ts new file mode 100644 index 000000000..555766f32 --- /dev/null +++ b/packages/shared-uload/src/index.ts @@ -0,0 +1,12 @@ +// Types +export type { UloadLink, CreateShortLinkOptions, CreatedLink, AppSource } from './types'; +export { APP_SOURCE_LABELS } from './types'; + +// Core API +export { initSharedUload, createShortLink, isSharedUloadReady } from './create-link'; + +// Utilities +export { generateShortCode, getQrCodeUrl, getShortUrl, downloadQrCode, QR_API } from './utils'; + +// Components +export { default as ShareModal } from './ShareModal.svelte'; diff --git a/packages/shared-uload/src/types.ts b/packages/shared-uload/src/types.ts new file mode 100644 index 000000000..040f07eb9 --- /dev/null +++ b/packages/shared-uload/src/types.ts @@ -0,0 +1,66 @@ +import type { BaseRecord } from '@manacore/local-store'; + +export interface UloadLink extends BaseRecord { + shortCode: string; + customCode?: string | null; + originalUrl: string; + title?: string | null; + description?: string | null; + isActive: boolean; + password?: string | null; + maxClicks?: number | null; + expiresAt?: string | null; + clickCount: number; + qrCodeUrl?: string | null; + utmSource?: string | null; + utmMedium?: string | null; + utmCampaign?: string | null; + folderId?: string | null; + order: number; + source?: string | null; +} + +export interface CreateShortLinkOptions { + url: string; + title?: string; + customCode?: string; + source: string; + tags?: string[]; + expiresAt?: string; + description?: string; +} + +export interface CreatedLink { + id: string; + shortCode: string; + shortUrl: string; + qrCodeUrl: string; +} + +export type AppSource = + | 'calendar' + | 'contacts' + | 'todo' + | 'chat' + | 'storage' + | 'presi' + | 'mukke' + | 'cards' + | 'picture' + | 'uload' + | 'manacore' + | (string & {}); + +export const APP_SOURCE_LABELS: Record = { + calendar: 'Kalender', + contacts: 'Kontakte', + todo: 'Todo', + chat: 'Chat', + storage: 'Storage', + presi: 'Presi', + mukke: 'Mukke', + cards: 'Cards', + picture: 'Picture', + uload: 'uLoad', + manacore: 'ManaCore', +}; diff --git a/packages/shared-uload/src/utils.ts b/packages/shared-uload/src/utils.ts new file mode 100644 index 000000000..886c34857 --- /dev/null +++ b/packages/shared-uload/src/utils.ts @@ -0,0 +1,29 @@ +export const QR_API = 'https://api.qrserver.com/v1/create-qr-code'; + +export function generateShortCode(length = 6): string { + const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; + let code = ''; + for (let i = 0; i < length; i++) { + code += chars[Math.floor(Math.random() * chars.length)]; + } + return code; +} + +export function getQrCodeUrl(shortUrl: string, size = 400): string { + return `${QR_API}/?size=${size}x${size}&data=${encodeURIComponent(shortUrl)}`; +} + +export function getShortUrl(shortCode: string, baseUrl?: string): string { + const base = + baseUrl || (typeof window !== 'undefined' ? window.location.origin : 'https://ulo.ad'); + return `${base}/${shortCode}`; +} + +export function downloadQrCode(shortCode: string, baseUrl?: string): void { + const shortUrl = getShortUrl(shortCode, baseUrl); + const url = getQrCodeUrl(shortUrl, 400); + const a = document.createElement('a'); + a.href = url; + a.download = `qr-${shortCode}.png`; + a.click(); +} diff --git a/packages/shared-uload/tsconfig.json b/packages/shared-uload/tsconfig.json new file mode 100644 index 000000000..330105708 --- /dev/null +++ b/packages/shared-uload/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "types": ["svelte"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +}