@bigdigital/kiosk-content-sdk
Version:
A Firebase-powered Content Management System SDK optimized for kiosks with offline support, template management, and real-time connection monitoring
186 lines (165 loc) • 4.97 kB
text/typescript
import { z } from "zod";
export const userRoles = ["admin", "editor", "viewer"] as const;
export const fieldTypeEnum = z.enum([
"text",
"number",
"boolean",
"select",
"rich-text",
"image",
"video",
"audio",
"date",
"multi-select",
"color"
]);
export const normalizeGroupName = (name: string): string => {
let words = name.split(/[\s\-_]+/);
if (words.length === 1) {
words = words[0]
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.replace(/([a-zA-Z])(\d)/g, '$1 $2')
.replace(/([a-z])([a-z]*)/gi, (match, first, rest) => `${first}${rest.toLowerCase()}`)
.split(/\s+/);
}
words = words
.filter(word => word.length > 0)
.map(word => word.toLowerCase());
if (words.length === 0) return '';
return words[0] + words.slice(1)
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
};
// Base schemas and types
export const validationSchema = z.object({
required: z.boolean().default(false),
min: z.number().optional(),
max: z.number().optional(),
pattern: z.string().optional(),
customError: z.string().optional()
}).optional();
export const fieldSchema = z.object({
id: z.string(),
name: z.string().min(1, "Field name is required"),
label: z.string(),
type: fieldTypeEnum,
description: z.string().optional(),
defaultValue: z.any().optional(),
options: z.array(z.string()).optional(),
validation: validationSchema,
step: z.number().optional(),
groupId: z.string().optional()
});
export const groupSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string().optional(),
order: z.number(),
fieldIds: z.array(z.string()),
normalizedName: z.string().optional()
});
export const templateSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string(),
type: z.string(),
fields: z.array(fieldSchema),
groups: z.record(z.string(), groupSchema),
ungroupedFieldIds: z.array(z.string()),
groupOrder: z.array(z.string()),
createdBy: z.string(),
createdAt: z.number(),
updatedAt: z.number(),
version: z.number().default(1),
settings: z.object({
cacheStrategy: z.enum(["memory", "local", "none"]).default("memory"),
syncInterval: z.number().optional(),
offlineSupport: z.boolean().default(false),
}).default({
cacheStrategy: "memory",
offlineSupport: false
})
});
export const templateValuesSchema = z.object({
groups: z.record(z.string(), z.record(z.string(), z.any())).default({}),
ungrouped: z.record(z.string(), z.any()).default({}),
_template: z.object({
id: z.string(),
version: z.number()
}).optional()
});
// Content schema with data field
const baseContentSchema = z.object({
id: z.string(),
title: z.string(),
description: z.string(),
type: z.enum(["text", "image", "video", "template"]),
mediaUrl: z.string().optional(),
projectIds: z.array(z.string()).default([]),
createdBy: z.string(),
createdAt: z.number(),
updatedAt: z.number(),
templateId: z.string().optional(),
templateValues: templateValuesSchema.optional(),
templateVersion: z.number().optional(),
version: z.number().default(1),
status: z.enum(["published", "draft", "archived"]).default("draft"),
metadata: z.record(z.string(), z.any()).default({}),
data: z.record(z.string(), z.any()).optional()
});
export const contentSchema = z.preprocess((data: any) => {
if (typeof data !== 'object' || data === null) return data;
const processed = { ...data };
if (processed.published === true) {
processed.status = "published";
}
delete processed.published;
return processed;
}, baseContentSchema);
// Export types
export type Content = z.infer<typeof baseContentSchema>;
export type Field = z.infer<typeof fieldSchema>;
export type Group = z.infer<typeof groupSchema>;
export type Template = z.infer<typeof templateSchema>;
export type FieldType = z.infer<typeof fieldTypeEnum>;
export type TemplateValues = z.infer<typeof templateValuesSchema>;
export interface KioskConfig {
projectId: string;
apiKey: string;
authDomain?: string;
offlineSupport?: boolean;
cacheStrategy?: "memory" | "local" | "none";
syncInterval?: number;
cacheMaxAge?: number;
}
export interface CacheOptions {
ttl?: number;
strategy?: "memory" | "local" | "none";
validateOnLoad?: boolean;
}
export interface SyncOptions {
force?: boolean;
onProgress?: (progress: number) => void;
onError?: (error: Error) => void;
retryAttempts?: number;
}
export interface GroupedTemplateContent {
group: Group & { normalizedName: string };
fields: Field[];
values: Record<string, any>;
}
export interface TemplateContentStructure {
groups: GroupedTemplateContent[];
ungroupedFields: {
fields: Field[];
values: Record<string, any>;
};
}
export interface ContentHookResult {
content: Content | null;
template: Template | null;
loading: boolean;
error: Error | null;
isOnline: boolean;
refresh?: () => void;
}