Sigue estos pasos para configurar el sistema en Hostinger y conectarlo con Google Sheets.
PARTE A — Google Sheets como Base de Datos
1
Crear el Google Sheet
Ve a sheets.google.com y crea una nueva hoja. Nómbrala TarjetasDigitales. Crea dos hojas dentro: tarjetas y invites.
2
Cabeceras de la hoja "tarjetas"
En la fila 1 escribe estas columnas en orden: id | token | nombre | cargo | empresa | telefono | telefono2 | whatsapp | facebook | instagram | tiktok | telegram | youtube | web | direccion | descripcion | maps | avatar | galeria | qr | estilo | color1 | color2 | fuente | visitas | activo | fecha
3
Cabeceras de la hoja "invites"
Columnas: token | clienteRef | usado | fecha
4
Crear App Script para la API
En el Sheet, ve a Extensiones → Apps Script. Borra el código y pega el script que se incluye al final de este archivo. Luego haz clic en Implementar → Nueva Implementación → Aplicación Web. En "Quién puede acceder" selecciona Cualquier usuario. Copia la URL del Web App — la necesitarás.
5
Imgur para imágenes
Ve a imgur.com/oauth2/addclient, registra una aplicación (tipo: Anonymous usage). Copia el Client ID. Las fotos se subirán a Imgur gratuitamente y se guardará solo la URL en Google Sheets.
PARTE B — Configurar en Hostinger
6
Subir el archivo HTML
En Hostinger, ve a Administrador de Archivos → public_html. Crea una carpeta tarjetadigital (o configura el subdominio apuntando a esa carpeta). Sube este archivo como index.html.
7
Configurar el subdominio
En Hostinger, ve a Dominios → Subdominios. Crea el subdominio tarjetadigital apuntando al directorio donde subiste el archivo. El sistema quedará en tarjetadigital.solucioneseideasbolivia.com.
8
Configurar credenciales en el Panel Admin
Entra al panel Admin → Configuración. Ingresa: la URL del Web App de Apps Script, el ID de tu Imgur Client. El sistema estará listo para funcionar.
9
Primer acceso y credenciales por defecto
Usuario admin por defecto: admin | Contraseña: solucionesideas2024. Cámbia la contraseña inmediatamente desde Panel Admin → Configuración → Cambiar Contraseña.
10
Cómo funciona el flujo completo
1) Admin genera link: tudominio.com?token=ABC123
2) Cliente abre el link, llena el formulario, sube fotos
3) El sistema sube fotos a Imgur, guarda todo en Google Sheets
4) La tarjeta queda disponible en: tudominio.com?id=TARJETA_ID
5) El admin ve la tarjeta en el panel y puede activar/desactivar
Apps Script — Pega este código en Google Apps Script
const SHEET_NAME = 'tarjetas';
const INVITE_SHEET = 'invites';
function doGet(e) {
return handleRequest(e);
}
function doPost(e) {
return handleRequest(e);
}
function handleRequest(e) {
const action = e.parameter.action || (e.postData ? JSON.parse(e.postData.contents).action : '');
let result = {};
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
if (action === 'getCards') result = getCards(ss);
else if (action === 'getCard') result = getCard(ss, e.parameter.id);
else if (action === 'saveCard') result = saveCard(ss, JSON.parse(e.postData.contents));
else if (action === 'updateCard') result = updateCard(ss, JSON.parse(e.postData.contents));
else if (action === 'deleteCard') result = deleteCard(ss, e.parameter.id);
else if (action === 'toggleCard') result = toggleCard(ss, e.parameter.id);
else if (action === 'incrementVisit') result = incrementVisit(ss, e.parameter.id);
else if (action === 'createInvite') result = createInvite(ss, JSON.parse(e.postData.contents));
else if (action === 'getInvites') result = getInvites(ss);
else if (action === 'validateToken') result = validateToken(ss, e.parameter.token);
else if (action === 'markTokenUsed') result = markTokenUsed(ss, e.parameter.token);
else result = { error: 'Acción no encontrada' };
} catch(err) {
result = { error: err.toString() };
}
return ContentService
.createTextOutput(JSON.stringify(result))
.setMimeType(ContentService.MimeType.JSON);
}
function getCards(ss) {
const sheet = ss.getSheetByName(SHEET_NAME);
const data = sheet.getDataRange().getValues();
const headers = data[0];
const rows = data.slice(1).map(row => {
let obj = {};
headers.forEach((h, i) => obj[h] = row[i]);
return obj;
});
return { cards: rows };
}
function getCard(ss, id) {
const sheet = ss.getSheetByName(SHEET_NAME);
const data = sheet.getDataRange().getValues();
const headers = data[0];
const row = data.find((r, i) => i > 0 && r[0] == id);
if (!row) return { error: 'No encontrada' };
let obj = {};
headers.forEach((h, i) => obj[h] = row[i]);
return { card: obj };
}
function saveCard(ss, data) {
const sheet = ss.getSheetByName(SHEET_NAME);
const id = Date.now().toString(36) + Math.random().toString(36).substr(2,5);
const row = [
id, data.token, data.nombre, data.cargo, data.empresa, data.telefono,
data.telefono2, data.whatsapp, data.facebook, data.instagram, data.tiktok,
data.telegram, data.youtube, data.web, data.direccion, data.descripcion,
data.maps, data.avatar, data.galeria, data.qr, data.estilo, data.color1,
data.color2, data.fuente, 0, true, new Date().toISOString()
];
sheet.appendRow(row);
return { success: true, id };
}
function updateCard(ss, data) {
const sheet = ss.getSheetByName(SHEET_NAME);
const rows = sheet.getDataRange().getValues();
for (let i = 1; i < rows.length; i++) {
if (rows[i][0] == data.id) {
const cols = ['id','token','nombre','cargo','empresa','telefono','telefono2','whatsapp','facebook','instagram','tiktok','telegram','youtube','web','direccion','descripcion','maps','avatar','galeria','qr','estilo','color1','color2','fuente','visitas','activo','fecha'];
cols.forEach((col, j) => {
if (data[col] !== undefined) sheet.getRange(i+1, j+1).setValue(data[col]);
});
return { success: true };
}
}
return { error: 'No encontrada' };
}
function deleteCard(ss, id) {
const sheet = ss.getSheetByName(SHEET_NAME);
const rows = sheet.getDataRange().getValues();
for (let i = 1; i < rows.length; i++) {
if (rows[i][0] == id) { sheet.deleteRow(i+1); return { success: true }; }
}
return { error: 'No encontrada' };
}
function toggleCard(ss, id) {
const sheet = ss.getSheetByName(SHEET_NAME);
const rows = sheet.getDataRange().getValues();
for (let i = 1; i < rows.length; i++) {
if (rows[i][0] == id) {
const current = rows[i][25];
sheet.getRange(i+1, 26).setValue(!current);
return { success: true, activo: !current };
}
}
return { error: 'No encontrada' };
}
function incrementVisit(ss, id) {
const sheet = ss.getSheetByName(SHEET_NAME);
const rows = sheet.getDataRange().getValues();
for (let i = 1; i < rows.length; i++) {
if (rows[i][0] == id) {
const visits = (parseInt(rows[i][24]) || 0) + 1;
sheet.getRange(i+1, 25).setValue(visits);
return { success: true, visitas: visits };
}
}
return { error: 'No encontrada' };
}
function createInvite(ss, data) {
const sheet = ss.getSheetByName(INVITE_SHEET);
sheet.appendRow([data.token, data.clienteRef, false, new Date().toISOString()]);
return { success: true };
}
function getInvites(ss) {
const sheet = ss.getSheetByName(INVITE_SHEET);
const data = sheet.getDataRange().getValues();
const headers = data[0];
const rows = data.slice(1).map(row => {
let obj = {};
headers.forEach((h, i) => obj[h] = row[i]);
return obj;
});
return { invites: rows };
}
function validateToken(ss, token) {
const sheet = ss.getSheetByName(INVITE_SHEET);
const rows = sheet.getDataRange().getValues();
for (let i = 1; i < rows.length; i++) {
if (rows[i][0] == token) return { valid: true, usado: rows[i][2], clienteRef: rows[i][1] };
}
return { valid: false };
}
function markTokenUsed(ss, token) {
const sheet = ss.getSheetByName(INVITE_SHEET);
const rows = sheet.getDataRange().getValues();
for (let i = 1; i < rows.length; i++) {
if (rows[i][0] == token) { sheet.getRange(i+1, 3).setValue(true); return { success: true }; }
}
return { error: 'Token no encontrado' };
}
⚠️ Importante: Google Apps Script tiene un límite de 20,000 lecturas/escrituras por día en cuentas gratuitas. Para una agencia en crecimiento, considera actualizar a Google Workspace. Las imágenes en Imgur tienen un límite de 50MB por imagen y son gratuitas para siempre. Para mayor privacidad, puedes usar Cloudinary en lugar de Imgur (también tiene plan gratuito generoso).