Moved things around
Some checks failed
Gitea Actions Demo / build-and-push (push) Failing after 6s

This commit is contained in:
2026-01-21 17:18:58 -05:00
parent a47df7171c
commit a0a059472b
160 changed files with 100 additions and 17 deletions

View File

@@ -0,0 +1,17 @@
export async function parseErrorResponse(res: Response): Promise<{ msg: string; code?: string }> {
try {
const data = await res.json()
return { msg: data.error || data.message || 'Error', code: data.code }
} catch {
const text = await res.text()
return { msg: text || 'Error' }
}
}
export function isEmailValid(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
export function isPasswordStrong(password: string): boolean {
return /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]{8,}$/.test(password)
}

View File

@@ -0,0 +1,30 @@
import { onMounted, onBeforeUnmount, watch } from 'vue'
import type { Ref } from 'vue'
import { eventBus } from './eventBus'
export function useBackendEvents(userId: Ref<string>) {
let eventSource: EventSource | null = null
const connect = () => {
if (eventSource) eventSource.close()
if (userId.value) {
console.log('Connecting to backend events for user:', userId.value)
eventSource = new EventSource(`/events?user_id=${userId.value}`)
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data)
// Emit globally for any component that cares
eventBus.emit(data.type, data)
eventBus.emit('sse', data) // optional: catch-all channel
}
}
}
onMounted(connect)
watch(userId, connect)
onBeforeUnmount(() => {
console.log('Disconnecting from backend events for user:', userId.value)
eventSource?.close()
})
}

View File

@@ -0,0 +1,12 @@
export const MISSING_FIELDS = 'MISSING_FIELDS'
export const EMAIL_EXISTS = 'EMAIL_EXISTS'
export const MISSING_TOKEN = 'MISSING_TOKEN'
export const INVALID_TOKEN = 'INVALID_TOKEN'
export const TOKEN_TIMESTAMP_MISSING = 'TOKEN_TIMESTAMP_MISSING'
export const TOKEN_EXPIRED = 'TOKEN_EXPIRED'
export const MISSING_EMAIL = 'MISSING_EMAIL'
export const USER_NOT_FOUND = 'USER_NOT_FOUND'
export const ALREADY_VERIFIED = 'ALREADY_VERIFIED'
export const MISSING_EMAIL_OR_PASSWORD = 'MISSING_EMAIL_OR_PASSWORD'
export const INVALID_CREDENTIALS = 'INVALID_CREDENTIALS'
export const NOT_VERIFIED = 'NOT_VERIFIED'

View File

@@ -0,0 +1,29 @@
type Callback = (payload: any) => void
class EventBus {
private listeners: Map<string, Callback[]> = new Map()
on(event: string, callback: Callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, [])
}
this.listeners.get(event)!.push(callback)
}
off(event: string, callback: Callback) {
const list = this.listeners.get(event)
if (!list) return
const index = list.indexOf(callback)
if (index !== -1) list.splice(index, 1)
}
emit(event: string, payload?: any) {
const list = this.listeners.get(event)
if (!list) return
list.forEach((callback) => callback(payload))
}
}
export const eventBus = new EventBus()

View File

