Free Shipping for orders above 1200 EGP10% OFF All products for a limited time use coupon *NEWSITE10*

Customize your figure

صمم قطعتك — Layrdصمم قطعتك بنفسكطبقة بطبقة، نُصممها خصيصاً لك.

اختر المجسم، حدد اللون والمقاس، وأضف تفاصيلك. سنقوم بطباعتها عند الطلب ونراسلك بالشكل النهائي قبل بدء الطباعة.

معاينة حية للتصميم

0%


LAYER 0 / 48

0.00mm

01اختر المجسم

يرجى اختيار مجسم للمتابعة

02اللون (الفينيش)

يرجى اختيار اللون أوصف اللون اللي محتاجه

يرجى كتابة وصف للون

03المقاس

يرجى اختيار المقاساختيار المقاس يغير وقت الطباعة والتكلفة بناءً عليه.

04التفاصيل (اختياري)

ملاحظات إضافية

صورة مرجعية

اضغط لرفع صورة من جهازك

05بيانات التواصل

الاسم بالكامل

يرجى إدخال اسمك

رقم الواتساب

يرجى إدخال رقم هاتف مصري صحيح (11 رقم يبدأ بـ 01)

06الكمية

1

+

ملخص الطلب

المجسم

اللون

المقاس

التفاصيل

الصورة

لا يوجد

الكمية

1

الإجمالي (جنيه)

0

أكمل البيانات للمتابعة

سيتم إرسال طلبك لقاعدة البيانات وسنتواصل معك لتأكيد السعر وموعد الاستلام.

