@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
291 lines • 11.8 kB
JavaScript
import { initializeApp, getApp } from "firebase/app";
import { getFirestore, collection, query, where, getDocs, doc, getDoc } from "firebase/firestore";
import { templateSchema, contentSchema, fieldSchema } from "./types";
// Helper function to find field by id
function findFieldById(fields, fieldId) {
return fields.find(field => field.id === fieldId);
}
// Normalize group names to proper camelCase
function normalizeGroupName(name) {
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('');
}
function processTemplateValues(template, rawData, content) {
console.log("Raw content being processed:", rawData);
console.log("Template being used:", {
id: template.id,
name: template.name,
fields: template.fields,
groups: template.groups
});
const result = {
groups: {},
ungrouped: {}
};
if (!rawData) {
console.log('No raw data provided');
return result;
}
// First, try to get values from templateValues if they exist
let existingValues = rawData.templateValues || {};
console.log("Existing template values:", existingValues);
// Process fields in groups
Object.entries(template.groups || {}).forEach(([groupId, group]) => {
// Always generate and use the proper camelCase name
const normalizedName = normalizeGroupName(group.name);
console.log(`Processing group ${group.name} -> ${normalizedName}`);
// Store the normalizedName back to the group for consistency
group.normalizedName = normalizedName;
// Initialize group
result.groups[normalizedName] = {};
// Check multiple locations for values in order of precedence
const groupValues = existingValues.groups?.[normalizedName] ||
existingValues.groups?.[group.name] ||
rawData[normalizedName] ||
rawData[group.name] ||
{};
console.log(`Group values found for ${normalizedName}:`, groupValues);
// Process fields in this group
group.fieldIds.forEach(fieldId => {
const field = findFieldById(template.fields, fieldId);
if (field) {
const normalizedFieldName = field.name.charAt(0).toLowerCase() + field.name.slice(1);
// Try to get value from various locations
const fieldValue = groupValues[normalizedFieldName] ||
groupValues[field.name] ||
rawData[normalizedFieldName] ||
rawData[field.name] ||
field.defaultValue ||
null;
console.log(`Field ${field.name} -> ${normalizedFieldName} in group ${normalizedName}:`, {
value: fieldValue,
source: groupValues[normalizedFieldName] ? 'normalized group' :
groupValues[field.name] ? 'original group' :
rawData[normalizedFieldName] ? 'normalized root' :
rawData[field.name] ? 'original root' :
field.defaultValue ? 'default' : 'null'
});
result.groups[normalizedName][normalizedFieldName] = fieldValue;
}
});
});
// Process ungrouped fields
const groupedFieldIds = new Set(Object.values(template.groups || {})
.flatMap(group => group.fieldIds));
template.fields.forEach(field => {
if (!groupedFieldIds.has(field.id)) {
const normalizedFieldName = field.name.charAt(0).toLowerCase() + field.name.slice(1);
result.ungrouped[normalizedFieldName] = rawData[normalizedFieldName] ||
rawData[field.name] ||
existingValues.ungrouped?.[normalizedFieldName] ||
existingValues.ungrouped?.[field.name] ||
field.defaultValue ||
null;
}
});
console.log("Final processed template values:", result);
return result;
}
export class KioskClient {
constructor(config) {
try {
this.app = getApp();
}
catch {
this.app = initializeApp({
apiKey: config.apiKey,
authDomain: config.authDomain || `${config.projectId}.firebaseapp.com`,
projectId: config.projectId,
});
}
this.db = getFirestore(this.app);
}
async getTemplates() {
const templatesRef = collection(this.db, "templates");
const snapshot = await getDocs(templatesRef);
const templates = snapshot.docs.map(doc => {
const data = doc.data();
// Ensure fields is an array
if (!Array.isArray(data.fields)) {
data.fields = Object.values(data.fields);
}
// Ensure all groups have normalizedName in camelCase
if (data.groups) {
Object.values(data.groups).forEach((group) => {
group.normalizedName = normalizeGroupName(group.name);
});
}
return { id: doc.id, ...data };
});
return templates;
}
async getTemplateById(templateId) {
const docRef = doc(this.db, "templates", templateId);
const docSnap = await getDoc(docRef);
if (!docSnap.exists()) {
console.log(`Template ${templateId} not found`);
return null;
}
const data = docSnap.data();
// Ensure fields is an array
if (!Array.isArray(data.fields)) {
data.fields = Object.values(data.fields);
}
// Ensure all groups have normalizedName in camelCase
if (data.groups) {
Object.values(data.groups).forEach((group) => {
group.normalizedName = normalizeGroupName(group.name);
});
}
const template = { id: docSnap.id, ...data };
console.log(`Retrieved template ${templateId}:`, template);
return template;
}
async getPublishedContent() {
const templates = await this.getTemplates();
console.log(`Retrieved ${templates.length} templates`);
const contentRef = collection(this.db, "contents");
const publishedQuery = query(contentRef, where("published", "==", true));
const snapshot = await getDocs(publishedQuery);
console.log(`Found ${snapshot.size} published content items`);
return Promise.all(snapshot.docs.map(async (doc) => {
const rawData = doc.data();
console.log(`Processing content ${doc.id}:`, {
templateId: rawData.templateId,
rawValues: rawData
});
console.log("Raw content data from Firestore:", rawData);
const content = {
id: doc.id,
...rawData,
templateValues: {
groups: {},
ungrouped: {}
}
};
if (content.templateId) {
const template = templates.find(t => t.id === content.templateId);
if (template) {
const templateValues = processTemplateValues(template, rawData, content);
console.log("Final processed template values:", templateValues);
content.templateValues = templateValues;
console.log(`Processed content ${doc.id} template values:`, content.templateValues);
}
}
return content;
}));
}
async getContentByProject(projectId) {
const templates = await this.getTemplates();
const contentRef = collection(this.db, "contents");
const projectQuery = query(contentRef, where("published", "==", true), where("projectIds", "array-contains", projectId));
const snapshot = await getDocs(projectQuery);
return Promise.all(snapshot.docs.map(async (doc) => {
const rawData = doc.data();
const content = {
id: doc.id,
...rawData,
templateValues: {
groups: {},
ungrouped: {}
}
};
if (content.templateId) {
const template = templates.find(t => t.id === content.templateId);
if (template) {
content.templateValues = processTemplateValues(template, rawData, content);
}
}
return content;
}));
}
async getContentWithTemplate(contentId) {
const docRef = doc(this.db, "contents", contentId);
const docSnap = await getDoc(docRef);
if (!docSnap.exists()) {
throw new Error("Content not found");
}
const rawData = docSnap.data();
const content = {
id: docSnap.id,
...rawData,
templateValues: {
groups: {},
ungrouped: {}
}
};
if (!content.templateId) {
return { content, template: null };
}
const template = await this.getTemplateById(content.templateId);
if (!template) {
return { content, template: null };
}
content.templateValues = processTemplateValues(template, rawData, content);
return {
content,
template,
groupedContent: this.getGroupedTemplateContent(template, content)
};
}
async getGroupedTemplateContent(template, content) {
const templateValues = content.templateValues ?? { groups: {}, ungrouped: {} };
const groups = (template.groupOrder || [])
.map(groupId => {
const group = template.groups[groupId];
if (!group)
return null;
const normalizedName = normalizeGroupName(group.name);
const fields = group.fieldIds
.map(fieldId => findFieldById(template.fields, fieldId))
.filter((field) => field !== undefined);
return {
group: {
...group,
normalizedName,
},
fields,
values: templateValues.groups[normalizedName] ?? {}
};
})
.filter((group) => group !== null);
const ungroupedFieldIds = template.fields
.filter(field => !Object.values(template.groups)
.some(group => group.fieldIds?.includes(field.id)))
.map(field => field);
return {
groups,
ungroupedFields: {
fields: ungroupedFieldIds,
values: templateValues.ungrouped ?? {}
}
};
}
}
export function isValidTemplate(template) {
const result = templateSchema.safeParse(template);
return result.success;
}
export function isValidContent(content) {
const result = contentSchema.safeParse(content);
return result.success;
}
export function validateTemplateField(field) {
const result = fieldSchema.safeParse(field);
return result.success;
}
//# sourceMappingURL=client.js.map