Our mission

Most Popular Categories

Our story begins

Carefully curating goods for life

Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint. Velit officia consequat duis enim velit mollit. Exercitation veniam consequat sunt nostrud amet.
0+
Years Experience
0%
Handcrafted Products
Lean more

what we do

Your Organic Online Store

Amet minim mollit non deserunt ullamco est sit aliqua dolor do amet sint. Velit officia consequat duis enim velit mollit. Exercitation veniam consequat sunt nostrud amet.

Artisan Made

Handcrafted in partnership with world-class artisans—real people.

Connection to Nature

Celebrating Natural Products from the Earth

Sustainable Partnerships

We establish sustainable relationships with our artisans.
why us

Handicraft Production Process

Materials Processing

Product Weaving

Quality

Drying

Our Services

See all

Friendly Material, Good Team And Quality Of Products

Every product we create reflects our dedication to quality and craftsmanship. Our team in Ngoc Dong village meticulously handcrafts each item, ensuring they meet the highest standards.

Strict Quality Control

We pay attention to even the smallest details to guarantee product quality, maintaining a defect rate of just 2.5% of the total order quantity.

Shipping Cost Optimization

With our own Packaging Plant and strong partnerships with freight forwarders, we optimize packaging costs and reduce international shipping fees effectively.

Competitive Prices

Our efficient processes and strong relationships allow us to offer competitive prices without compromising on quality.

The Widest Range of Products

Over the past decade, we have built trust with numerous craft villages across Vietnam, offering a wide range of products to meet diverse needs.

Our Clients Say

FAQs

We are a manufacturer with many years of experience in producing storage baskets, storage boxes, storage trays, serving trays, and charger plates.

Yes, you can choose the material you prefer.

Yes, you can choose the material you prefer.

Yes, you can choose the material you prefer.

Yes, you can choose the material you prefer.

Yes, you can choose the material you prefer.

Recent updates

Our Latest News

Featured Customers