@@ -0,0 +1,64 @@
export const DEFAULT_IMAGE_CACHE = 'images-v1'
const objectUrlMap = new Map<string, string>()
export async function getCachedImageUrl(
imageId: string,
cacheName = DEFAULT_IMAGE_CACHE,
): Promise<string> {
if (!imageId) throw new Error('imageId required')
// reuse existing object URL if created in this session
const existing = objectUrlMap.get(imageId)
if (existing) return existing
const requestUrl = `/api/image/request/${imageId}`
// Try Cache Storage first
let response: Response | undefined
if ('caches' in window) {
const cache = await caches.open(cacheName)
response = await cache.match(requestUrl)
if (!response) {
const fetched = await fetch(requestUrl)
if (!fetched.ok) throw new Error(`HTTP ${fetched.status}`)
// store a clone in Cache Storage (non-blocking)
cache.put(requestUrl, fetched.clone()).catch((e) => {
console.warn('Cache put failed:', e)
})
response = fetched
}
} else {
const fetched = await fetch(requestUrl)
if (!fetched.ok) throw new Error(`HTTP ${fetched.status}`)
response = fetched
}
const blob = await response.blob()
const objectUrl = URL.createObjectURL(blob)
objectUrlMap.set(imageId, objectUrl)
return objectUrl
}
export function revokeImageUrl(imageId: string) {
const url = objectUrlMap.get(imageId)
if (url) {
try {
URL.revokeObjectURL(url)
} catch (e) {
/* ignore */
}
objectUrlMap.delete(imageId)
}
}
export function revokeAllImageUrls() {
for (const url of objectUrlMap.values()) {
try {
URL.revokeObjectURL(url)
} catch (e) {
/* ignore */
}
}
objectUrlMap.clear()
}

View File

@@ -0,0 +1,124 @@
export interface Task {
id: string
name: string
points: number
is_good: boolean
image_id: string | null
image_url?: string | null // optional, for resolved URLs
}
export const TASK_FIELDS = ['id', 'name', 'points', 'is_good', 'image_id'] as const
export interface Child {
id: string
name: string
age: number
tasks: string[]
rewards: string[]
points: number
image_id: string | null
image_url?: string | null // optional, for resolved URLs
}
export const CHILD_FIELDS = ['id', 'name', 'age', 'tasks', 'rewards', 'points', 'image_id'] as const
export interface Reward {
id: string
name: string
cost: number
points_needed: number
image_id: string | null
image_url?: string | null // optional, for resolved URLs
}
export const REWARD_FIELDS = ['id', 'name', 'cost', 'points_needed', 'image_id'] as const
export interface RewardStatus {
id: string
name: string
points_needed: number
cost: number
redeeming: boolean
image_id: string | null
image_url?: string | null // optional, for resolved URLs
}
export const REWARD_STATUS_FIELDS = [
'id',
'name',
'points_needed',
'cost',
'redeeming',
'image_id',
] as const
export interface PendingReward {
id: string
child_id: string
child_name: string
child_image_id: string | null
child_image_url?: string | null // optional, for resolved URLs
reward_id: string
reward_name: string
reward_image_id: string | null
reward_image_url?: string | null // optional, for resolved URLs
}
export const PENDING_REWARD_FIELDS = [
'id',
'child_id',
'child_name',
'child_image_id',
'reward_id',
'reward_name',
'reward_image_id',
] as const
export interface Event {
type: string
payload:
| ChildModifiedEventPayload
| ChildTaskTriggeredEventPayload
| ChildRewardTriggeredEventPayload
| ChildRewardRequestEventPayload
| ChildTasksSetEventPayload
| ChildRewardsSetEventPayload
| TaskModifiedEventPayload
| RewardModifiedEventPayload
}
export interface ChildModifiedEventPayload {
child_id: string
operation: 'ADD' | 'DELETE' | 'EDIT'
}
export interface ChildTaskTriggeredEventPayload {
task_id: string
child_id: string
points: number
}
export interface ChildRewardTriggeredEventPayload {
task_id: string
child_id: string
points: number
}
export interface ChildRewardRequestEventPayload {
child_id: string
reward_id: string
operation: 'GRANTED' | 'CREATED' | 'CANCELLED'
}
export interface ChildTasksSetEventPayload {
child_id: string
task_ids: string[]
}
export interface ChildRewardsSetEventPayload {
child_id: string
reward_ids: string[]
}
export interface TaskModifiedEventPayload {
task_id: string
operation: 'ADD' | 'DELETE' | 'EDIT'
}
export interface RewardModifiedEventPayload {
reward_id: string
operation: 'ADD' | 'DELETE' | 'EDIT'
}