This project powers the QR landing page at join.almalekicecream.com.
The site now supports a time-based prize entry flow without changing the QR URL. The homepage stays on the same URL, and a prize CTA appears only when the campaign is active in Google Sheets.
- Homepage content is now fetched on the server for a faster first render and no client-side loading spinner.
- The homepage can show a prize CTA when campaign settings are enabled and within the active time window.
- A new page at
/prize-entryshows the form. - A new API route at
/api/prize-entrysends submitted entries to a second Google Apps Script endpoint. - Entries are kept in a separate Google document from the editable content labels.
Copy .env.local.example to .env.local and fill in these values:
CONTENT_URL=https://script.google.com/macros/s/YOUR_CONTENT_SCRIPT_ID/exec
ENTRY_SUBMIT_URL=https://script.google.com/macros/s/YOUR_ENTRY_SCRIPT_ID/execCONTENT_URL is the existing Apps Script endpoint that returns page labels.
ENTRY_SUBMIT_URL is a new Apps Script endpoint that accepts prize-entry form submissions.
Keep the current labels document as the source of truth for homepage content and campaign control.
If your current sheet is a key/value sheet, add new rows with these keys.
If your current sheet is a header-based sheet, add new columns with these names.
The existing required content keys stay the same:
taglinetagline_notepromo_textpromo_notewhatsapp_labelwhatsapp_subwhatsapp_urltiktok_labeltiktok_subtiktok_urlinstagram_labelinstagram_subinstagram_url
Add these new campaign keys to the same content output:
campaign_idcampaign_enabledcampaign_button_labelcampaign_form_titlecampaign_form_descriptioncampaign_success_message
Optional scheduling keys if you ever want timed windows later:
campaign_start_atcampaign_end_at
Recommended values:
campaign_id:summer-hour-1campaign_enabled:TRUEorFALSEcampaign_button_label:ادخل معلوماتك للفوزcampaign_form_title:ادخل معلوماتك للفوز بجوائز قيمةcampaign_form_description:املأ النموذج التالي وسنتواصل معك عند السحب.campaign_success_message:تم تسجيل معلوماتك بنجاح، بالتوفيق.campaign_start_at: optional, for example2026-05-15T18:00:00+03:00campaign_end_at: optional, for example2026-05-15T19:00:00+03:00
The CTA only appears when all of these are true:
campaign_enabledis oncampaign_button_labelis filledcampaign_form_titleis filled- Current time is after
campaign_start_atwhen a start time exists - Current time is before
campaign_end_atwhen an end time exists
For the recommended manual workflow, leave campaign_start_at and campaign_end_at empty and control visibility only with campaign_enabled.
If the campaign fields are empty, the homepage behaves normally and no CTA is shown.
Create a new Google Sheet document only for form submissions.
Create one tab named entries with this header row:
campaign_idcampaign_titlefirst_namelast_namephone_numbersubmitted_atsource_pathrefereruser_agent
This keeps customer data separate from editable content labels.
Open the new entries Google Sheet, then create an Apps Script project and deploy it as a web app.
Use this script as the starting point:
const SHEET_NAME = 'entries';
function doPost(e) {
try {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
if (!sheet) {
return jsonResponse({ ok: false, error: 'Entries sheet not found' }, 500);
}
const payload = JSON.parse(e.postData.contents || '{}');
sheet.appendRow([
payload.campaign_id || '',
payload.campaign_title || '',
payload.first_name || '',
payload.last_name || '',
payload.phone_number || '',
payload.submitted_at || new Date().toISOString(),
payload.source_path || '',
payload.referer || '',
payload.user_agent || '',
]);
return jsonResponse({ ok: true });
} catch (error) {
return jsonResponse({ ok: false, error: String(error) }, 500);
}
}
function jsonResponse(data, status) {
const output = ContentService.createTextOutput(JSON.stringify(data));
output.setMimeType(ContentService.MimeType.JSON);
return output;
}Deploy steps:
- Click
Deploy. - Choose
New deployment. - Select
Web app. - Set
Execute astoMe. - Set access to
AnyoneorAnyone with the link. - Copy the web app URL.
- Put that URL into
ENTRY_SUBMIT_URL.
Your current content Apps Script should keep returning the existing fields and now also return the new campaign fields.
The app expects a flat JSON object like this:
{
"tagline": "لم يحالفك الحظ اليوم...",
"tagline_note": "القادم أجمل",
"promo_text": "ابتسم... الدنيا ما بتقاوم اللي يضحك",
"promo_note": "كن جزءاً من عائلتنا",
"whatsapp_label": "قناة الواتساب",
"whatsapp_sub": "لم ترغب بأن تكون جزءاً من التجربة...",
"whatsapp_url": "https://...",
"tiktok_label": "تيك توك",
"tiktok_sub": "عائلة ايس كريم الملك تكبر فيك",
"tiktok_url": "https://...",
"instagram_label": "انستغرام",
"instagram_sub": "ستوريك يسعدنا",
"instagram_url": "https://...",
"campaign_id": "summer-hour-1",
"campaign_enabled": true,
"campaign_start_at": "2026-05-15T18:00:00+03:00",
"campaign_end_at": "2026-05-15T19:00:00+03:00",
"campaign_button_label": "ادخل معلوماتك للفوز",
"campaign_form_title": "ادخل معلوماتك للفوز بجوائز قيمة",
"campaign_form_description": "املأ النموذج التالي وسنتواصل معك عند السحب.",
"campaign_success_message": "تم تسجيل معلوماتك بنجاح، بالتوفيق."
}When the user submits the form, the site sends this JSON payload to ENTRY_SUBMIT_URL:
{
"campaign_id": "summer-hour-1",
"campaign_title": "ادخل معلوماتك للفوز بجوائز قيمة",
"first_name": "Ali",
"last_name": "Hussein",
"phone_number": "+966500000000",
"submitted_at": "2026-05-15T15:03:22.311Z",
"source_path": "/prize-entry",
"referer": "https://join.almalekicecream.com/prize-entry",
"user_agent": "Mozilla/5.0 ..."
}npm install
npm run devOpen http://localhost:3000.
The feature was added with minimal runtime cost:
- Homepage content now loads on the server instead of waiting for a client fetch.
- The prize form is only rendered when the campaign is active.
- No polling, intervals, or heavy analytics logic were added.
- The form page uses simple native inputs and one POST request on submit.
Run:
npm run lint