├── logo.png ├── 128x128.png ├── 440x280.png ├── 1400x560.png ├── fonts ├── Shabnam-FD.eot ├── Shabnam-FD.ttf ├── Shabnam-FD.woff └── Shabnam-FD.woff2 ├── manifest.json ├── loading.svg ├── heart.svg ├── LICENSE ├── background.js ├── index.html ├── README.md ├── script.js ├── style.css └── content.js /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pouriasabaghi/kojachande_extension/HEAD/logo.png -------------------------------------------------------------------------------- /128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pouriasabaghi/kojachande_extension/HEAD/128x128.png -------------------------------------------------------------------------------- /440x280.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pouriasabaghi/kojachande_extension/HEAD/440x280.png -------------------------------------------------------------------------------- /1400x560.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pouriasabaghi/kojachande_extension/HEAD/1400x560.png -------------------------------------------------------------------------------- /fonts/Shabnam-FD.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pouriasabaghi/kojachande_extension/HEAD/fonts/Shabnam-FD.eot -------------------------------------------------------------------------------- /fonts/Shabnam-FD.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pouriasabaghi/kojachande_extension/HEAD/fonts/Shabnam-FD.ttf -------------------------------------------------------------------------------- /fonts/Shabnam-FD.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pouriasabaghi/kojachande_extension/HEAD/fonts/Shabnam-FD.woff -------------------------------------------------------------------------------- /fonts/Shabnam-FD.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pouriasabaghi/kojachande_extension/HEAD/fonts/Shabnam-FD.woff2 -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Koja Chande", 4 | "version": "1.7", 5 | "description": "بهترین قیمت رو پیدا کن", 6 | "permissions": ["activeTab", "contextMenus"], 7 | "background": { 8 | "service_worker": "background.js" 9 | }, 10 | "host_permissions": [ 11 | "https://www.digikala.com/*", 12 | "https://www.amazon.com/*" 13 | ], 14 | "action": { 15 | "default_popup": "index.html" 16 | }, 17 | "icons": { 18 | "16": "128x128.png", 19 | "48": "128x128.png", 20 | "128": "128x128.png" 21 | }, 22 | "content_scripts": [ 23 | { 24 | "matches": ["https://www.digikala.com/*", "https://www.amazon.com/*"], 25 | "js": ["content.js"] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Pouria Sabaghi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Opens the popup and sends product data. 3 | * @param {{ name: string, price: number, origin: string }} product - The product object containing name, price, and origin. 4 | */ 5 | 6 | const openPopup = async (product) => { 7 | try { 8 | await chrome.action.openPopup(); 9 | } catch (error) { 10 | console.error(error, "Popup is already open"); 11 | } 12 | 13 | if (product.name) { 14 | chrome.runtime.sendMessage({ 15 | action: "get_prices", 16 | product: product, 17 | }); 18 | } 19 | }; 20 | 21 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 22 | if (request.action === "open_popup") { 23 | openPopup(request.product); 24 | return true; 25 | } 26 | }); 27 | 28 | // Create context menu 29 | chrome.runtime.onInstalled.addListener(function () { 30 | chrome.contextMenus.create({ 31 | id: "get_prices_context_menu", 32 | title: "کجا چنده", 33 | contexts: ["selection"], 34 | }); 35 | }); 36 | 37 | // Event listener for context menu item click 38 | chrome.contextMenus.onClicked.addListener(function (info, tab) { 39 | if (info.menuItemId === "get_prices_context_menu" && info.selectionText) 40 | openPopup({ name: info.selectionText }); 41 | }); 42 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chande 7 | 8 | 9 | 10 | 11 |
12 |

کجا چنده؟

13 |

14 | کجا چنده 15 | یک اکستنشن کروم است که به شما کمک میکند هنگام خرید، قیمت محصول مورد 17 | نظر خود را با بقیه فروشگاه ها مقایسه و بهترین خرید را داشته باشید. 19 |

20 |

21 | برای شروع بالا سمت چپ در صفحه محصول بر روی دکمه 22 | 23 | کلیک کنید. 24 |