// --- DETECT LANGUAGE (Smart Detection) --- const htmlTag = document.documentElement; const isAr = htmlTag.dir === "rtl" || htmlTag.lang.startsWith("ar") || window.location.href.includes("/ar"); // --- DICTIONARY (Translation Dictionary) --- const dict = { tag: isAr ? "مجسمات 3D مخصصة · القاهرة" : "CUSTOM 3D-PRINTED PIECES · CAIRO", eyebrow: isAr ? "صمم قطعتك بنفسك" : "Build your own", heroTitle: isAr ? "طبقة بطبقة، نُصممها خصيصاً لك." : "Layer by layer, made yours.", heroP: isAr ? "اختر المجسم، حدد اللون والمقاس، وأضف تفاصيلك. سنقوم بطباعتها عند الطلب ونراسلك بالشكل النهائي قبل بدء الطباعة." : "Pick a piece, choose your finish, add your details. We print it on demand and message you the final look before it goes on the bed.", livePrev: isAr ? "معاينة حية للتصميم" : "Live build preview", step1Title: isAr ? "اختر المجسم" : "Choose your piece", step1Err: isAr ? "يرجى اختيار مجسم للمتابعة" : "Please select a piece to continue", step2Title: isAr ? "اللون (الفينيش)" : "Finish", step2Err: isAr ? "يرجى اختيار اللون" : "Please select a finish", customColLbl: isAr ? "أوصف اللون اللي محتاجه" : "Describe your custom color", customColPlc: isAr ? "مثال: أخضر غامق مطفي" : "e.g. matte forest green", customColErr: isAr ? "يرجى كتابة وصف للون" : "Please describe your custom color", step3Title: isAr ? "المقاس" : "Size", step3Err: isAr ? "يرجى اختيار المقاس" : "Please select a size", sizeHelper: isAr ? "اختيار المقاس يغير وقت الطباعة والتكلفة بناءً عليه." : "Size changes print time and material — price scales with it.", step4Title: isAr ? "التفاصيل (اختياري)" : "Personalize (Optional)", detailsLbl: isAr ? "ملاحظات إضافية" : "Details", detailsPlc: isAr ? "اكتب أي ملاحظات أو تفاصيل للمجسم" : "Tell us what to put on it", photoLbl: isAr ? "صورة مرجعية" : "Reference photo", photoBox: isAr ? "اضغط لرفع صورة من جهازك" : "Tap to attach a photo", step5Title: isAr ? "بيانات التواصل" : "Contact details", nameLbl: isAr ? "الاسم بالكامل" : "Your Name", namePlc: isAr ? "مثال: عمر أحمد" : "e.g. Omar", nameErr: isAr ? "يرجى إدخال اسمك" : "Please enter your name", phoneLbl: isAr ? "رقم الواتساب" : "WhatsApp Number", phonePlc: isAr ? "مثال: 01012345678" : "01012345678", phoneErr: isAr ? "يرجى إدخال رقم هاتف مصري صحيح (11 رقم يبدأ بـ 01)" : "Please enter a valid 11-digit Egyptian number (starts with 01)", step6Title: isAr ? "الكمية" : "Quantity", sumTitle: isAr ? "ملخص الطلب" : "Order summary", sumPiece: isAr ? "المجسم" : "Piece", sumFinish: isAr ? "اللون" : "Finish", sumSize: isAr ? "المقاس" : "Size", sumDetails: isAr ? "التفاصيل" : "Details", sumPhoto: isAr ? "الصورة" : "Photo", sumQty: isAr ? "الكمية" : "Quantity", sumTotal: isAr ? "الإجمالي (جنيه)" : "Total (EGP)", btnDefault: isAr ? "أكمل البيانات للمتابعة" : "Complete details to continue", btnReady: isAr ? "تأكيد الطلب" : "Submit Order", btnSubmit: isAr ? "جاري الإرسال... ⏳" : "Submitting order... ⏳", btnSuccess: isAr ? "تم استلام طلبك! اطلب مجسم جديد ↺" : "Order received! Start a new order ↺", btnError: isAr ? "حدث خطأ في الاتصال. اضغط للمحاولة ⚠️" : "Connection error. Tap to try again ⚠️", noteDefault: isAr ? "سيتم إرسال طلبك لقاعدة البيانات وسنتواصل معك لتأكيد السعر وموعد الاستلام." : "Sends your build straight to our database — we will contact you to confirm price and timing.", noteSuccess: isAr ? "تم تسجيل طلبك بنجاح، سنتواصل معك قريباً." : "Your details have been securely saved to our database.", uploading: isAr ? "جاري رفع الصورة... ⏳" : "Uploading photo... Please wait ⏳", uploaded: isAr ? "تم إرفاق الصورة ✓" : "Photo attached ✓", uploadErr: isAr ? "فشل الرفع. اضغط للمحاولة مرة أخرى ⚠️" : "Upload failed. Tap to try again ⚠️", btnWaitImg: isAr ? "يرجى انتظار رفع الصورة..." : "Waiting for photo upload...", none: isAr ? "لا يوجد" : "None", attached: isAr ? "✓ مرفقة" : "✓ Attached", customLbl: isAr ? "لون مخصص" : "Custom", fromStr: isAr ? "من" : "from", selectSize: isAr ? "اختر مقاس" : "Select a size", }; // Translate elements only if site is in English (default in HTML is Arabic) if (!isAr) { document.querySelectorAll("[data-i18n]").forEach((el) => { const key = el.getAttribute("data-i18n"); if (dict[key]) el.textContent = dict[key]; }); document.getElementById("customColor").placeholder = dict.customColPlc; document.getElementById("personalText").placeholder = dict.detailsPlc; document.getElementById("customerName").placeholder = dict.namePlc; document.getElementById("customerPhone").placeholder = dict.phonePlc; document.getElementById("sumPhoto").textContent = dict.none; } // Adjust phone field text alignment const phoneInputEl = document.getElementById("customerPhone"); phoneInputEl.style.textAlign = isAr ? "right" : "left"; // --- CONFIG: SETTINGS (Data Restored) --- const GOOGLE_SCRIPT_URL = "https://script.google.com/macros/s/AKfycbwqHo1Kel7GTdh9-LsHoxdhxPH-M5kAzqLaPb6u_5CzCb21l3Brmv2Qq6zhzzoIoNGqWA/exec"; const CLOUDINARY_CLOUD_NAME = "uxbnk2hd"; const CLOUDINARY_UPLOAD_PRESET = "layrd_uploads"; // Dynamically translate products and colors const PRODUCTS = [ { id: "popfig", name: isAr ? "مجسم بوب كالتشر" : "Pop Culture Figure", unit: isAr ? "لكل مجسم" : "per figure", base: 350, hasSize: true, hasPhoto: true, textLabel: isAr ? "ملاحظات عن الشخصية والوضعية" : "Character & pose notes", color: "#C44630", }, { id: "culturefig", name: isAr ? "مجسم شخصية ثقافية" : "Cultural Character Figure", unit: isAr ? "لكل مجسم" : "per figure", base: 400, hasSize: true, hasPhoto: true, textLabel: isAr ? "ملاحظات عن الشخصية والأزياء" : "Character & styling notes", color: "#C44630", }, { id: "spotify", name: isAr ? "ميدالية أغنية سبوتيفاي" : "Spotify Song Keychain", unit: isAr ? "لكل ميدالية" : "per keychain", base: 150, hasSize: false, hasPhoto: false, textLabel: isAr ? "اسم الأغنية والمغني أو الرابط" : "Song name & artist, or Spotify link", color: "#f5b942", }, { id: "chess", name: isAr ? "ميدالية قطعة شطرنج" : "Chess Piece Keychain", unit: isAr ? "لكل ميدالية" : "per keychain", base: 90, hasSize: false, hasPhoto: false, textLabel: isAr ? "أي قطعة؟ (ملك، وزير، إلخ)" : "Which piece — King, Queen, Knight, etc.", color: "#f5b942", }, { id: "turtle", name: isAr ? "طقم قواعد أكواب سلحفاة (4 قطع)" : "Turtle Coaster Set (4pcs)", unit: isAr ? "للطقم" : "per set", base: 280, hasSize: false, hasPhoto: false, textLabel: isAr ? "ألوان أو ملاحظات (اختياري)" : "Color theme or notes (optional)", color: "#C44630", }, ]; const COLORS = [ { id: "black", label: isAr ? "أسود مطفي" : "Matte black", hex: "#1a1a1a", }, { id: "white", label: isAr ? "أبيض ناصع" : "Arctic white", hex: "#f2f2f2", }, { id: "gold", label: isAr ? "ذهبي" : "Gold", hex: "#cfa02f" }, { id: "silver", label: isAr ? "فضي" : "Silver", hex: "#c3c6cc" }, { id: "teal", label: isAr ? "أحمر لايرد" : "Layrd Red", hex: "#C44630", }, { id: "custom", label: dict.customLbl, hex: "linear-gradient(135deg,#f2665a,#C44630,#f5b942)", }, ]; const SIZES = [ { id: "s", label: isAr ? "صغير · 7 سم" : "Small · 7cm", mult: 1 }, { id: "m", label: isAr ? "وسط · 10 سم" : "Medium · 10cm", mult: 1.3 }, { id: "l", label: isAr ? "كبير · 14 سم" : "Large · 14cm", mult: 1.6 }, ]; const state = { product: null, color: null, customColor: "", size: null, text: "", name: "", phone: "", phoneIsValid: false, hasPhoto: false, photoUrl: "", qty: 1, isUploading: false, orderSubmitted: false, submitAttempted: false, }; const el = (id) => document.getElementById(id); function validateForm() { let valid = true; if (!state.product) { el("productGrid").classList.add("grid-error"); el("productError").style.display = "block"; valid = false; } else { el("productGrid").classList.remove("grid-error"); el("productError").style.display = "none"; } if (!state.color) { el("swatchGrid").classList.add("grid-error"); el("colorError").style.display = "block"; valid = false; } else { el("swatchGrid").classList.remove("grid-error"); el("colorError").style.display = "none"; if (state.color.id === "custom" && !state.customColor.trim()) { el("customColor").classList.add("input-error"); el("customColorError").style.display = "block"; valid = false; } else { el("customColor").classList.remove("input-error"); el("customColorError").style.display = "none"; } } if (state.product && state.product.hasSize && !state.size) { el("sizeRow").classList.add("grid-error"); el("sizeError").style.display = "block"; valid = false; } else { el("sizeRow").classList.remove("grid-error"); el("sizeError").style.display = "none"; } if (!state.name.trim()) { el("customerName").classList.add("input-error"); el("nameError").style.display = "block"; valid = false; } else { el("customerName").classList.remove("input-error"); el("nameError").style.display = "none"; } if (!state.phoneIsValid) { el("customerPhone").classList.add("input-error"); el("phoneError").style.display = "block"; valid = false; } else { el("customerPhone").classList.remove("input-error"); el("phoneError").style.display = "none"; } return valid; } const productGrid = el("productGrid"); PRODUCTS.forEach((p) => { const card = document.createElement("button"); card.type = "button"; card.className = "product-card"; card.setAttribute("role", "radio"); card.setAttribute("aria-checked", "false"); card.innerHTML = `<div class="pname">${p.name}</div><div class="punit">${p.unit}</div><div class="pprice" dir="ltr">${dict.fromStr} ${p.base}</div>`; card.addEventListener("click", () => { if (state.orderSubmitted) return; state.product = p; document.querySelectorAll(".product-card").forEach((c) => { c.classList.remove("active"); c.setAttribute("aria-checked", "false"); }); card.classList.add("active"); card.setAttribute("aria-checked", "true"); el("sizePanel").style.display = p.hasSize ? "" : "none"; el("photoRow").style.display = p.hasPhoto ? "" : "none"; el("textFieldLabel").textContent = p.textLabel; if (!p.hasSize) state.size = null; update(); }); productGrid.appendChild(card); }); const swatchGrid = el("swatchGrid"); COLORS.forEach((c) => { const wrap = document.createElement("div"); wrap.className = "swatch-wrap"; const sw = document.createElement("button"); sw.type = "button"; sw.className = "swatch"; sw.setAttribute("role", "radio"); sw.setAttribute("aria-checked", "false"); sw.setAttribute("aria-label", c.label); sw.style.background = c.hex; sw.addEventListener("click", () => { if (state.orderSubmitted) return; state.color = c; document.querySelectorAll(".swatch").forEach((s) => { s.classList.remove("active"); s.setAttribute("aria-checked", "false"); }); sw.classList.add("active"); sw.setAttribute("aria-checked", "true"); el("customColorRow").style.display = c.id === "custom" ? "" : "none"; update(); }); const label = document.createElement("div"); label.className = "swatch-label"; label.textContent = c.label; wrap.appendChild(sw); wrap.appendChild(label); swatchGrid.appendChild(wrap); }); el("customColor").addEventListener("input", (e) => { state.customColor = e.target.value; update(); }); el("personalText").addEventListener("input", (e) => { state.text = e.target.value; update(); }); el("customerName").addEventListener("input", (e) => { state.name = e.target.value; update(); }); const handlePhoneInput = (e) => { let val = e.target.value; val = val.replace(/[٠-٩]/g, (d) => "٠١٢٣٤٥٦٧٨٩".indexOf(d)); val = val.replace(/\D/g, ""); e.target.value = val; state.phone = val; const phoneRegex = /^01[0125]\d{8}$/; state.phoneIsValid = phoneRegex.test(val); if (!state.submitAttempted) { if (val.length > 0 && !state.phoneIsValid) { el("customerPhone").classList.add("input-error"); el("phoneError").style.display = "block"; } else { el("customerPhone").classList.remove("input-error"); el("phoneError").style.display = "none"; } } update(); }; el("customerPhone").addEventListener("input", handlePhoneInput); el("customerPhone").addEventListener("change", handlePhoneInput); const sizeRow = el("sizeRow"); SIZES.forEach((s) => { const pill = document.createElement("button"); pill.type = "button"; pill.className = "pill"; pill.setAttribute("role", "radio"); pill.setAttribute("aria-checked", "false"); pill.textContent = s.label; pill.addEventListener("click", () => { if (state.orderSubmitted) return; state.size = s; document.querySelectorAll(".pill").forEach((p) => { p.classList.remove("active"); p.setAttribute("aria-checked", "false"); }); pill.classList.add("active"); pill.setAttribute("aria-checked", "true"); update(); }); sizeRow.appendChild(pill); }); const uploadBox = el("uploadBox"); const photoInput = el("photoInput"); async function uploadToCloudinary(file) { const formData = new FormData(); formData.append("file", file); formData.append("upload_preset", CLOUDINARY_UPLOAD_PRESET); const response = await fetch( `https://api.cloudinary.com/v1_1/${CLOUDINARY_CLOUD_NAME}/image/upload`, { method: "POST", body: formData }, ); if (!response.ok) throw new Error("Upload failed"); const data = await response.json(); return data.secure_url; } photoInput.addEventListener("change", async () => { if (photoInput.files.length > 0) { const file = photoInput.files[0]; state.isUploading = true; uploadBox.textContent = dict.uploading; uploadBox.className = "upload-box loading"; update(); try { const url = await uploadToCloudinary(file); state.photoUrl = url; state.hasPhoto = true; uploadBox.textContent = `${dict.uploaded} (${file.name})`; uploadBox.className = "upload-box has-file"; } catch (err) { state.photoUrl = ""; state.hasPhoto = false; uploadBox.textContent = dict.uploadErr; uploadBox.className = "upload-box error"; } finally { state.isUploading = false; photoInput.value = ""; update(); } } }); el("qtyMinus").addEventListener("click", () => { if (state.orderSubmitted) return; state.qty = Math.max(1, state.qty - 1); el("qtyValue").textContent = state.qty; update(); }); el("qtyPlus").addEventListener("click", () => { if (state.orderSubmitted) return; state.qty = Math.min(20, state.qty + 1); el("qtyValue").textContent = state.qty; update(); }); function computeTotal() { if (!state.product) return 0; const mult = state.product.hasSize && state.size ? state.size.mult : 1; return Math.round(state.product.base * mult * state.qty); } function update() { let done = 0; let total = 4; if (state.product) done += 1; if ( state.color && (state.color.id !== "custom" || state.customColor.trim()) ) done += 1; if (state.product && state.product.hasSize) { total += 1; if (state.size) done += 1; } if (state.name.trim()) done += 1; if (state.phoneIsValid) done += 1; const pct = Math.round((done / total) * 100); const layers = Math.round((pct / 100) * 48); el("figureFill").style.height = Math.max(pct, 4) + "%"; const fillColor = state.color ? state.color.id === "custom" ? "rgba(196, 70, 48, 0.7)" : state.color.hex : "#9ca3af"; el("figureFill").style.background = fillColor + (state.color ? "" : ""); el("figureFill").style.backgroundColor = fillColor; el("readout").textContent = pct + "%"; el("layerText").textContent = `LAYER ${layers} / 48`; el("heightText").textContent = (layers * 0.2).toFixed(2) + "mm"; el("sumProduct").textContent = state.product ? state.product.name : "—"; el("sumColor").textContent = state.color ? state.color.id === "custom" ? state.customColor.trim() || dict.customLbl : state.color.label : "—"; el("sumSize").textContent = state.product && state.product.hasSize ? state.size ? state.size.label : dict.selectSize : "—"; el("sumText").textContent = state.text.trim() || "—"; el("sumPhoto").textContent = state.hasPhoto ? dict.attached : dict.none; el("sumQty").textContent = state.qty; el("sumTotal").textContent = computeTotal().toLocaleString(); el("photoInput").disabled = state.isUploading || state.orderSubmitted; if (state.submitAttempted) validateForm(); const btn = el("orderBtn"); if (state.orderSubmitted) return; if (state.isUploading) { btn.classList.add("disabled"); btn.disabled = true; btn.textContent = dict.btnWaitImg; } else { btn.classList.remove("disabled"); btn.disabled = false; const ready = state.product && state.color && (state.color.id !== "custom" || state.customColor.trim()) && (!state.product.hasSize || state.size) && state.name.trim() && state.phoneIsValid; btn.textContent = ready ? dict.btnReady : dict.btnDefault; } } el("orderBtn").addEventListener("click", async (e) => { e.preventDefault(); const btn = e.target; if (state.orderSubmitted) { window.location.reload(); return; } if (state.isUploading) return; state.submitAttempted = true; const isValid = validateForm(); if (!isValid) { const firstError = document.querySelector( ".input-error, .grid-error", ); if (firstError) firstError.scrollIntoView({ behavior: "smooth", block: "center" }); return; } btn.textContent = dict.btnSubmit; btn.classList.add("disabled"); btn.disabled = true; const colorName = state.color.id === "custom" ? state.customColor.trim() || "Custom" : state.color.label; const sizeName = state.product.hasSize && state.size ? state.size.label : "N/A"; const orderData = { name: state.name.trim(), phone: state.phone.trim(), product: state.product.name, color: colorName, size: sizeName, details: state.text.trim() || "None", quantity: state.qty, total: computeTotal() + " EGP", photo: state.photoUrl || "No Photo", }; try { const response = await fetch(GOOGLE_SCRIPT_URL, { method: "POST", body: JSON.stringify(orderData), }); const result = await response.json(); if (result.status === "success") { state.orderSubmitted = true; btn.textContent = dict.btnSuccess; btn.style.background = "#059669"; btn.classList.remove("disabled"); btn.disabled = false; // Update confirmation color const note = el("ctaNote"); note.textContent = dict.noteSuccess; note.style.color = "#059669"; note.style.fontWeight = "bold"; } else { throw new Error("Script error"); } } catch (error) { btn.textContent = dict.btnError; btn.classList.remove("disabled"); btn.disabled = false; } }); update();