Initial commit: Meshkee dashboards monorepo.
Includes business, customer, and super-admin apps with shared packages and production deploy scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
import { getAccessToken, clearTokens, setTokens } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import { API_BASE_URL } from '../lib/config'
|
||||
import { ensureJpegUploadFile, ensureUploadFile } from '../utils/imageUpload'
|
||||
|
||||
export interface MediaItem {
|
||||
id: string
|
||||
publicUrl: string
|
||||
fileName: string
|
||||
originalFileName: string
|
||||
mimeType: string
|
||||
width: number | null
|
||||
height: number | null
|
||||
}
|
||||
|
||||
function isDataUrl(value: string) {
|
||||
return value.startsWith('data:')
|
||||
}
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/media${suffix}`
|
||||
}
|
||||
|
||||
async function refreshAccessToken() {
|
||||
const refreshToken = localStorage.getItem('meshkee_business_refresh_token')
|
||||
if (!refreshToken) return false
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
clearTokens()
|
||||
return false
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
setTokens(data.accessToken, data.refreshToken)
|
||||
return true
|
||||
}
|
||||
|
||||
function parseUploadError(payload: unknown, status: number) {
|
||||
if (payload && typeof payload === 'object') {
|
||||
const message = (payload as { message?: string | string[] }).message
|
||||
if (Array.isArray(message)) {
|
||||
return message.join(', ')
|
||||
}
|
||||
if (typeof message === 'string' && message) {
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
return `Upload failed with status ${status}`
|
||||
}
|
||||
|
||||
export async function uploadMediaFiles(
|
||||
files: File[],
|
||||
signal?: AbortSignal,
|
||||
): Promise<MediaItem[]> {
|
||||
if (!files.length) return []
|
||||
|
||||
const send = async () => {
|
||||
const formData = new FormData()
|
||||
files.forEach((file) => formData.append('files', file))
|
||||
|
||||
const headers = new Headers()
|
||||
const accessToken = getAccessToken()
|
||||
if (accessToken) {
|
||||
headers.set('Authorization', `Bearer ${accessToken}`)
|
||||
}
|
||||
|
||||
return fetch(`${API_BASE_URL}${businessPath()}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData,
|
||||
signal,
|
||||
})
|
||||
}
|
||||
|
||||
let response = await send()
|
||||
|
||||
if (response.status === 401) {
|
||||
const refreshed = await refreshAccessToken()
|
||||
if (refreshed) {
|
||||
response = await send()
|
||||
}
|
||||
}
|
||||
|
||||
const payload = await response.json().catch(() => null)
|
||||
if (!response.ok) {
|
||||
throw new Error(parseUploadError(payload, response.status))
|
||||
}
|
||||
|
||||
return (payload.items as Array<Record<string, unknown>>).map((item) => ({
|
||||
id: String(item.id),
|
||||
publicUrl: String(item.publicUrl),
|
||||
fileName: String(item.fileName),
|
||||
originalFileName: String(item.originalFileName),
|
||||
mimeType: String(item.mimeType),
|
||||
width: typeof item.width === 'number' ? item.width : null,
|
||||
height: typeof item.height === 'number' ? item.height : null,
|
||||
}))
|
||||
}
|
||||
|
||||
export async function resolveDataUrlToMediaId(
|
||||
value: string | null,
|
||||
filename: string,
|
||||
existingMediaId?: string | null,
|
||||
): Promise<string | null> {
|
||||
if (!value) return null
|
||||
if (!isDataUrl(value)) {
|
||||
return existingMediaId ?? null
|
||||
}
|
||||
|
||||
const file = filename.toLowerCase().endsWith('.png')
|
||||
? await ensureUploadFile(value, filename)
|
||||
: await ensureJpegUploadFile(value, filename)
|
||||
const uploaded = await uploadMediaFiles([file])
|
||||
return uploaded[0]?.id ?? null
|
||||
}
|
||||
|
||||
export async function resolveHtmlEmbeddedDataUrls(html: string): Promise<string> {
|
||||
if (!html.includes('data:')) return html
|
||||
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(html, 'text/html')
|
||||
const images = Array.from(doc.querySelectorAll('img'))
|
||||
|
||||
for (let index = 0; index < images.length; index += 1) {
|
||||
const src = images[index].getAttribute('src')
|
||||
if (!src || !isDataUrl(src)) continue
|
||||
|
||||
const file = await ensureJpegUploadFile(src, `inline-image-${index + 1}.jpg`)
|
||||
const uploaded = await uploadMediaFiles([file])
|
||||
if (uploaded[0]) {
|
||||
images[index].setAttribute('src', uploaded[0].publicUrl)
|
||||
}
|
||||
}
|
||||
|
||||
return doc.body.innerHTML
|
||||
}
|
||||
|
||||
export async function resolveDataUrlsToMediaIds(
|
||||
values: string[],
|
||||
existingMediaIds: string[],
|
||||
): Promise<string[]> {
|
||||
const resolved: string[] = []
|
||||
|
||||
for (let index = 0; index < values.length; index += 1) {
|
||||
const value = values[index]
|
||||
if (isDataUrl(value)) {
|
||||
const file = await ensureJpegUploadFile(value, `product-image-${index + 1}.jpg`)
|
||||
const uploaded = await uploadMediaFiles([file])
|
||||
if (uploaded[0]) resolved.push(uploaded[0].id)
|
||||
} else if (existingMediaIds[index]) {
|
||||
resolved.push(existingMediaIds[index])
|
||||
}
|
||||
}
|
||||
|
||||
return resolved
|
||||
}
|
||||
Reference in New Issue
Block a user