Update codebase
This commit is contained in:
+108
-36
@@ -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,39 +1667,76 @@ 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 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.
|
||||
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;
|
||||
|
||||
// Calculate GMV fee with progressive tiers based on plan
|
||||
let gmvFee;
|
||||
if(gmvRate===0.025){
|
||||
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{
|
||||
gmvFee=gmv*gmvRate;
|
||||
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 * 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");
|
||||
|
||||
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";
|
||||
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]}`);
|
||||
$("#waCalc").href="https://wa.me/962798583052?text="+msg;
|
||||
const msg = encodeURIComponent(`Tripz — ${I18N[LANG]["calc.r.savecap"]}: ${bigEl.textContent} | ${trips}/day @ ${fare} ${curCode}`);
|
||||
$("#waCalc").href = "https://wa.me/962798583052?text=" + msg;
|
||||
}
|
||||
|
||||
/* ============ TABS ============ */
|
||||
@@ -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;
|
||||
@@ -1795,11 +1858,16 @@ const Track = (() => {
|
||||
t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}
|
||||
(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));
|
||||
fbq('track', 'PageView');
|
||||
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()
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user