```react import React, { useState, useMemo, useRef, useEffect } from 'react'; import { ShoppingCart, Plus, Minus, Trash2, Upload, CheckCircle, X, Home, Grid, PenTool, Image as ImageIcon, Star, Search, Facebook, Instagram, MessageCircle, Heart, Sparkles, ChevronRight, Settings, Package, ClipboardList, Edit3, PlusCircle, Database } from 'lucide-react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInWithCustomToken, signInAnonymously, onAuthStateChanged } from 'firebase/auth'; import { getFirestore, collection, onSnapshot, doc, setDoc, updateDoc, deleteDoc } from 'firebase/firestore'; const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {}; const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; export default function App() { // --- State Management --- const [activeTab, setActiveTab] = useState('home'); const [user, setUser] = useState(null); // Dynamic Data States (from Firebase) const [categories, setCategories] = useState([]); const [orderWorks, setOrderWorks] = useState([]); const [reviews, setReviews] = useState([]); const [orders, setOrders] = useState([]); // Review Form const [newReviewText, setNewReviewText] = useState(''); const [newReviewName, setNewReviewName] = useState(''); const [newReviewRating, setNewReviewRating] = useState(5); // Admin States const [isAdminMode, setIsAdminMode] = useState(false); const [adminTab, setAdminTab] = useState('orders'); // orders, products, services, reviews const [showAdminLogin, setShowAdminLogin] = useState(false); const [adminPasswordInput, setAdminPasswordInput] = useState(''); const [adminLoginError, setAdminLoginError] = useState(false); // Settings States const [adminPin, setAdminPin] = useState("123456"); // รหัสผ่านตั้งต้น const [lineUrl, setLineUrl] = useState(""); const [newPinInput, setNewPinInput] = useState(""); const [newLineUrlInput, setNewLineUrlInput] = useState(""); // Shop Info States const [shopInfo, setShopInfo] = useState({ logo: 'https://i.postimg.cc/9fdRrYWG/att-b-TBt8GNQs1JNUVZ4Z20Nqzig-JMn-Jm-Dz-Tg6i-Hdk-XGp-Vg.jpg', name: 'Nori Design', slogan: '🌻ร้านโนริพร้อมบริการลูกค้าที่น่ารักทุกท่านค้าบ🌈 มีบัตรสะสมแต้ม สำหรับลูกค้าเข้ากลุ่มรับผ่อนด้วยนะคะ 💥สนใจ/สั่งซื้อ สามารถทักถามได้เลยค้าบ✨', marquee1: '💗 ยินดีต้อนรับเข้าสู้ร้านโนริ 💗', marquee2: 'พร้อมบริการตลอด25ชั่วโมง', marquee3: '🌻Have a good day 🌻', fbMain: 'https://www.facebook.com/nori.desing?', fbSub: 'https://www.facebook.com/profile.php?id=61574306123714' }); const [editShopInfo, setEditShopInfo] = useState(shopInfo); const shopLogoInputRef = useRef(null); // Item Edit Form const [isEditingItem, setIsEditingItem] = useState(false); const [editType, setEditType] = useState('categories'); // 'categories' or 'services' const [editFormData, setEditFormData] = useState({ id: '', name: '', price: 0, image: '', desc: '', tag: '', previewUrl: '', addons: [], subcategory: '' }); const itemFileInputRef = useRef(null); // Category Subcategory & Search States const [selectedSubcategory, setSelectedSubcategory] = useState('ทั้งหมด'); const [searchQuery, setSearchQuery] = useState(''); // Cart & Checkout States const [cart, setCart] = useState([]); const [isCartOpen, setIsCartOpen] = useState(false); const [slipImage, setSlipImage] = useState(null); const [customerName, setCustomerName] = useState(''); const [customerPhone, setCustomerPhone] = useState(''); const [orderStatus, setOrderStatus] = useState('idle'); const fileInputRef = useRef(null); // Addon Selection Modal States const [addonModalItem, setAddonModalItem] = useState(null); const [selectedAddons, setSelectedAddons] = useState([]); // ผลงานของร้าน const portfolios = [ { name: 'งานเว็บ', url: 'https://www.facebook.com/profile.php?id=61574306123714', image: 'https://i.postimg.cc/3xfvL4s0/att-Xi-Rr-QYx-FFfwak-V-cjxz9MT4YLUdd-GIEz-XD8-2Rz-D3UE.jpg' }, { name: 'งานป้าย', url: 'https://drive.google.com/drive/folders/1ovnS1imBV8jKO-fCHfBhnd3NDw8Yl1lv', image: 'https://i.postimg.cc/3xfvL4s0/att-Xi-Rr-QYx-FFfwak-V-cjxz9MT4YLUdd-GIEz-XD8-2Rz-D3UE.jpg' }, { name: 'งานสติกเกอร์', url: 'https://drive.google.com/drive/folders/1rd5PkbYVH5lnOSqgqLH7SdDBvwX5Ae3b', image: 'https://i.postimg.cc/3xfvL4s0/att-Xi-Rr-QYx-FFfwak-V-cjxz9MT4YLUdd-GIEz-XD8-2Rz-D3UE.jpg' }, { name: 'ปล่อยเอฟ', url: 'https://www.facebook.com/media/set/?set=a.122153343278842506&type=3', image: 'https://i.postimg.cc/3xfvL4s0/att-Xi-Rr-QYx-FFfwak-V-cjxz9MT4YLUdd-GIEz-XD8-2Rz-D3UE.jpg' } ]; // --- Optimized Data Calculations (ใส่เกราะป้องกันข้อมูลว่าง) --- const uniqueSubcategories = useMemo(() => { const subs = categories.map(c => c.subcategory).filter(Boolean); return ['ทั้งหมด', ...new Set(subs)]; }, [categories]); const filteredCategories = useMemo(() => { return categories.filter(item => { const matchSub = selectedSubcategory === 'ทั้งหมด' || item.subcategory === selectedSubcategory; // ป้องกัน Error หาก item.name เป็นค่าว่าง (undefined) const itemName = item.name || ''; const matchSearch = !searchQuery || itemName.toLowerCase().includes(searchQuery.toLowerCase()); return matchSub && matchSearch; }); }, [categories, selectedSubcategory, searchQuery]); // --- Firebase Effects --- useEffect(() => { const initAuth = async () => { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { await signInWithCustomToken(auth, __initial_auth_token); } else { await signInAnonymously(auth); } }; initAuth(); const unsubscribe = onAuthStateChanged(auth, setUser); return () => unsubscribe(); }, []); useEffect(() => { if (!user) return; // Fetch Categories (สินค้า) const catRef = collection(db, 'artifacts', appId, 'public', 'data', 'categories'); const unsubCat = onSnapshot(catRef, (snapshot) => { setCategories(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })).sort((a,b) => (b.createdAt || 0) - (a.createdAt || 0))); }, (err) => console.error(err)); // Fetch Services (งานสั่งทำ) const servRef = collection(db, 'artifacts', appId, 'public', 'data', 'services'); const unsubServ = onSnapshot(servRef, (snapshot) => { setOrderWorks(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })).sort((a,b) => (b.createdAt || 0) - (a.createdAt || 0))); }, (err) => console.error(err)); // Fetch Reviews const reviewsRef = collection(db, 'artifacts', appId, 'public', 'data', 'reviews'); const unsubRev = onSnapshot(reviewsRef, (snapshot) => { setReviews(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })).sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0))); }, (err) => console.error(err)); // Fetch Settings (PIN & LINE URL) const settingsRef = doc(db, 'artifacts', appId, 'public', 'data', 'settings', 'adminConfig'); const unsubSettings = onSnapshot(settingsRef, (docSnap) => { if (docSnap.exists()) { if (docSnap.data().pin) setAdminPin(docSnap.data().pin); if (docSnap.data().lineUrl) { setLineUrl(docSnap.data().lineUrl); setNewLineUrlInput(docSnap.data().lineUrl); } } }, (err) => console.error(err)); // Fetch Shop Info const shopConfigRef = doc(db, 'artifacts', appId, 'public', 'data', 'settings', 'shopConfig'); const unsubShop = onSnapshot(shopConfigRef, (docSnap) => { if (docSnap.exists()) { setShopInfo(prev => ({...prev, ...docSnap.data()})); setEditShopInfo(prev => ({...prev, ...docSnap.data()})); } }, (err) => console.error(err)); return () => { unsubCat(); unsubServ(); unsubRev(); unsubSettings(); unsubShop(); }; }, [user]); // Fetch Orders (Admin Only) useEffect(() => { if (!user || !isAdminMode) return; const ordersRef = collection(db, 'artifacts', appId, 'public', 'data', 'orders'); const unsubscribe = onSnapshot(ordersRef, (snapshot) => { setOrders(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })).sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0))); }, (error) => console.error(error)); return () => unsubscribe(); }, [user, isAdminMode]); // --- Cart Functions --- const addToCart = (product, newAddons = null) => { let finalProduct = { ...product }; // คำนวณราคาใหม่ถ้ารายการนั้นมี Addons แนบมาด้วยจากการกดใหม่ if (newAddons !== null) { if (newAddons.length > 0) { const addonsTotal = newAddons.reduce((sum, a) => sum + (Number(a.price) || 0), 0); finalProduct.price = (Number(product.price) || 0) + addonsTotal; finalProduct.selectedAddons = newAddons; } else { finalProduct.selectedAddons = null; } } // สร้าง ID พิเศษให้ตะกร้า เพื่อแยกสินค้าที่เลือก addon ไม่เหมือนกัน const uid = finalProduct.id + (finalProduct.selectedAddons ? JSON.stringify(finalProduct.selectedAddons) : ''); finalProduct.cartUid = uid; setCart((prev) => { const existing = prev.find((item) => item.cartUid === uid); if (existing) return prev.map((item) => item.cartUid === uid ? { ...item, quantity: item.quantity + 1 } : item); return [...prev, { ...finalProduct, quantity: 1 }]; }); setIsCartOpen(true); }; const decreaseQuantity = (cartUid) => { setCart((prev) => { const existing = prev.find((item) => item.cartUid === cartUid); if (existing.quantity === 1) return prev.filter((item) => item.cartUid !== cartUid); return prev.map((item) => item.cartUid === cartUid ? { ...item, quantity: item.quantity - 1 } : item); }); }; const removeFromCart = (cartUid) => setCart((prev) => prev.filter((item) => item.cartUid !== cartUid)); const totalPrice = useMemo(() => cart.reduce((total, item) => total + (Number(item.price) || 0) * (Number(item.quantity) || 0), 0), [cart]); const totalItems = useMemo(() => cart.reduce((total, item) => total + (Number(item.quantity) || 0), 0), [cart]); const handleFileUpload = (e, setter) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onloadend = () => setter(reader.result); reader.readAsDataURL(file); } }; // --- Checkout --- const handleCheckout = async () => { if (cart.length === 0) return alert('ตะกร้าว่างเปล่าจ้า'); if (!slipImage) return alert('รบกวนแนบสลิปด้วยน้า'); if (!customerName || !customerPhone) return alert('กรอกข้อมูลให้ครบถ้วนก่อนน้า'); if (!user) return alert('ระบบกำลังเชื่อมต่อฐานข้อมูล กรุณารอสักครู่ค่ะ'); setOrderStatus('processing'); const newOrder = { items: cart, totalPrice, customerName, customerPhone, slipImage, status: 'pending', createdAt: Date.now(), userId: user.uid }; try { const orderId = Date.now().toString(); const orderRef = doc(db, 'artifacts', appId, 'public', 'data', 'orders', orderId); await setDoc(orderRef, newOrder); setOrderStatus('success'); } catch (error) { console.error("Error saving order:", error); alert('เกิดข้อผิดพลาดในการสั่งซื้อค่ะ (ไฟล์สลิปอาจจะใหญ่เกินไป)'); setOrderStatus('idle'); } }; const finishAndCloseCart = () => { setIsCartOpen(false); setOrderStatus('idle'); setCart([]); setSlipImage(null); setCustomerName(''); setCustomerPhone(''); }; const openLineChat = () => { const finalLineUrl = lineUrl || 'https://lin.ee/nhLfGj0'; window.open(finalLineUrl, '_blank'); finishAndCloseCart(); }; const handleAddReview = async () => { if (!newReviewText.trim()) return alert('กรุณาพิมพ์ข้อความรีวิวก่อนน้า'); if (!user) return alert('รอโหลดระบบสักครู่นะคะ'); try { const reviewId = Date.now().toString(); await setDoc(doc(db, 'artifacts', appId, 'public', 'data', 'reviews', reviewId), { name: newReviewName.trim() || 'ลูกค้าไม่ประสงค์ออกนาม', text: newReviewText, rating: newReviewRating, createdAt: Date.now(), userId: user.uid }); setNewReviewText(''); setNewReviewName(''); setNewReviewRating(5); } catch (error) { alert('เกิดข้อผิดพลาดในการส่งรีวิว'); } }; // --- Admin Functions --- const handleAdminLogin = () => { if (adminPasswordInput === adminPin) { setIsAdminMode(true); setShowAdminLogin(false); setAdminPasswordInput(''); setAdminLoginError(false); } else { setAdminLoginError(true); setAdminPasswordInput(''); } }; const handleSaveSettings = async () => { if (window.confirm('คุณแน่ใจหรือไม่ที่จะบันทึกการตั้งค่า?')) { try { const payload = {}; if (newPinInput.trim() !== "") payload.pin = newPinInput.trim(); payload.lineUrl = newLineUrlInput.trim(); await setDoc(doc(db, 'artifacts', appId, 'public', 'data', 'settings', 'adminConfig'), payload, { merge: true }); await setDoc(doc(db, 'artifacts', appId, 'public', 'data', 'settings', 'shopConfig'), editShopInfo, { merge: true }); alert('บันทึกการตั้งค่าเรียบร้อยแล้ว!'); setNewPinInput(''); } catch (error) { console.error(error); alert('เปลี่ยนการตั้งค่าไม่สำเร็จ'); } } }; const handleUpdateOrderStatus = async (orderId, currentStatus) => { try { await updateDoc(doc(db, 'artifacts', appId, 'public', 'data', 'orders', orderId), { status: currentStatus === 'pending' ? 'completed' : 'pending' }); } catch (error) { console.error(error); } }; const handleDeleteItem = async (collectionName, id) => { if(window.confirm('ต้องการลบรายการนี้ใช่หรือไม่?')) { try { await deleteDoc(doc(db, 'artifacts', appId, 'public', 'data', collectionName, id)); } catch (error) { console.error(error); } } }; const openEditModal = (type, item = null) => { setEditType(type); if (item) { setEditFormData({ ...item, addons: item.addons || [], subcategory: item.subcategory || '' }); } else { setEditFormData({ id: '', name: '', price: '', image: '', desc: '', tag: '', previewUrl: '', addons: [], subcategory: '' }); } setIsEditingItem(true); }; // Addon Editor Functions (Admin) const handleAddonChange = (idx, field, value) => { const newAddons = [...(editFormData.addons || [])]; newAddons[idx] = { ...newAddons[idx], [field]: value }; setEditFormData({ ...editFormData, addons: newAddons }); }; const handleAddAddonField = () => { setEditFormData({ ...editFormData, addons: [...(editFormData.addons || []), { name: '', price: '' }] }); }; const handleRemoveAddonField = (idx) => { const newAddons = [...(editFormData.addons || [])]; newAddons.splice(idx, 1); setEditFormData({ ...editFormData, addons: newAddons }); }; const handleSaveItem = async () => { if(!editFormData.name || !editFormData.price) return alert('กรุณากรอกชื่อและราคาให้ครบ'); try { const isNew = !editFormData.id; const itemId = isNew ? Date.now().toString() : editFormData.id; const itemRef = doc(db, 'artifacts', appId, 'public', 'data', editType, itemId); const payload = { name: editFormData.name, price: Number(editFormData.price) || 0, image: editFormData.image || 'https://images.unsplash.com/photo-1611162617474-5b21e879e113?auto=format&fit=crop&w=300&q=80', tag: editFormData.tag || '', subcategory: editType === 'categories' ? (editFormData.subcategory || '') : '', desc: editFormData.desc || '', previewUrl: editFormData.previewUrl || '', addons: editFormData.addons ? editFormData.addons.filter(a => (a.name || '').trim() !== '').map(a => ({ name: a.name, price: Number(a.price) || 0 })) : [], createdAt: editFormData.createdAt || Date.now() }; await setDoc(itemRef, payload, { merge: true }); setIsEditingItem(false); } catch (error) { console.error(error); alert('บันทึกไม่สำเร็จ'); } }; const loadSampleData = async () => { if(!user) return; try { const sampleCats = [ { id: 'c1', name: 'พรีเซ็ตแต่งรูป', price: 150, image: 'https://images.unsplash.com/photo-1611162617474-5b21e879e113?auto=format&fit=crop&w=300&q=80', tag: 'Hot', subcategory: 'พรีเซ็ต' }, { id: 'c2', name: 'เทมเพลต IG Story', price: 290, image: 'https://images.unsplash.com/photo-1616469829581-73993eb86b02?auto=format&fit=crop&w=300&q=80', tag: 'New', subcategory: 'เทมเพลต' }, ]; const sampleServs = [ { id: 'w1', name: 'ออกแบบงานเว็บ', desc: 'มีบริการเพิ่มรูปและเพลงได้ตามใจชอบเลยค่ะ', price: 129, image: 'https://images.unsplash.com/photo-1559589689-577aabd1ce4c?auto=format&fit=crop&w=300&q=80', addons: [{name: 'ใส่รูป 10 รูป', price: 50}, {name: 'ใส่เพลง', price: 59}, {name: 'ใส่วิดีโอ', price: 69}] }, ]; for(let cat of sampleCats) { await setDoc(doc(db, 'artifacts', appId, 'public', 'data', 'categories', Date.now().toString() + Math.random()), { ...cat, createdAt: Date.now() }); } for(let srv of sampleServs) { await setDoc(doc(db, 'artifacts', appId, 'public', 'data', 'services', Date.now().toString() + Math.random()), { ...srv, createdAt: Date.now() }); } alert('โหลดข้อมูลตัวอย่างสำเร็จ!'); } catch(e) { console.error(e); } }; // --- Render Views --- const renderHome = () => (
Shop Logo

{shopInfo.name}

{shopInfo.slogan}

รายละเอียด & กฎของร้าน

ระยะเวลารองาน

  • งานเว็บ: ตามปกติรองาน 4-8 ชั่วโมง แล้วแต่คิวของวันนั้นๆ
  • งานป้าย/สติกเกอร์: คิวไม่เยอะรอภายในวันที่สั่ง ปกติ 1-2 วัน แล้วแต่คิวแต่ละวัน

เรทราคางาน

  • งานเว็บ เอาตามแบบที่ร้านมีให้ราคา 129.-
  • งานเว็บ ให้ทางร้านออกแบบใหม่ไม่ซ้ำใครราคา 329.-
  • งานเว็บ ที่ร้านปล่อยเอฟไม่เหมือนใครราคา 300.-
  • งานป้าย เริ่มต้น 89.- {'{ขึ้นอยู่กับแนวและรายละเอียด}'}
  • งานสติกเกอร์แชท 6 ตัว 59.- / 9 ตัว 75.-
  • งานสติกเกอร์เจนรูป/รูปตัวเอง เซ็ตละ 110.-
  • งานสติกเกอร์ไลน์เริ่มต้น 280.-

*** งานเร่งราคาคูณ 2 ***

📍 กฎของร้าน 📍

  • • ถ้าโอนเงินแล้วทางร้านไม่คืนเงินทุกกรณีนะคะ
  • • หากยกเลิกงานหลังส่งบรีฟมาแล้ว หากงานเริ่มไปแล้วจะคืนแค่ 50% ค่ะ
  • • หากยกเลิกหลังจากเช็คงาน = ไม่คืน และส่งงานให้ตามปกติ
  • • หากแก้งานหลัง 1 เดือน นับจากส่งเว็บ +เพิ่มครั้งละ 50 {'{ขึ้นอยู่กับความยาก}'}
); const renderCategory = () => { return (
setSearchQuery(e.target.value)} className="w-full pl-12 pr-4 py-3.5 bg-white border-2 border-pink-100 rounded-full focus:outline-none focus:border-pink-300 focus:ring-4 focus:ring-pink-100/50 text-[#785346] shadow-sm transition-all placeholder:text-pink-200" />
{uniqueSubcategories.length > 1 && (
{uniqueSubcategories.map(sub => ( ))}
)}

สินค้า {selectedSubcategory !== 'ทั้งหมด' ? `หมวดหมู่ "${selectedSubcategory}"` : 'ทั้งหมด'}

{filteredCategories.length === 0 ? (

ไม่พบสินค้าที่ค้นหา

) : (
{filteredCategories.map((item) => (
{item.tag && {item.tag}}
{item.name}

{item.name}

฿{item.price}

{item.previewUrl && ( )}
))}
)}
); }; const renderOrder = () => (

รายการสั่งงาน

{orderWorks.length === 0 ? (
ยังไม่มีรายการงานสั่งทำ
) : ( orderWorks.map((work) => (
{work.name} {work.addons && work.addons.length > 0 && (
มีออปชั่นเสริม
)}

{work.name}

{work.desc}

เริ่มต้น ฿{work.price}
{work.previewUrl && ( )}
)) )}
); const renderPortfolio = () => (

ผลงานของเรา

{portfolios.map((item, idx) => (
window.open(item.url, '_blank')} className="bg-white p-2 rounded-2xl shadow-sm border border-pink-50 cursor-pointer hover:shadow-md transition-all group">
{item.name}

{item.name}

))}
{/* กฎข้อบังคับผลงาน */}

💥ไม่อนุญาตให้ก๊อป ตัด หิ้ว ผลงานของทางร้าน ผิดกฎพบเจอ ชิ้นงานละ 500 บาท📍

หลังจากได้รับงานแล้วให้เซฟภายในสิ้นเดือน ร้านลบไฟล์ทุกสิ้นเดือน หากไม่เซฟทางร้านไม่รับผิดชอบใดๆทั้งสิ้น
ได้รับงานแล้วแต่อยากแก้ จุดละ 10 บาท 📍

); const renderReview = () => (

รีวิวจากลูกค้า

เขียนรีวิวให้ร้านเรา

{[1, 2, 3, 4, 5].map((star) => ( ))}
setNewReviewName(e.target.value)} className="w-full px-4 py-2.5 bg-[#FFFBFB] border border-pink-100 rounded-xl focus:outline-none text-sm"/>