ad-reflections / server /index.js
Tristan Yu
Rename site to 'Ad Reflections' - update branding in hf-space
3f4cb51
const express = require('express');
const cors = require('cors');
const axios = require('axios');
const cheerio = require('cheerio');
const path = require('path');
const fs = require('fs').promises;
const cron = require('node-cron');
const { v4: uuidv4 } = require('uuid');
const AirtableService = require('./airtable-service');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 7860;
app.use(cors());
app.use(express.json());
// Serve static files from React build in production
if (process.env.NODE_ENV === 'production') {
app.use(express.static(path.join(__dirname, '../client/build')));
}
// Initialize Airtable service
const airtableService = new AirtableService();
// Fallback in-memory storage for examples if Airtable not configured
let transcreationExamples = [];
// Load cached examples from file on startup
const loadCachedExamples = async () => {
try {
const data = await fs.readFile(path.join(__dirname, 'cached-examples.json'), 'utf8');
transcreationExamples = JSON.parse(data);
console.log(`📚 Loaded ${transcreationExamples.length} cached examples`);
} catch (error) {
console.log('📝 No cached examples found, starting fresh');
transcreationExamples = [];
}
};
// Save examples to file
const saveCachedExamples = async () => {
try {
// Sort examples by dateAdded to maintain consistent order
const sortedExamples = [...transcreationExamples].sort((a, b) =>
new Date(a.dateAdded) - new Date(b.dateAdded)
);
// Ensure all examples have consistent structure
const cleanedExamples = sortedExamples.map(example => ({
...example,
// Ensure optional fields are properly handled
status: example.status ?? 'pending',
contributor: example.contributor === undefined ? null : example.contributor,
type: example.type ?? 'slogan',
description: example.description ?? '',
lastModified: example.lastModified ?? example.dateAdded
}));
await fs.writeFile(
path.join(__dirname, 'cached-examples.json'),
JSON.stringify(cleanedExamples, null, 2)
);
console.log(`💾 Saved ${cleanedExamples.length} examples to cache`);
} catch (error) {
console.error('❌ Failed to save examples:', error);
}
};
// Search for transcreation examples online
const searchTranscreationExamples = async (category = '', maxResults = 5) => {
console.log(`🔍 Searching for transcreation examples...`);
try {
// Simulate online search with curated examples
// In a real implementation, this would scrape marketing sites, case studies, etc.
const simulatedResults = await simulateOnlineSearch(category, maxResults);
// Add to our cache
for (const example of simulatedResults) {
// Check if already exists
const exists = transcreationExamples.find(ex =>
ex.english.toLowerCase() === example.english.toLowerCase() &&
ex.brand.toLowerCase() === example.brand.toLowerCase()
);
if (!exists) {
example.id = Date.now().toString() + Math.random().toString(36).substr(2, 9);
example.dateAdded = new Date().toISOString();
example.source = 'Online Search';
transcreationExamples.push(example);
console.log(`✅ Added new example: ${example.brand} - ${example.english}`);
}
}
// Save to cache
await saveCachedExamples();
return simulatedResults;
} catch (error) {
console.error('❌ Search failed:', error);
return [];
}
};
// Simulate online search with realistic examples
const simulateOnlineSearch = async (category = '', maxResults = 5) => {
// All 38 local examples exactly as they are
const allExamples = [
{
"english": "Open Happiness",
"mainland": "畅爽开怀",
"taiwan": "暢爽開懷",
"brand": "Coca-Cola",
"category": "Food & Beverage",
"description": "Coca-Cola's uplifting slogan adaptation celebrating carefree refreshment and open-hearted joy.",
"type": "slogan",
"culturalNote": "Mainland version focuses on personal refreshing experience, Taiwan version emphasizes social sharing aspect valued in Taiwanese culture.",
"status": "verified",
"contributor": ""
},
{
"english": "Because You're Worth It",
"mainland": "你值得拥有",
"taiwan": "因為你值得",
"brand": "L'Oréal",
"category": "Beauty & Cosmetics",
"description": "L'Oréal's empowerment message adapted for different concepts of self-worth and beauty standards.",
"type": "slogan",
"culturalNote": "Mainland focuses on possession/ownership, Taiwan emphasizes inherent worthiness.",
"status": "verified",
"contributor": ""
},
{
"english": "Have a Break, Have a Kit Kat",
"mainland": "轻松一刻,奇巧时刻",
"taiwan": "休息一下,吃個Kit Kat",
"brand": "Kit Kat",
"category": "Food & Beverage",
"description": "Kit Kat's break concept maintaining rhythm while using different approaches to product naming.",
"type": "slogan",
"culturalNote": "Mainland uses localized brand name, Taiwan keeps original brand name with local language structure.",
"status": "verified",
"contributor": ""
},
{
"english": "Think Different",
"mainland": "非同凡想",
"taiwan": "不同凡想",
"brand": "Apple",
"category": "Technology",
"description": "Apple's famous campaign with slight variations - mainland uses more contemporary language, Taiwan preserves traditional elements.",
"type": "slogan",
"culturalNote": "Both versions maintain the creative rebellion message but adapt to local linguistic preferences.",
"status": "verified",
"contributor": ""
},
{
"english": "Connecting People",
"mainland": "科技以人为本",
"taiwan": "科技始終來自於人性",
"brand": "Nokia",
"category": "Technology",
"description": "Nokia's brand promise adapted to different cultural values about technology's role in society.",
"type": "slogan",
"culturalNote": "Mainland emphasizes human-centered technology philosophy, Taiwan emphasizes personal connection.",
"status": "verified",
"contributor": ""
},
{
"english": "Finger Lickin' Good",
"mainland": "吮指回味,自在滋味",
"taiwan": "吮指美味",
"brand": "KFC",
"category": "Food & Beverage",
"description": "KFC's sensory appeal slogan with regional preferences for describing taste intensity.",
"type": "slogan",
"culturalNote": "Both maintain the playful imagery but use different intensifiers for taste description.",
"status": "verified",
"contributor": ""
},
{
"english": "I'm Lovin' It",
"mainland": "我就喜欢",
"taiwan": "我就喜歡",
"brand": "McDonald's",
"category": "Food & Beverage",
"description": "McDonald's global jingle adapted for regional expressions of enjoyment and enthusiasm.",
"type": "slogan",
"culturalNote": "Mainland uses softer expression of liking, Taiwan uses more enthusiastic colloquial expression.",
"status": "verified",
"contributor": ""
},
{
"english": "Life's Good",
"mainland": "让生活更美好",
"taiwan": "讓生活更美好",
"brand": "LG",
"category": "Technology",
"description": "LG's positive lifestyle message adapted to different cultural perspectives on good life.",
"type": "slogan",
"culturalNote": "Mainland suggests improvement, Taiwan emphasizes living life to the fullest.",
"status": "verified",
"contributor": ""
},
{
"english": "Do What You Can't",
"mainland": "战胜不可能",
"taiwan": "挑戰你所不能",
"brand": "Samsung",
"category": "Technology",
"description": "Samsung's defiant call to go beyond limits, translated to rally users in each market to challenge the impossible.",
"type": "slogan",
"culturalNote": "Mainland emphasizes personal utility, Taiwan emphasizes communal sharing of benefits.",
"status": "verified",
"contributor": ""
},
{
"english": "Play Has No Limits",
"mainland": "玩无极限",
"taiwan": "玩樂無設限",
"brand": "PlayStation",
"category": "Entertainment",
"description": "PlayStation's boundless gaming message adapted regionally.",
"type": "slogan",
"culturalNote": "Mainland emphasizes boundlessness, Taiwan emphasizes infinity.",
"status": "verified",
"contributor": ""
},
{
"english": "Melts in Your Mouth, Not in Your Hands",
"mainland": "只溶在口,不溶在手",
"taiwan": "只溶你口,不溶你手",
"brand": "M&M's",
"category": "Food & Beverage",
"description": "M&M's unique selling proposition translated with slight regional variations.",
"type": "slogan",
"culturalNote": "Both versions maintain the dual benefit message with minor character variations.",
"status": "verified",
"contributor": ""
},
{
"english": "Bank for a Changing World",
"mainland": "携手变,通世界",
"taiwan": "攜手變,通世界",
"brand": "BNP Paribas",
"category": "Financial Services",
"description": "BNP's change-focused message adapted regionally.",
"type": "slogan",
"culturalNote": "Mainland emphasizes guidance, Taiwan emphasizes providing choices.",
"status": "verified",
"contributor": "",
"isChineseToEnglish": false
},
{
"english": "The Best or Nothing",
"mainland": "惟有最好",
"taiwan": "惟有最好",
"brand": "Mercedes-Benz",
"category": "Automotive",
"description": "Mercedes' commitment to excellence expressed through different cultural values.",
"type": "slogan",
"culturalNote": "Mainland emphasizes meeting expectations, Taiwan emphasizes uncompromising standards.",
"status": "verified",
"contributor": ""
},
{
"english": "Rise",
"mainland": "打出名堂",
"taiwan": "打出名堂",
"brand": "Nike",
"category": "Sports",
"description": "Nike's aspirational basketball mantra urging players to make a name for themselves and elevate their game.",
"type": "slogan",
"culturalNote": "Both versions maintain the concept of achievement through play.",
"status": "verified",
"contributor": ""
},
{
"english": "Red Bull Gives You Wings",
"mainland": "你的能量超乎你想象",
"taiwan": "Red Bull 給你一對翅膀",
"brand": "Red Bull",
"category": "Food & Beverage",
"description": "Red Bull's energetic message adapted for different markets.",
"type": "slogan",
"culturalNote": "Mainland emphasizes the action of flying, Taiwan emphasizes the gift of wings.",
"status": "verified",
"contributor": ""
},
{
"english": "Save Money. Live Better.",
"mainland": "省钱,省心,好生活",
"taiwan": "省錢,生活變更好",
"brand": "Walmart",
"category": "Retail",
"description": "Another Walmart campaign adapted to different value perceptions.",
"type": "slogan",
"culturalNote": "Both maintain saving concept but with different emphasis on lifestyle benefits.",
"status": "verified",
"contributor": ""
},
{
"english": "The World's Local Bank",
"mainland": "环球金融,地方智慧",
"taiwan": "環球金融,地方智慧",
"brand": "HSBC",
"category": "Financial Services",
"description": "HSBC's global-local positioning adapted for different markets.",
"type": "slogan",
"culturalNote": "Different approaches to expressing the global-local balance.",
"status": "verified",
"contributor": "",
"isChineseToEnglish": false
},
{
"english": "Impossible is Nothing",
"mainland": "没有不可能",
"taiwan": "没有不可能",
"brand": "Adidas",
"category": "Sports",
"description": "Adidas' motivational message adapted to local expressions of possibility.",
"type": "slogan",
"culturalNote": "Mainland uses direct negation of impossibility, Taiwan emphasizes unlimited ability.",
"status": "verified",
"contributor": ""
},
{
"english": "Mercedes-Benz",
"mainland": "奔驰",
"taiwan": "賓士",
"brand": "Mercedes-Benz",
"category": "Automotive",
"description": "Luxury car manufacturer name adapted differently in each region.",
"type": "slogan",
"culturalNote": "Mainland emphasizes speed (\"galloping\"), Taiwan uses phonetic translation of \"Benz\".",
"status": "verified",
"contributor": ""
},
{
"english": "Revlon",
"mainland": "露华浓",
"taiwan": "露華濃",
"brand": "Revlon",
"category": "Beauty & Cosmetics",
"description": "Cosmetics brand name adapted using poetic beauty references.",
"type": "slogan",
"culturalNote": "Both use poetic phrase meaning \"concentrated dew\", evoking natural beauty and luxury.",
"status": "verified",
"contributor": ""
},
{
"english": "BMW",
"mainland": "宝马",
"taiwan": "寶馬",
"brand": "BMW",
"category": "Automotive",
"description": "German automaker name adapted using traditional lucky symbolism.",
"type": "slogan",
"culturalNote": "Both use \"precious horse\", a culturally auspicious symbol, differing only in traditional vs simplified characters.",
"status": "verified",
"contributor": ""
},
{
"english": "MasterCard",
"mainland": "万事达卡",
"taiwan": "萬事達卡",
"brand": "MasterCard",
"category": "Financial Services",
"description": "Global payment brand name adaptation emphasizing universal capability.",
"type": "slogan",
"culturalNote": "Both versions use characters meaning \"everything achievable\", with traditional vs simplified characters being the main difference.",
"status": "verified",
"contributor": "",
"isChineseToEnglish": false
},
{
"english": "Bing",
"mainland": "必应",
"taiwan": "Bing",
"brand": "Microsoft",
"category": "Technology",
"description": "Microsoft's search engine name adapted to reflect responsiveness.",
"type": "slogan",
"culturalNote": "Both versions use characters meaning \"must respond\", maintaining the concept of getting answers.",
"status": "verified",
"contributor": null
},
{
"english": "IKEA",
"mainland": "宜家",
"taiwan": "宜家",
"brand": "IKEA",
"category": "Retail",
"type": "slogan",
"description": "A hybrid transcreation evoking comfort and domestic harmony, with semantic reference to the idiom \"宜室宜家\", reinforcing IKEA's image as a builder of ideal homes.",
"status": "verified",
"contributor": ""
},
{
"english": "Taste The Feeling",
"mainland": "这感觉够爽",
"taiwan": "這感覺夠爽",
"brand": "Coca-Cola",
"category": "Food & Beverage",
"type": "slogan",
"description": "Coca-Cola's effervescent slogan adaptation amplifying the drink's crisp, refreshing thrill.",
"status": "verified",
"contributor": ""
},
{
"english": "Coca-Cola",
"mainland": "可口可乐",
"taiwan": "可口可樂",
"brand": "Coca-Cola",
"category": "Food & Beverage",
"type": "slogan",
"description": "Coca-Cola's joyful phonetic rendering emphasizes taste and delight.",
"status": "verified",
"contributor": null
},
{
"english": "Pepsi-Cola",
"mainland": "百事可乐",
"taiwan": "百事可樂",
"brand": "Pepsi",
"category": "Food & Beverage",
"type": "slogan",
"description": "Pepsi's optimistic phonetic adaptation highlights universal happiness.",
"status": "verified",
"contributor": null
},
{
"english": "Procter & Gamble",
"mainland": "宝洁",
"taiwan": "寶僑",
"brand": "Procter & Gamble",
"category": "Retail",
"type": "slogan",
"description": "P&G's name localized differently—Mainland emphasizes cleanliness; HK/TW favors phonetic approach.",
"status": "verified",
"contributor": ""
},
{
"english": "Land Rover",
"mainland": "路虎",
"taiwan": "荒原路華",
"brand": "Land Rover",
"category": "Automotive",
"type": "slogan",
"description": "Land Rover's adventurous spirit expressed differently, with TW preserving the Rover identity.",
"status": "verified",
"contributor": null
},
{
"english": "Volkswagen",
"mainland": "大众",
"taiwan": "福斯",
"brand": "Volkswagen",
"category": "Automotive",
"type": "slogan",
"description": "Volkswagen's original \"people's car\" concept localized with an auspicious phonetic variant in TW.",
"status": "verified",
"contributor": null
},
{
"english": "Lexus",
"mainland": "雷克萨斯",
"taiwan": "凌志",
"brand": "Lexus",
"category": "Automotive",
"type": "slogan",
"description": "Lexus' luxury status adapted conceptually in TW, using aspiration imagery rather than direct phonetics.",
"status": "verified",
"contributor": null
},
{
"english": "Airbnb",
"mainland": "爱彼迎",
"taiwan": "愛彼迎",
"brand": "Airbnb",
"category": "Technology",
"type": "slogan",
"description": "Airbnb's hospitality-focused identity creatively adapted via concept-driven naming in the translation.",
"status": "verified",
"contributor": null
},
{
"english": "WeChat",
"mainland": "微信",
"brand": "微信",
"category": "Technology",
"type": "slogan",
"description": "WeChat's social communication concept succinctly captured beyond literal translation.",
"status": "verified",
"contributor": "",
"isChineseToEnglish": true
},
{
"english": "Geely",
"mainland": "吉利",
"brand": "吉利",
"category": "Automotive",
"type": "slogan",
"description": "Geely's auspicious Chinese name transcreated phonetically, successfully preserving cultural positivity and international appeal.",
"status": "verified",
"contributor": "",
"isChineseToEnglish": true
},
{
"english": "Build Your Dream",
"mainland": "比亚迪",
"brand": "比亚迪",
"category": "Automotive",
"type": "slogan",
"description": "BYD's original acronym reinterpreted into an aspirational message emphasizing ambition and innovation across global markets.",
"status": "verified",
"contributor": "",
"isChineseToEnglish": true
},
{
"english": "Octopus Card",
"mainland": "八達通",
"brand": "八達通",
"category": "Financial Services",
"type": "slogan",
"description": "Hong Kong's transit pass named for reaching in all directions.",
"status": "verified",
"contributor": "",
"isChineseToEnglish": true
},
{
"english": "Meet Fresh",
"mainland": "鲜芋仙",
"brand": "鲜芋仙",
"category": "Food & Beverage",
"type": "slogan",
"description": "Taiwanese dessert chain's poetic name combining fresh taro with fairy-like imagery.",
"status": "verified",
"contributor": "",
"isChineseToEnglish": true
},
{
"english": "TikTok",
"mainland": "抖音",
"brand": "抖音",
"category": "Technology",
"type": "slogan",
"description": "ByteDance's platform name capturing musical rhythm and sound vibrations.",
"status": "verified",
"contributor": "",
"isChineseToEnglish": true
}
];
// Filter by category if specified
let results = allExamples;
if (category) {
results = allExamples.filter(ex =>
ex.category.toLowerCase().includes(category.toLowerCase())
);
}
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 1500 + Math.random() * 2000));
// Return random subset or all if maxResults is high
const shuffled = results.sort(() => Math.random() - 0.5);
return shuffled.slice(0, Math.min(maxResults, shuffled.length));
};
// Initialize cache on startup and load all examples if none exist
const initializeServer = async () => {
console.log('🚀 Starting server initialization...');
console.log('🔧 Airtable service configured:', airtableService.isConfigured, '(updated)');
if (airtableService.isConfigured) {
console.log('🔗 Airtable configured, checking for existing data...');
try {
// Check if Airtable has examples
console.log('📡 Attempting to connect to Airtable...');
const airtableExamples = await airtableService.getAllExamples();
if (airtableExamples.length === 0) {
console.log('📦 Airtable is empty, importing 38 examples...');
// Load examples from simulateOnlineSearch and import to Airtable
const allExamples = await simulateOnlineSearch('', 38);
console.log(`📦 Generated ${allExamples.length} examples, importing to Airtable...`);
// Log first few examples to verify data
console.log('📋 Sample examples:');
allExamples.slice(0, 3).forEach((ex, i) => {
console.log(` ${i+1}. ${ex.brand} - ${ex.english} (${ex.mainland} / ${ex.taiwan})`);
});
await airtableService.bulkImport(allExamples);
console.log('✅ Successfully imported all examples to Airtable');
} else if (airtableExamples.length < 38) {
console.log(`⚠️ Found only ${airtableExamples.length}/38 examples in Airtable, reimporting all...`);
// Clear and reimport all examples
const allExamples = await simulateOnlineSearch('', 38);
console.log(`📦 Generated ${allExamples.length} examples, reimporting to Airtable...`);
await airtableService.bulkImport(allExamples);
console.log('✅ Successfully reimported all examples to Airtable');
} else {
console.log(`📚 Found ${airtableExamples.length} examples in Airtable`);
}
} catch (error) {
console.error('❌ Failed to initialize Airtable:', error);
console.log('🔄 Falling back to file-based storage...');
// Fall back to file-based storage
await loadCachedExamples();
if (transcreationExamples.length === 0) {
console.log('🔍 No cached examples found, loading all 38 examples...');
await searchTranscreationExamples('', 38);
}
}
} else {
console.log('📁 Using file-based storage...');
await loadCachedExamples();
// If no examples are cached, automatically load all 38 examples
if (transcreationExamples.length === 0) {
console.log('🔍 No cached examples found, loading all 38 examples...');
await searchTranscreationExamples('', 38);
}
}
};
initializeServer();
// API Routes
// Get all examples
app.get('/api/examples', async (req, res) => {
try {
const { category, type, random } = req.query;
let examples;
if (airtableService.isConfigured) {
// Use Airtable
const filters = {};
if (category) filters.category = category;
if (type) filters.type = type;
examples = await airtableService.getExamples(filters);
} else {
// Fallback to in-memory storage
examples = [...transcreationExamples];
// Filter by category
if (category) {
examples = examples.filter(ex =>
ex.category.toLowerCase().includes(category.toLowerCase())
);
}
// Filter by type
if (type) {
examples = examples.filter(ex => ex.type === type);
}
}
// Randomize if requested
if (random === 'true') {
examples = examples.sort(() => Math.random() - 0.5);
}
res.json({
success: true,
data: examples,
total: examples.length
});
} catch (error) {
console.error('❌ Failed to get examples:', error);
res.status(500).json({
success: false,
error: 'Failed to retrieve examples'
});
}
});
// Get random example (or search for new one if cache is low)
app.get('/api/examples/random', async (req, res) => {
try {
let examples;
if (airtableService.isConfigured) {
// Use Airtable
examples = await airtableService.getAllExamples();
} else {
// Fallback to in-memory storage
// If we have few examples, try to find more
if (transcreationExamples.length < 5) {
console.log('🔍 Cache low, searching for new examples...');
await searchTranscreationExamples('', 10);
}
examples = transcreationExamples;
}
if (examples.length === 0) {
return res.json({
success: true,
data: null,
message: 'No examples available yet. Try searching to discover new ones!'
});
}
const randomIndex = Math.floor(Math.random() * examples.length);
const example = examples[randomIndex];
res.json({
success: true,
data: example
});
} catch (error) {
console.error('❌ Failed to get random example:', error);
res.status(500).json({
success: false,
error: 'Failed to get random example'
});
}
});
// Search for new examples online
app.post('/api/examples/search-online', async (req, res) => {
try {
const { category } = req.body;
console.log(`🔍 Online search requested for category: ${category || 'all'}`);
const newExamples = await searchTranscreationExamples(category, 5);
res.json({
success: true,
data: newExamples,
message: `Found ${newExamples.length} new example${newExamples.length !== 1 ? 's' : ''}`,
totalCached: transcreationExamples.length
});
} catch (error) {
console.error('Search error:', error);
res.status(500).json({
success: false,
error: 'Online search failed'
});
}
});
// Get example by ID
app.get('/api/examples/:id', async (req, res) => {
try {
let example;
if (airtableService.isConfigured) {
// Use Airtable - get all examples and find by ID
const examples = await airtableService.getAllExamples();
example = examples.find(ex => ex.id === req.params.id);
} else {
// Fallback to in-memory storage
example = transcreationExamples.find(ex => ex.id === req.params.id);
}
if (!example) {
return res.status(404).json({
success: false,
error: 'Example not found'
});
}
res.json({
success: true,
data: example
});
} catch (error) {
console.error('❌ Failed to get example by ID:', error);
res.status(500).json({
success: false,
error: 'Failed to retrieve example'
});
}
});
// Get categories
app.get('/api/categories', async (req, res) => {
try {
let categories;
if (airtableService.isConfigured) {
const examples = await airtableService.getAllExamples();
categories = [...new Set(examples.map(ex => ex.category))];
} else {
categories = [...new Set(transcreationExamples.map(ex => ex.category))];
}
res.json({
success: true,
data: categories
});
} catch (error) {
console.error('❌ Failed to get categories:', error);
res.status(500).json({
success: false,
error: 'Failed to retrieve categories'
});
}
});
// Get types
app.get('/api/types', async (req, res) => {
try {
let types;
if (airtableService.isConfigured) {
const examples = await airtableService.getAllExamples();
types = [...new Set(examples.map(ex => ex.type))];
} else {
types = [...new Set(transcreationExamples.map(ex => ex.type))];
}
res.json({
success: true,
data: types
});
} catch (error) {
console.error('❌ Failed to get types:', error);
res.status(500).json({
success: false,
error: 'Failed to retrieve types'
});
}
});
// Search examples
app.get('/api/search', async (req, res) => {
try {
const { q } = req.query;
if (!q) {
return res.json({
success: true,
data: [],
total: 0
});
}
let results;
if (airtableService.isConfigured) {
results = await airtableService.searchExamples(q);
} else {
const searchTerm = q.toLowerCase();
results = transcreationExamples.filter(ex =>
ex.english.toLowerCase().includes(searchTerm) ||
ex.mainland.toLowerCase().includes(searchTerm) ||
ex.taiwan?.toLowerCase().includes(searchTerm) ||
ex.brand.toLowerCase().includes(searchTerm) ||
ex.category.toLowerCase().includes(searchTerm) ||
ex.description?.toLowerCase().includes(searchTerm)
);
}
res.json({
success: true,
data: results,
total: results.length
});
} catch (error) {
console.error('❌ Failed to search examples:', error);
res.status(500).json({
success: false,
error: 'Failed to search examples'
});
}
});
// Get database stats
app.get('/api/stats', async (req, res) => {
try {
let stats;
if (airtableService.isConfigured) {
stats = await airtableService.getStats();
} else {
stats = {
totalExamples: transcreationExamples.length,
categories: [...new Set(transcreationExamples.map(ex => ex.category))].length,
types: [...new Set(transcreationExamples.map(ex => ex.type))].length,
lastUpdated: transcreationExamples.length > 0 ?
Math.max(...transcreationExamples.map(ex => new Date(ex.dateAdded || Date.now()).getTime())) : null
};
}
res.json({
success: true,
data: stats
});
} catch (error) {
console.error('❌ Failed to get stats:', error);
res.status(500).json({
success: false,
error: 'Failed to retrieve statistics'
});
}
});
// Health check
app.get('/api/health', (req, res) => {
res.json({
success: true,
message: 'Ad Reflections API is running',
timestamp: new Date().toISOString(),
cachedExamples: transcreationExamples.length
});
});
// Manual Edit API Endpoints
// Add new example
app.post('/api/examples/add', async (req, res) => {
try {
console.log('📝 Received new example request:', JSON.stringify(req.body, null, 2));
// Determine if this is a Chinese to English entry (no taiwan field)
const isChineseToEnglish = req.body.hasOwnProperty('isChineseToEnglish') && req.body.isChineseToEnglish;
// Validate required fields based on direction
const requiredFields = isChineseToEnglish
? ['english', 'mainland', 'brand', 'category', 'type']
: ['english', 'mainland', 'taiwan', 'brand', 'category', 'type'];
const missingFields = requiredFields.filter(field => !req.body[field]);
if (missingFields.length > 0) {
console.error('❌ Missing required fields:', {
missingFields,
receivedFields: Object.keys(req.body),
receivedValues: req.body
});
return res.status(400).json({
success: false,
message: `Missing required fields: ${missingFields.join(', ')}`,
details: {
missingFields,
receivedFields: Object.keys(req.body)
}
});
}
// Create new example with cleaned data
const newExample = {
...req.body,
// Clean strings and handle optional fields
english: req.body.english.trim(),
mainland: req.body.mainland.trim(),
taiwan: isChineseToEnglish ? undefined : req.body.taiwan?.trim(),
brand: req.body.brand.trim(),
category: req.body.category.trim(),
type: req.body.type || 'slogan',
description: req.body.description?.trim() ?? '',
status: req.body.status || 'pending',
contributor: req.body.contributor === undefined ? null : req.body.contributor.trim(),
id: Date.now().toString() + Math.random().toString(36).substring(2),
dateAdded: new Date().toISOString()
};
console.log('✨ Created new example:', JSON.stringify(newExample, null, 2));
let savedExample;
if (airtableService.isConfigured) {
// Use Airtable
savedExample = await airtableService.addExample(newExample);
} else {
// Fallback to in-memory storage
transcreationExamples.push(newExample);
await saveCachedExamples();
savedExample = newExample;
}
res.json({
success: true,
message: 'Example added successfully',
example: savedExample
});
} catch (error) {
console.error('❌ Error adding example:', error);
res.status(500).json({
success: false,
message: 'Error adding example',
error: error.message
});
}
});
// Update existing example
app.put('/api/examples/:id', async (req, res) => {
try {
const { id } = req.params;
console.log(`📝 UPDATE REQUEST for ID: ${id}`);
console.log(`📝 REQUEST BODY:`, JSON.stringify(req.body, null, 2));
let existingExample;
if (airtableService.isConfigured) {
// Use Airtable - get all examples and find by ID
const examples = await airtableService.getAllExamples();
existingExample = examples.find(ex => ex.id === id);
} else {
// Fallback to in-memory storage
const index = transcreationExamples.findIndex(ex => ex.id === id);
existingExample = index !== -1 ? transcreationExamples[index] : null;
}
if (!existingExample) {
console.log(`❌ Example with ID ${id} not found`);
return res.status(404).json({
success: false,
message: 'Example not found'
});
}
console.log(`📝 FOUND EXAMPLE:`, JSON.stringify(existingExample, null, 2));
// Determine if this is a Chinese to English entry (no taiwan field)
const isChineseToEnglish = !req.body.hasOwnProperty('taiwan');
// Validate required fields based on direction
const requiredFields = isChineseToEnglish
? ['english', 'mainland', 'brand', 'category', 'type']
: ['english', 'mainland', 'taiwan', 'brand', 'category', 'type'];
const missingFields = requiredFields.filter(field => !req.body[field]);
if (missingFields.length > 0) {
console.error('❌ Missing required fields:', {
missingFields,
receivedFields: Object.keys(req.body),
receivedValues: req.body
});
return res.status(400).json({
success: false,
message: `Missing required fields: ${missingFields.join(', ')}`,
details: {
missingFields,
receivedFields: Object.keys(req.body)
}
});
}
// Preserve original dateAdded and merge updates
const updatedData = {
...existingExample, // Start with existing data
...req.body, // Merge in updates
id, // Ensure ID doesn't change
dateAdded: existingExample.dateAdded, // Preserve original date
lastModified: new Date().toISOString(), // Add last modified timestamp
// Handle optional fields - if not provided in request, keep existing value
status: req.body.status ?? existingExample.status ?? 'pending',
contributor: req.body.contributor === undefined ? existingExample.contributor : req.body.contributor,
type: req.body.type ?? existingExample.type ?? 'slogan',
description: req.body.description ?? existingExample.description ?? ''
};
let updatedExample;
if (airtableService.isConfigured) {
// Use Airtable
updatedExample = await airtableService.updateExample(id, updatedData);
} else {
// Fallback to in-memory storage
const index = transcreationExamples.findIndex(ex => ex.id === id);
transcreationExamples[index] = updatedData;
await saveCachedExamples();
updatedExample = updatedData;
}
console.log(`📝 UPDATED EXAMPLE:`, JSON.stringify(updatedExample, null, 2));
console.log(`✅ UPDATE COMPLETED for ID: ${id}`);
res.json({
success: true,
message: 'Example updated successfully',
data: updatedExample
});
} catch (error) {
console.error('❌ Error updating example:', error);
res.status(500).json({
success: false,
message: 'Failed to update example',
error: error.message
});
}
});
// Delete example
app.delete('/api/examples/:id', async (req, res) => {
try {
const { id } = req.params;
if (airtableService.isConfigured) {
// Use Airtable
await airtableService.deleteExample(id);
} else {
// Fallback to in-memory storage
const index = transcreationExamples.findIndex(ex => ex.id === id);
if (index === -1) {
return res.status(404).json({
success: false,
error: 'Example not found'
});
}
transcreationExamples.splice(index, 1);
await saveCachedExamples();
}
res.json({
success: true,
message: 'Example deleted successfully'
});
} catch (error) {
console.error('❌ Failed to delete example:', error);
res.status(500).json({
success: false,
error: 'Failed to delete example'
});
}
});
// Serve static files from React build (for production)
if (process.env.NODE_ENV === 'production') {
app.use(express.static(path.join(__dirname, '../client/build')));
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../client/build', 'index.html'));
});
}
app.listen(PORT, () => {
console.log(`🚀 Ad Reflections API running on port ${PORT}`);
console.log(`📊 Cached examples: ${transcreationExamples.length}`);
});
module.exports = app;