25 |
26 | 27 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📌 کجا چنده 2 | 3 | اکستنشن کجا چنده به شما کمک می‌کند تا قیمت محصول مورد نظر خود را در فروشگاه‌های مختلف بررسی کنید و ببینید که آیا می‌توانید آن را با قیمت ارزان‌تری تهیه کنید یا خیر. 4 | 5 | --- 6 | 7 | ## 🚀 ویژگی‌ها 8 | ✅ مقایسه قیمت محصول در فروشگاه‌های مختلف به صورت خودکار 9 | ✅ نمایش قیمت فعلی محصول در فروشگاهی که بازدید می‌کنید 10 | ✅ رابط کاربری ساده و سریع 11 | ✅ دکمه‌ای برای بررسی قیمت در سایر فروشگاه‌ها 12 | ✅ قابلیت مخفی و نمایش مجدد ویجت اکستنشن 13 | 14 | --- 15 | 16 | ## 📥 نصب و راه‌اندازی 17 | [نصب از کروم وب استور](https://chromewebstore.google.com/detail/koja-chande/gaacdleodfajpcdoffbmnkijnabjocac?utm_source=ext_app_menu) 18 | 19 | --- 20 | 21 | ## 🔧 نحوه استفاده 22 | 23 | 1. وارد صفحه محصول مورد نظر خود در فروشگاه شوید. 24 | 2. روی دکمه‌ی **"کجا چنده؟"** که در صفحه اضافه شده است کلیک کنید. 25 | 3. اکستنشن قیمت این محصول را در سایر فروشگاه‌ها بررسی می‌کند و شما را مطلع می‌کند که آیا جای ارزان‌تری برای خرید آن وجود دارد یا نه. 26 | 4. اگر قیمت بهتری پیدا شد، می‌توانید با کلیک روی لینک فروشگاه ارزان‌تر، به آنجا منتقل شوید. 27 | 28 | ## 👨‍💻 گزارش خرابی 29 | 30 | توجه داشته باشید که عملکرد افزونه ممکن است بسته به به‌روزرسانی‌های فروشگاه‌ها دچار اختلال شود، اما جای نگرانی نیست! ما به‌صورت مستمر در حال به‌روزرسانی و افزودن قابلیت‌های جدید هستیم. در صورت وجود هرگونه مشکل، از طریق Issues با ما در ارتباط باشید. 31 | 32 | ## ارتباط 33 | - [Linkedin](linkedin.com/in/pouria-sabaghi-ba052730b) 34 | - [Email](mailto:pouirasabaghi@gamil.com) 35 | - [Telegram](https://t.me/p_nightwolf) 36 | 37 | --- 38 | 39 | ## 📜 مجوز 40 | این پروژه تحت مجوز **MIT** منتشر شده است. برای اطلاعات بیشتر به فایل `LICENSE` مراجعه کنید. 41 | 42 | --- 43 | 44 | ❤️ اگر این پروژه برای شما مفید بود، لطفاً ⭐️ آن را در گیت‌هاب ثبت کنید! -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 2 | if (request.action === "get_prices") { 3 | const { name, price } = request.product; 4 | 5 | const main = document.body.querySelector("main"); 6 | const fetchData = async () => { 7 | try { 8 | main.innerHTML = loading(); 9 | const response = await fetch("https://api.kojachande.ir/api/v1/prices", { 10 | method: "POST", 11 | headers: { 12 | Accept: "application/json", 13 | "Content-Type": "application/json", 14 | }, 15 | body: JSON.stringify({ product_name: name, product_price: price }), 16 | }); 17 | 18 | const data = await response.json(); 19 | 20 | if (!response.ok) throw new Error(data.message); 21 | 22 | main.innerHTML = loading(false); 23 | 24 | data.forEach((item) => { 25 | if (!item.error) { 26 | const list = ``; 29 | main.innerHTML += list; 30 | 31 | // handle image error 32 | document 33 | .querySelectorAll("img") 34 | .forEach((img) => 35 | img.addEventListener("error", handleImageError) 36 | ); 37 | 38 | } else { 39 | main.innerHTML += `${item.error}`; 40 | } 41 | }); 42 | } catch (error) { 43 | main.innerHTML = `${error.message}`; 44 | console.error(error); 45 | } 46 | }; 47 | 48 | fetchData(); 49 | } 50 | }); 51 | 52 | function productItem(product) { 53 | return ` 54 |
  • 55 | 56 |
    57 | 58 | ${product.product} 59 | 60 |
    61 | در ${product.shop_name}   62 | ${product.price} 63 |
    64 |
    65 |
  • 66 | `; 67 | } 68 | 69 | function loading(status = true) { 70 | if (status) 71 | return ``; 72 | 73 | return ""; 74 | } 75 | 76 | function handleImageError(event) { 77 | event.target.src = "./logo.png"; 78 | } 79 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: shabnam; 3 | font-style: normal; 4 | font-weight: 500; 5 | src: url(./fonts/); 6 | src: url(./fonts/?#iefix) format("embedded-opentype"), 7 | url(./fonts//Shabnam-FD.woff2) format("woff2"), 8 | url(./fonts//Shabnam-FD.woff) format("woff"), 9 | url(./fonts//Shabnam-FD.ttf) format("truetype"); 10 | } 11 | 12 | * { 13 | padding: 0; 14 | margin: 0; 15 | } 16 | 17 | body { 18 | font-family: shabnam; 19 | background-color: rgb(249, 249, 249); 20 | padding: 16px; 21 | padding-bottom: 48px; 22 | } 23 | 24 | a { 25 | text-decoration: none; 26 | } 27 | 28 | main { 29 | min-width: 700px; 30 | } 31 | 32 | li { 33 | list-style: none; 34 | } 35 | 36 | h1 { 37 | margin-bottom: 16px; 38 | } 39 | 40 | .guide { 41 | display: inline-flex; 42 | column-gap: 4px; 43 | align-items: center; 44 | font-size: 14px; 45 | } 46 | 47 | .kojachande--button { 48 | width: 100px; 49 | color: white; 50 | background-color: rgb(239, 64, 86); 51 | font-weight: bold; 52 | padding: 10px; 53 | border-radius: 0.5rem; 54 | margin: auto; 55 | border: none; 56 | } 57 | 58 | .product--list { 59 | display: flex; 60 | flex-direction: column; 61 | row-gap: 16px; 62 | } 63 | 64 | .product--image { 65 | width: 100px; 66 | height: 100px; 67 | object-fit: cover; 68 | border-radius: 12px; 69 | } 70 | 71 | .product--item { 72 | display: flex; 73 | column-gap: 16px; 74 | background-color: white; 75 | padding: 12px; 76 | border-radius: 12px; 77 | } 78 | 79 | .product--item-text { 80 | display: flex; 81 | flex-direction: column; 82 | justify-content: space-between; 83 | } 84 | 85 | .product--link { 86 | min-height: 48px; 87 | font-size: 16px; 88 | text-overflow: ellipsis; 89 | overflow: hidden; 90 | display: -webkit-box; 91 | -webkit-line-clamp: 2; 92 | -webkit-box-orient: vertical; 93 | line-height: 24px; 94 | } 95 | 96 | .product--price { 97 | font-size: 14px; 98 | } 99 | 100 | [data-compare="1"] { 101 | color: green; 102 | } 103 | 104 | [data-compare="-1"] { 105 | color: red; 106 | } 107 | 108 | .badge { 109 | color: white; 110 | max-width: max-content; 111 | padding: 6px 10px; 112 | border-radius: 10px; 113 | display: inline-flex; 114 | font-size: 12px; 115 | } 116 | 117 | .badge--danger { 118 | background-color: #ff4242; 119 | } 120 | 121 | .error{ 122 | font-weight: bold; 123 | font-size: 18px; 124 | padding: 8px; 125 | } 126 | 127 | 128 | footer { 129 | position: fixed; 130 | bottom: 0; 131 | left: 0; 132 | right: 0; 133 | margin: auto; 134 | background: white; 135 | width: 100%; 136 | max-width: 685px; 137 | height: 36px; 138 | border-radius: 0.5rem 0.5rem 0 0; 139 | display: flex; 140 | align-items: center; 141 | padding-left: 1rem; 142 | box-shadow: 0 0 17px 0 rgb(0 0 0 / 12%); 143 | } 144 | 145 | footer span { 146 | margin-left: 0.75rem; 147 | } 148 | 149 | footer a { 150 | font-weight: bold; 151 | color: black; 152 | text-decoration: underline; 153 | } -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | const kc = createWrapper(); 2 | 3 | const button = createButton(); 4 | const hide = createHide(); 5 | 6 | kc.appendChild(hide); 7 | kc.appendChild(button); 8 | 9 | document.body.appendChild(kc); 10 | 11 | function createWrapper() { 12 | const kc = document.createElement("div"); 13 | addStyles(kc, { 14 | display: "flex", 15 | alignItems: "center", 16 | columnGap: "8px", 17 | justifyContent: "center", 18 | color: "white", 19 | backgroundColor: "#ef4056", 20 | fontWeight: "bold", 21 | borderRadius: "0.5rem", 22 | margin: "auto", 23 | top: "8px", 24 | left: "-85px", 25 | position: "fixed", 26 | zIndex: "999999999", 27 | border: "none", 28 | overflow: "hidden", 29 | transition: "left 500ms", 30 | }); 31 | 32 | window.addEventListener("load", () => { 33 | kc.style.left = "23px"; 34 | }); 35 | 36 | return kc; 37 | } 38 | 39 | function createButton() { 40 | const button = document.createElement("button"); 41 | button.textContent = "کجاچنده"; 42 | addStyles(button, { 43 | color: "white", 44 | padding: "8px 0px 8px 8px", 45 | }); 46 | 47 | button.addEventListener("click", () => { 48 | const productNameElement = document.querySelector("h1"); 49 | const productPriceElement = 50 | document.querySelector(".text-h4-compact[data-testid='price-final']") || 51 | document.querySelector( 52 | ".text-neutral-800[data-testid='price-no-discount']" 53 | ); 54 | 55 | if (!productNameElement) { 56 | alert("لطفا تا نمایش نام محصول روی صفحه صبر کنید."); 57 | return; 58 | } 59 | 60 | chrome.runtime.sendMessage({ 61 | action: "open_popup", 62 | product: { 63 | name: productNameElement.textContent, 64 | price: fixNumber(productPriceElement?.textContent.replaceAll(",", "")), 65 | origin: window.location.hostname, 66 | }, 67 | }); 68 | }); 69 | 70 | return button; 71 | } 72 | 73 | function createHide() { 74 | const hide = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 75 | hide.setAttribute("xmlns", "http://www.w3.org/2000/svg"); 76 | hide.setAttribute("fill", "none"); 77 | hide.setAttribute("viewBox", "0 0 24 24"); 78 | hide.setAttribute("stroke", "currentColor"); 79 | hide.setAttribute("width", "24"); 80 | hide.setAttribute("height", "24"); 81 | addStyles(hide, { 82 | backgroundColor: "#de354a", 83 | height: "35px", 84 | cursor: "pointer", 85 | transition: "transform 300ms", 86 | }); 87 | 88 | const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); 89 | path.setAttribute("stroke-linecap", "round"); 90 | path.setAttribute("stroke-linejoin", "round"); 91 | path.setAttribute("d", "M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18"); 92 | 93 | // add hide event 94 | hide.addEventListener("click", () => { 95 | hide.classList.toggle("hide"); 96 | 97 | if (hide.classList.contains("hide")) { 98 | kc.style.left = "-63px"; 99 | hide.style.transform = "rotateY(180deg)"; 100 | } else { 101 | kc.style.left = "23px"; 102 | hide.style.transform = "rotateY(0deg)"; 103 | } 104 | }); 105 | 106 | hide.appendChild(path); 107 | 108 | return hide; 109 | } 110 | 111 | // Utils 112 | function fixNumber(str) { 113 | let persianNumbers = [ 114 | /۰/g, 115 | /۱/g, 116 | /۲/g, 117 | /۳/g, 118 | /۴/g, 119 | /۵/g, 120 | /۶/g, 121 | /۷/g, 122 | /۸/g, 123 | /۹/g, 124 | ]; 125 | let arabicNumbers = [ 126 | /٠/g, 127 | /١/g, 128 | /٢/g, 129 | /٣/g, 130 | /٤/g, 131 | /٥/g, 132 | /٦/g, 133 | /٧/g, 134 | /٨/g, 135 | /٩/g, 136 | ]; 137 | 138 | if (typeof str === "string") { 139 | for (var i = 0; i < 10; i++) { 140 | str = str.replace(persianNumbers[i], i).replace(arabicNumbers[i], i); 141 | } 142 | } 143 | return str; 144 | } 145 | 146 | /** 147 | * Adds CSS styles to a given HTML or SVG element. 148 | * 149 | * @param {HTMLElement | SVGElement} element - The element to which styles will be applied. 150 | * @param {Record} styles - An object containing CSS properties and values. 151 | */ 152 | function addStyles(element, styles) { 153 | Object.keys(styles).forEach((key) => { 154 | element.style[key] = styles[key]; 155 | }); 156 | } 157 | --------------------------------------------------------------------------------