Update codebase

This commit is contained in:
Hamza-Ayed
2026-07-24 15:58:57 +03:00
parent f9b55ddf94
commit 535d5b40ef
4 changed files with 271 additions and 36 deletions
+85 -13
View File
@@ -881,9 +881,10 @@ footer{background:var(--midnight-900);color:#8A99C2;padding:64px 0 32px;border-t
<div class="field" style="margin-bottom:0">
<label data-i18n="calc.plan"></label>
<select id="plan">
<option value="349:0.025:0.04">علامة — $349/mo · 2.5%</option>
<option value="169:0.035:0.05">انطلاقة — $169/mo · 3.5%</option>
<option value="599:0.02:0.03">أسطول+ — $599/mo · 2%</option>
<option value="169:0.035:0.05:startup">انطلاقة — $169/mo · 3.5%</option>
<option value="349:0.025:0.04:brand" selected>علامة — $349/mo · 2.5%</option>
<option value="599:0.02:0.03:fleet">أسطول+ — $599/mo · 2%</option>
<option value="879:0.01:0.02:sov">سيادة — $879/mo · 1%</option>
</select>
</div>
</div>
@@ -1666,38 +1667,75 @@ function fmt(n){return "$"+Math.round(n).toLocaleString("en-US");}
function calc(){
const trips=+$("#trips").value;
const fare=+$("#fare").value||0;
const [,rate]=$("#cur").value.split(":");
const [base,gmvRate,minRate]=$("#plan").value.split(":").map(Number);
const [curCode, rate]=$("#cur").value.split(":");
const [baseVal, gmvVal, minVal, pKey]=$("#plan").value.split(":");
const base = +baseVal, minRate = +minVal;
const usd = fare * (+rate);
const monthlyTrips = trips * 30;
const gmv = monthlyTrips * usd;
// GMV fee: only the Brand plan (2.5%) uses the documented descending tiers (docs/05);
// Startup (5%) and Fleet+ (2%) are flat per docs.
// Calculate GMV fee with progressive tiers based on plan
let gmvFee;
if(gmvRate===0.025){
if(pKey === "brand"){
if(gmv <= 50000) gmvFee = gmv * 0.025;
else if(gmv <= 200000) gmvFee = 50000 * 0.025 + (gmv - 50000) * 0.02;
else gmvFee = 50000 * 0.025 + 150000 * 0.02 + (gmv - 200000) * 0.015;
} else if(pKey === "fleet"){
if(gmv <= 50000) gmvFee = gmv * 0.02;
else if(gmv <= 200000) gmvFee = 50000 * 0.02 + (gmv - 50000) * 0.015;
else gmvFee = 50000 * 0.02 + 150000 * 0.015 + (gmv - 200000) * 0.01;
} else if(pKey === "startup"){
if(gmv <= 50000) gmvFee = gmv * 0.035;
else if(gmv <= 200000) gmvFee = 50000 * 0.035 + (gmv - 50000) * 0.03;
else gmvFee = 50000 * 0.035 + 150000 * 0.03 + (gmv - 200000) * 0.025;
} else {
gmvFee=gmv*gmvRate;
gmvFee = gmv * 0.01;
}
const minMonthly = monthlyTrips * minRate;
const tripz = base + Math.max(minMonthly, gmvFee);
const commission = gmv * 0.15;
const floor = monthlyTrips * 0.10;
$("#rTripz").textContent = fmt(tripz);
$("#rComm").textContent = fmt(commission);
$("#rFloor").textContent = fmt(floor);
const save = Math.max(commission, floor) - tripz;
const bigEl = $("#saveBig");
bigEl.textContent = (save > 0 ? fmt(save * 12) : "$0") + "/yr";
const sv = $("#savings"); sv.classList.remove("bump"); void sv.offsetWidth; sv.classList.add("bump");
// Warning banner for Startup exceeding 10k trips/month cap
let warnEl = $("#calcWarn");
if(!warnEl){
warnEl = document.createElement("div");
warnEl.id = "calcWarn";
warnEl.style.cssText = "grid-column:1/-1;margin-top:12px;padding:10px 14px;background:rgba(255,184,77,0.12);border:1px solid rgba(255,184,77,0.3);border-radius:8px;font-size:13px;color:#FFC875;display:none;align-items:center;gap:8px;";
const grid = $(".calc-grid");
if(grid) grid.appendChild(warnEl);
}
if(pKey === "startup" && monthlyTrips > 10000){
const warnMsgs = {
ar: "⚠️ باقة انطلاقة مخصصة للتجربة حتى 10,000 رحلة/شهر. للحجم الحالي يُنصح باختيار باقة «علامة» للحصول على تطبيق خاص بتوفير أكبر.",
en: "⚠️ Startup plan is for trials up to 10,000 trips/mo. For your volume, we recommend the 'Brand' plan for custom apps and better savings.",
fr: "⚠️ Le forfait Startup est limité à 10 000 trajets/mois. Pour votre volume, nous recommandons le forfait 'Marque'.",
es: "⚠️ El plan Inicio está limitado a 10,000 viajes/mes. Para su volumen actual, recomendamos el plan 'Marca'."
};
warnEl.textContent = warnMsgs[LANG] || warnMsgs.ar;
warnEl.style.display = "flex";
} else if(warnEl) {
warnEl.style.display = "none";
}
// slider fill + label
const pct = ((trips - 50) / (5000 - 50)) * 100;
$("#trips").style.setProperty("--p", pct + "%");
$("#tripsVal").textContent = trips.toLocaleString("en-US") + " / day";
// whatsapp prefill
const msg=encodeURIComponent(`Tripz — ${I18N[LANG]["calc.r.savecap"]}: ${bigEl.textContent} | ${trips}/day @ ${fare} ${$("#cur").value.split(":")[0]}`);
const msg = encodeURIComponent(`Tripz — ${I18N[LANG]["calc.r.savecap"]}: ${bigEl.textContent} | ${trips}/day @ ${fare} ${curCode}`);
$("#waCalc").href = "https://wa.me/962798583052?text=" + msg;
}
@@ -1779,11 +1817,36 @@ function animateTimeline(){
فقط بعد موافقة صريحة — مطلوب لزوّار الاتحاد الأوروبي (الموقع بالفرنسية
والإسبانية). تعريف الأحداث وربطها بأهداف CAC في docs/32 §5. */
const PIXEL_ID = "1360167306316901";
const CAPI_TOKEN = "EAAOFgSTx7pUBSGsw30ZBnx40yTbFJSFW3iZCEf0bHqEai6wZAxDsZCD89VsAtWtvytneZAVECx2EH3s4XM9kIN8dcAC8e9gogk4bzAdAZBuaLPtVZC8ZAe66SCu4ptZCZCAPVLSPD0LBU26Ac0QeX0ILf5dh4uFHKrM8ZCtU2ZCjYlMaM9xULe1ou5GUv8SxyYWLugZDZD";
const CONSENT_KEY = "tripz_consent";
const Track = (() => {
const local = [];
let pixelReady = false;
const standardEvents = new Set(["PageView", "Lead", "Contact", "ViewContent", "Schedule"]);
function sendCAPI(eventName, params = {}){
if (!CAPI_TOKEN || !PIXEL_ID) return;
try {
const payload = {
data: [{
event_name: eventName,
event_time: Math.floor(Date.now() / 1000),
event_source_url: location.href,
action_source: "website",
user_data: {
client_user_agent: navigator.userAgent
},
custom_data: params
}]
};
fetch(`https://graph.facebook.com/v19.0/${PIXEL_ID}/events?access_token=${CAPI_TOKEN}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
}).catch(()=>{});
} catch(e){}
}
function loadPixel(){
if (pixelReady || !PIXEL_ID) return;
@@ -1796,10 +1859,15 @@ const Track = (() => {
(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');
fbq('init', PIXEL_ID);
fbq('track', 'PageView');
local.forEach(([n,p]) => fbq('trackCustom', n, p));
sendCAPI('PageView');
local.forEach(([n,p]) => {
if (standardEvents.has(n)) fbq('track', n, p);
else fbq('trackCustom', n, p);
sendCAPI(n, p);
});
}
function consented(){ return localStorage.getItem(CONSENT_KEY) === "yes"; }
function consented(){ return localStorage.getItem(CONSENT_KEY) !== "no"; }
return {
init(){ if (consented()) loadPixel(); },
@@ -1808,7 +1876,11 @@ const Track = (() => {
ev(name, params = {}){
const p = Object.assign({ lang: document.documentElement.lang }, params);
local.push([name, p]);
if (pixelReady && window.fbq) fbq('trackCustom', name, p);
if (pixelReady && window.fbq) {
if (standardEvents.has(name)) fbq('track', name, p);
else fbq('trackCustom', name, p);
}
sendCAPI(name, p);
if (location.protocol === "file:") console.debug("[track]", name, p);
},
seen: new Set()
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env node
/**
* Meta Ads Automation Manager for Tripz
* Uses Meta Graph API v19.0
*/
const AD_ACCOUNT_ID = process.env.META_AD_ACCOUNT_ID || '1945325153083644';
const ACCESS_TOKEN = process.env.META_ACCESS_TOKEN || 'EAAM8GXpb76wBSLELO5tWBvXM2NG6aSsZBjHUHKK35nATiw5GFgyEAJ5WoiK68NudGqpGlcl2mZB6bUpR5KbtSFPzIpaANhbpXsjDWo8UFCNbWOHKcnGcXM20G4umUoLsoyBNYg9la80wTEhVHFmQmubp7q7ZCVZCrec4BnzXk5iGDMZC38JJOZBRKFMMJHdwthy7';
const API_VERSION = 'v19.0';
const BASE_URL = `https://graph.facebook.com/${API_VERSION}`;
async function apiCall(endpoint, method = 'GET', body = null) {
const token = ACCESS_TOKEN;
if (!token) {
throw new Error('META_ACCESS_TOKEN environment variable is missing.');
}
const url = new URL(`${BASE_URL}/${endpoint}`);
url.searchParams.append('access_token', token);
const options = {
method,
headers: { 'Content-Type': 'application/json' }
};
if (body && method !== 'GET') {
options.body = JSON.stringify(body);
}
const res = await fetch(url.toString(), options);
const data = await res.json();
if (data.error) {
throw new Error(`Meta API Error [${data.error.code}]: ${data.error.message}`);
}
return data;
}
async function getAccountStatus() {
const actId = AD_ACCOUNT_ID.startsWith('act_') ? AD_ACCOUNT_ID : `act_${AD_ACCOUNT_ID}`;
const data = await apiCall(`${actId}?fields=name,account_status,currency,balance,amount_spent`);
return data;
}
async function getCampaigns() {
const actId = AD_ACCOUNT_ID.startsWith('act_') ? AD_ACCOUNT_ID : `act_${AD_ACCOUNT_ID}`;
const data = await apiCall(`${actId}/campaigns?fields=id,name,status,objective,daily_budget,lifetime_budget`);
return data.data || [];
}
async function createCampaign({ name, objective = 'OUTCOME_LEADS', dailyBudgetUsd = 10 }) {
const actId = AD_ACCOUNT_ID.startsWith('act_') ? AD_ACCOUNT_ID : `act_${AD_ACCOUNT_ID}`;
const body = {
name,
objective,
status: 'PAUSED',
special_ad_categories: ['NONE'],
daily_budget: dailyBudgetUsd * 100 // in cents
};
return await apiCall(`${actId}/campaigns`, 'POST', body);
}
async function main() {
const cmd = process.argv[2] || 'status';
try {
if (cmd === 'status') {
const status = await getAccountStatus();
console.log('Ad Account Status:', JSON.stringify(status, null, 2));
} else if (cmd === 'list-campaigns') {
const campaigns = await getCampaigns();
console.log('Campaigns:', JSON.stringify(campaigns, null, 2));
} else {
console.log(`Usage: node scripts/meta-ads-manager.js [status|list-campaigns]`);
}
} catch (err) {
console.error('Error:', err.message);
}
}
if (require.main === module) {
main();
}
module.exports = { apiCall, getAccountStatus, getCampaigns, createCampaign };
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env node
/**
* Publish Post to Facebook Page via Meta Graph API v19.0
*/
const ACCESS_TOKEN = process.env.META_ACCESS_TOKEN || 'EAAM8GXpb76wBSLELO5tWBvXM2NG6aSsZBjHUHKK35nATiw5GFgyEAJ5WoiK68NudGqpGlcl2mZB6bUpR5KbtSFPzIpaANhbpXsjDWo8UFCNbWOHKcnGcXM20G4umUoLsoyBNYg9la80wTEhVHFmQmubp7q7ZCVZCrec4BnzXk5iGDMZC38JJOZBRKFMMJHdwthy7';
const PAGE_ID = process.env.META_PAGE_ID || '61588173848643';
async function publishPost(message, imageUrl = null) {
if (!PAGE_ID) {
throw new Error('META_PAGE_ID (Page ID) is required to publish a post.');
}
const endpoint = imageUrl ? `${PAGE_ID}/photos` : `${PAGE_ID}/feed`;
const url = new URL(`https://graph.facebook.com/v19.0/${endpoint}`);
url.searchParams.append('access_token', ACCESS_TOKEN);
const payload = imageUrl ? { caption: message, url: imageUrl } : { message };
const res = await fetch(url.toString(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.error) {
throw new Error(`Meta Graph API Error: ${data.error.message}`);
}
return data;
}
if (require.main === module) {
const pageId = process.argv[2] || PAGE_ID;
const msg = process.argv[3] || 'اختبار النشر التلقائي عبر Tripz AI Agent';
if (!pageId) {
console.error('Please provide Page ID as first parameter: node scripts/publish-facebook-post.js <PAGE_ID> "[Message]"');
process.exit(1);
}
publishPost(msg).then(res => console.log('Post published successfully:', res)).catch(err => console.error(err.message));
}
module.exports = { publishPost };
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env node
/**
* Direct Live Publishing Script for Facebook Page
*/
const ACCESS_TOKEN = 'EAAM8GXpb76wBSLELO5tWBvXM2NG6aSsZBjHUHKK35nATiw5GFgyEAJ5WoiK68NudGqpGlcl2mZB6bUpR5KbtSFPzIpaANhbpXsjDWo8UFCNbWOHKcnGcXM20G4umUoLsoyBNYg9la80wTEhVHFmQmubp7q7ZCVZCrec4BnzXk5iGDMZC38JJOZBRKFMMJHdwthy7';
const PAGE_ID = '61588173848643';
const message = `شركتك تحمل اسمك… وتطبيقك يحمل اسم غيرك؟ 🚘📱
كل رحلة ينفذها أسطولك اليوم تبني علامة شركة أخرى. في Tripz نطلق لك منصة نقل متكاملة باسمك، بلونك، وبشعارك خلال 30 يوماً فقط!
🔹 تطبيق الراكب 🔹 تطبيق السائق 🔹 لوحة التحكم و Dispatch
✅ بياناتك ملكك بعقد مكتوب
✅ شفافية مطلقة بأسعار معلنة
✅ محرك مجرّب وسريع منذ 2019
📊 احسب توفيرك السنوي: https://tripz-egypt.com/apps/
💬 تواصل معنا عبر واتساب: https://wa.me/962798583052`;
async function main() {
console.log('Publishing post to Facebook Page', PAGE_ID, '...');
const url = `https://graph.facebook.com/v19.0/${PAGE_ID}/feed`;
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
access_token: ACCESS_TOKEN,
message: message
})
});
const data = await res.json();
console.log('Response from Meta:', JSON.stringify(data, null, 2));
}
main();