function getQueryParameter(paramName) { const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); return urlParams.get(paramName); } async function queryAll(url, select = '*', params = {}) { let data = []; const MAX_RETRIES = 3; let lastError = null; let lastStatus = 0; // 1. FETCH DỮ LIỆU VỚI LOGIC THỬ LẠI VÀ BẮT LỖI MẠNG/HTTP for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { try { const response = await fetch(url); if (!response.ok) { lastStatus = response.status; throw new Error(`HTTP error! Status: ${lastStatus}`); } data = await response.json(); lastError = null; break; } catch (error) { lastError = error; if (attempt === MAX_RETRIES - 1) break; await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1))); } } // XỬ LÝ LỖI CUỐI CÙNG (NÉM LỖI RA NGOÀI - PHÂN TÁCH TRÁCH NHIỆM) if (lastError) { let errorMessage = `Không thể tải dữ liệu từ API sau ${MAX_RETRIES} lần thử.`; if (lastStatus === 404) { errorMessage += ` (Lỗi 404: Không tìm thấy tài nguyên)`; } else if (lastStatus >= 400 && lastStatus < 500) { errorMessage += ` (Lỗi Client: ${lastStatus})`; } else if (lastStatus >= 500) { errorMessage += ` (Lỗi Server: ${lastStatus})`; } else { errorMessage += ` (Lỗi mạng/CORS)`; } throw new Error(errorMessage); } // --- 2. XỬ LÝ THAM SỐ VÀ DỮ LIỆU ĐÃ TẢI THÀNH CÔNG --- if (!Array.isArray(data)) return []; const defaultParams = { orderby: 'id', order: 'desc', limit: 10, offset: 0, where: {}, priority_ids: [] }; const filteredParams = Object.fromEntries( Object.entries(params).filter(([_, value]) => value !== undefined && value !== null && value !== '' ) ); const queryParams = { ...defaultParams, ...filteredParams }; let results = data; // --- 3. Lọc dữ liệu theo where (Giữ lại logic của bạn) --- if (queryParams.where && Object.keys(queryParams.where).length > 0) { results = results.filter(item => { return Object.entries(queryParams.where).every(([field, condition]) => { const itemValue = item[field]; if (typeof condition === 'object' && condition !== null) { if (condition.BETWEEN) { const [min, max] = condition.BETWEEN; return itemValue >= min && itemValue <= max; } if (condition.IN) { if (!Array.isArray(condition.IN)) return false; return condition.IN.includes(itemValue); } const entries = Object.entries(condition); if (entries.length === 0) return true; const [operator, value] = entries[0]; switch (operator) { case '=': return itemValue === value; case '>': return itemValue > value; case '<': return itemValue < value; case '>=': return itemValue >= value; case '<=': return itemValue <= value; case '<>': return itemValue != value; case 'LIKE': { if (typeof value !== 'string') return false; const regex = new RegExp(value.replace(/%/g, '.*'), 'i'); return regex.test(String(itemValue)); } default: return false; } } return itemValue == condition; }); }); } // --- 4. Xử lý select fields (Giữ lại logic của bạn) --- if (select && select !== '*') { const fields = Array.isArray(select) ? select : select.split(',').map(field => field.trim()); results = results.map(item => { const selectedData = {}; fields.forEach(field => { if (field in item) { selectedData[field] = item[field]; } }); return selectedData; }); } // --- 5. Sắp xếp kết quả (Giữ lại logic của bạn) --- results.sort((a, b) => { const PRIORITY_IDS = queryParams.priority_ids; const aId = a.id; const bId = b.id; const aIndex = Array.isArray(PRIORITY_IDS) ? PRIORITY_IDS.indexOf(aId) : -1; const bIndex = Array.isArray(PRIORITY_IDS) ? PRIORITY_IDS.indexOf(bId) : -1; const aInPriority = aIndex !== -1; const bInPriority = bIndex !== -1; // LOGIC ƯU TIÊN ID (Chạy đầu tiên) if (aInPriority && bInPriority) { // Case 1: Cả hai đều có ưu tiên -> Sắp xếp theo thứ tự mảng ưu tiên return aIndex - bIndex; } if (aInPriority) { // Case 2: Chỉ a có ưu tiên -> a lên trước (-1) return -1; } if (bInPriority) { // Case 3: Chỉ b có ưu tiên -> b lên trước (1) return 1; } // KẾT THÚC LOGIC ƯU TIÊN // --- Bắt đầu Sắp xếp Chung (Nếu không có ưu tiên) --- const sortCriteria = Array.isArray(queryParams.orderby) ? queryParams.orderby : [{ field: queryParams.orderby, order: queryParams.order }]; for (const sort of sortCriteria) { const field = sort.field; const order = (sort.order || 'desc').toLowerCase(); // Lấy giá trị thô để phân biệt true, false, và null/undefined const aValRaw = a[field]; const bValRaw = b[field]; // Gán giá trị fallback cho các trường hợp không phải featured_post let aVal = aValRaw ?? a.id ?? 0; let bVal = bValRaw ?? b.id ?? 0; let comparison = 0; // NHÁNH XỬ LÝ ĐẶC BIỆT: featured_post (true > null/false) if (field === 'featured_post') { // Chỉ có 'true' (so sánh nghiêm ngặt) mới nhận điểm 1 const aScore = aValRaw === true ? 1 : 0; const bScore = bValRaw === true ? 1 : 0; comparison = aScore - bScore; } else if (typeof aVal === 'boolean') { // Xử lý các trường boolean khác theo cách thông thường comparison = (aVal ? 1 : 0) - (bVal ? 1 : 0); } else if (field === 'date' || field.includes('date') || field.includes('updated')) { const dateA = new Date(aVal || 0).getTime(); const dateB = new Date(bVal || 0).getTime(); comparison = dateB - dateA; } else if (typeof aVal === 'number' && typeof bVal === 'number') { comparison = aVal - bVal; } else { const valueA = String(aVal || '').toLowerCase(); const valueB = String(bVal || '').toLowerCase(); comparison = valueA.localeCompare(valueB); } if (comparison !== 0) { return order === 'desc' ? -comparison : comparison; } } return 0; }); // --- 6. PHÂN TRANG / LOAD MORE --- const page = queryParams.page || 1; const offset = (page - 1) * queryParams.limit; const limit = queryParams.limit; return results.slice(offset, offset + limit); } // ======================================================= // II. HÀM RENDER LINH HOẠT (renderPost) // ======================================================= /** * Hiển thị một bài viết lên giao diện bằng Data Binding linh hoạt (dựa trên data-bind). * @param {Object} post Dữ liệu bài viết. * @param {HTMLElement} container Vùng chứa để thêm bài viết vào. * @param {HTMLElement} template Template HTML. */ function renderPost(post, container, template) { const clonedPost = template.cloneNode(true); // 1. Duyệt qua tất cả các phần tử có thuộc tính data-bind trong template const bindElements = clonedPost.querySelectorAll('[data-bind]'); bindElements.forEach(element => { const bindString = element.getAttribute('data-bind'); // Phân tích chuỗi data-bind: "text: title; href: url" bindString.split(';').forEach(binding => { const parts = binding.trim().split(':'); if (parts.length < 2) return; const [attribute, fieldName] = parts.map(s => s.trim()); // Lấy giá trị trường (có thể là title, body, userId, v.v.) const value = post[fieldName]; // Nếu giá trị không tồn tại hoặc là null/undefined if (value === undefined || value === null) { // Xử lý đặc biệt cho : nếu không có src, loại bỏ element if (element.tagName === 'IMG' && (attribute.toLowerCase() === 'src' || attribute.toLowerCase() === 'alt')) { element.remove(); } return; } // 2. Điền dữ liệu dựa trên thuộc tính: switch (attribute.toLowerCase()) { case 'text': element.innerHTML = value; break; case 'html': element.innerHTML = value; break; case 'src': case 'href': case 'alt': case 'caption': element.setAttribute(attribute, value); break; default: // Đặt các thuộc tính khác (ví dụ: data-*) element.setAttribute(attribute, value); break; } }); }); container.appendChild(clonedPost); } // ======================================================= // IV. CLASS QUẢN LÝ LOAD MORE ĐỘC LẬP (LoadMoreComponent) // ======================================================= class LoadMoreComponent { /** * @param {Object} config Cấu hình bao gồm ID/Selector của các phần tử UI và tham số truy vấn ban đầu. */ constructor(config) { // Tham chiếu DOM cục bộ (không phải toàn cục) this.container = document.getElementById(config.containerId); this.template = document.querySelector(config.templateSelector); this.loadMoreBtn = document.getElementById(config.loadMoreButtonId); this.loader = document.querySelector(config.loaderSelector); this.noMorePostsMsg = document.getElementById(config.noMorePostsMsgId); this.apiUrl = config.apiUrl; this.currentPage = 1; this.isLoading = false; this.hasMorePosts = true; this.initialParams = config.initialParams; this.delayMs = config.delayMs || 500; this.initialLimit = this.initialParams.limit || 5; // BIND: Đảm bảo 'this' trỏ đúng đến instance khi hàm được gọi như một callback this.handleLoadMore = this.handleLoadMore.bind(this); this.handleLoadError = this.handleLoadError.bind(this); } /** * Hàm xử lý lỗi tải: Dọn dẹp giao diện controls của instance này. * @param {string} message Thông báo lỗi */ handleLoadError(message) { console.error(`[${this.container.id}] Fetch/Load Error Handled:`, message); // Ẩn các phần tử controls của instance hiện tại if (this.loadMoreBtn) this.loadMoreBtn.style.display = 'none'; if (this.loader) this.loader.style.display = 'none'; // Hiển thị thông báo lỗi trên khu vực hiển thị bài viết if (this.container) { this.container.innerHTML = `

Đã xảy ra lỗi: ${message}

`; } if (this.noMorePostsMsg) this.noMorePostsMsg.style.display = 'none'; } /** * Logic chính để tải thêm bài viết (Load More). */ async handleLoadMore() { if (!this.loadMoreBtn || this.isLoading || !this.hasMorePosts) return; try { this.isLoading = true; this.loadMoreBtn.style.display = 'none'; if (this.loader) this.loader.style.display = 'block'; // MÔ PHỎNG ĐỘ TRỄ MẠNG await new Promise(resolve => setTimeout(resolve, this.delayMs)); const nextPage = this.currentPage + 1; const nextParams = { ...this.initialParams, limit: this.initialLimit, page: nextPage }; // Gọi hàm queryAll const newPosts = await queryAll(this.apiUrl, '*', nextParams); if (newPosts && newPosts.length > 0) { this.currentPage = nextPage; newPosts.forEach(post => { renderPost(post, this.container, this.template); }); this.hasMorePosts = newPosts.length === this.initialLimit; } else { this.hasMorePosts = false; } } catch (error) { console.error('Lỗi Load More:', error.message); this.handleLoadError(error.message); // Gọi hàm xử lý lỗi cục bộ this.hasMorePosts = false; } finally { this.isLoading = false; if (this.loader) this.loader.style.display = 'none'; if (this.hasMorePosts) { if (this.loadMoreBtn) this.loadMoreBtn.style.display = 'inline-flex'; } else { if (this.loadMoreBtn) this.loadMoreBtn.style.display = 'none'; if (this.noMorePostsMsg) this.noMorePostsMsg.style.display = 'inline-flex'; } } } /** * Khởi tạo: Tải trang đầu tiên và thiết lập sự kiện. */ async init() { if (!this.container || !this.template) { console.error(`[${this.container ? this.container.id : 'N/A'}] Cấu hình DOM không hợp lệ.`); return; } // --- BƯỚC 4: BÁO LỖI NẾU KHÔNG CÓ URL --- if (!this.apiUrl) { this.handleLoadError("Thiếu URL API trong cấu hình."); return; } try { const firstPagePosts = await queryAll(this.apiUrl, '*', { ...this.initialParams, page: 1 }); if (firstPagePosts && firstPagePosts.length > 0) { firstPagePosts.forEach(post => { renderPost(post, this.container, this.template); }); this.hasMorePosts = firstPagePosts.length === this.initialLimit; if (this.loadMoreBtn && this.hasMorePosts) { this.loadMoreBtn.style.display = 'inline-flex'; this.loadMoreBtn.addEventListener('click', this.handleLoadMore); } else if (this.loadMoreBtn) { this.loadMoreBtn.style.display = 'none'; if (this.noMorePostsMsg) this.noMorePostsMsg.style.display = 'inline-flex'; } } else { this.container.innerHTML = `

Không tìm thấy bài viết nào.

`; if (this.loadMoreBtn) this.loadMoreBtn.style.display = 'none'; } } catch (error) { console.error('Lỗi Tải Trang Ban Đầu:', error.message); this.handleLoadError(error.message); } } }