Upload files to "/"
This commit is contained in:
BIN
656021437_18185552755368114_8753230857366729982_n.jpg
Normal file
BIN
656021437_18185552755368114_8753230857366729982_n.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 281 KiB |
422
new 1.txt
Normal file
422
new 1.txt
Normal file
@@ -0,0 +1,422 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ST Yellow Pages</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Space+Grotesk', sans-serif;
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
.poster-card {
|
||||
transition: transform 0.2s ease;
|
||||
border: 3px solid #000;
|
||||
box-shadow: 6px 6px 0px 0px #000;
|
||||
}
|
||||
.poster-card:active {
|
||||
transform: translate(3px, 3px);
|
||||
box-shadow: 3px 3px 0px 0px #000;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.bg-mustard { background-color: #FBBF24; }
|
||||
.bg-sky { background-color: #60A5FA; }
|
||||
.bg-pink { background-color: #F472B6; }
|
||||
.bg-sage { background-color: #A7F3D0; }
|
||||
|
||||
header {
|
||||
max-height: 30vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Modal Overlay Animation */
|
||||
.modal-overlay {
|
||||
background-color: rgba(0,0,0,0.8);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* Grab cursor for desktop drag-to-scroll */
|
||||
.drag-scroll {
|
||||
cursor: grab;
|
||||
}
|
||||
.drag-scroll:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100 min-h-screen">
|
||||
<div id="root"></div>
|
||||
|
||||
<script type="text/babel">
|
||||
const { useState, useEffect, useMemo, useRef } = React;
|
||||
|
||||
const Character = ({ type, colorClass }) => {
|
||||
if (type === 'dog') {
|
||||
return (
|
||||
<svg viewBox="0 0 100 100" className={`w-full h-24 ${colorClass}`}>
|
||||
<path d="M30,40 Q30,20 50,20 Q70,20 70,40 L70,80 L30,80 Z" fill="currentColor" />
|
||||
<circle cx="45" cy="35" r="3" fill="black" />
|
||||
<path d="M70,35 L85,45 L70,55 Z" fill="currentColor" stroke="black" strokeWidth="1" />
|
||||
<rect x="40" y="70" width="20" height="10" fill="#ff6b6b" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (type === 'cat') {
|
||||
return (
|
||||
<svg viewBox="0 0 100 100" className={`w-full h-24 ${colorClass}`}>
|
||||
<circle cx="50" cy="50" r="35" fill="currentColor" />
|
||||
<path d="M25,25 L35,45 L15,40 Z" fill="currentColor" />
|
||||
<path d="M75,25 L65,45 L85,40 Z" fill="currentColor" />
|
||||
<circle cx="40" cy="45" r="4" fill="black" />
|
||||
<circle cx="60" cy="45" r="4" fill="black" />
|
||||
<path d="M45,60 Q50,65 55,60" fill="none" stroke="black" strokeWidth="2" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<svg viewBox="0 0 100 100" className={`w-full h-24 ${colorClass}`}>
|
||||
<rect x="25" y="25" width="50" height="50" rx="10" fill="currentColor" />
|
||||
<circle cx="40" cy="45" r="4" fill="black" />
|
||||
<circle cx="60" cy="45" r="4" fill="black" />
|
||||
<path d="M40,65 L60,65" stroke="black" strokeWidth="3" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
const [listings, setListings] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('All');
|
||||
const [copyFeedback, setCopyFeedback] = useState(null);
|
||||
const [isReadmeOpen, setIsReadmeOpen] = useState(false);
|
||||
|
||||
// Refs and state for drag-to-scroll
|
||||
const scrollRef = useRef(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [startX, setStartX] = useState(0);
|
||||
const [scrollLeftState, setScrollLeftState] = useState(0);
|
||||
|
||||
const colors = ['bg-mustard', 'bg-sky', 'bg-pink', 'bg-sage'];
|
||||
const charTypes = ['dog', 'cat', 'other'];
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof google !== 'undefined' && google.script && google.script.run) {
|
||||
google.script.run
|
||||
.withSuccessHandler((data) => {
|
||||
setListings(data || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.withFailureHandler((err) => {
|
||||
setError("Connection Failed: " + err.message);
|
||||
setLoading(false);
|
||||
})
|
||||
.getDirectoryData();
|
||||
} else {
|
||||
setError("Not in Google environment. Please deploy as Web App.");
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const categories = useMemo(() => {
|
||||
const cats = ['All', ...new Set(listings.map(l => l.category))];
|
||||
return cats;
|
||||
}, [listings]);
|
||||
|
||||
// Drag-to-scroll handlers
|
||||
const handleMouseDown = (e) => {
|
||||
setIsDragging(true);
|
||||
setStartX(e.pageX - scrollRef.current.offsetLeft);
|
||||
setScrollLeftState(scrollRef.current.scrollLeft);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (!isDragging) return;
|
||||
e.preventDefault();
|
||||
const x = e.pageX - scrollRef.current.offsetLeft;
|
||||
const walk = (x - startX) * 2; // scroll speed multiplier
|
||||
scrollRef.current.scrollLeft = scrollLeftState - walk;
|
||||
};
|
||||
|
||||
const handleSearchChange = (e) => {
|
||||
const value = e.target.value;
|
||||
setSearchTerm(value);
|
||||
if (value.trim() !== "") {
|
||||
setSelectedCategory('All');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCallClick = (phone, id) => {
|
||||
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
|
||||
const isIOS = /iPad|iPhone|iPod/.test(userAgent) && !window.MSStream;
|
||||
const isOtherMobile = /Android|webOS|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
|
||||
|
||||
if (isIOS) {
|
||||
copyToClipboard(phone, id);
|
||||
} else if (isOtherMobile) {
|
||||
window.location.href = `tel:${phone}`;
|
||||
} else {
|
||||
copyToClipboard(phone, id);
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = (text, id) => {
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
setCopyFeedback(id);
|
||||
setTimeout(() => setCopyFeedback(null), 2000);
|
||||
} catch (err) {
|
||||
console.error('Copy failed', err);
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
};
|
||||
|
||||
const filteredListings = useMemo(() => {
|
||||
return listings.filter(item => {
|
||||
const searchLower = searchTerm.toLowerCase().trim();
|
||||
const matchesSearch = searchTerm === "" ||
|
||||
item.name.toLowerCase().includes(searchLower) ||
|
||||
item.category.toLowerCase().includes(searchLower) ||
|
||||
item.tags.some(tag => tag.toLowerCase().includes(searchLower));
|
||||
const matchesCategory = selectedCategory === 'All' || item.category === selectedCategory;
|
||||
return matchesSearch && matchesCategory;
|
||||
});
|
||||
}, [searchTerm, selectedCategory, listings]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-mustard p-6">
|
||||
<div className="animate-bounce">
|
||||
<Character type="dog" colorClass="text-black" />
|
||||
</div>
|
||||
<h1 className="mt-4 text-2xl font-bold uppercase tracking-tighter text-center">Now loading...</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-pink p-6 text-center">
|
||||
<h1 className="text-4xl font-bold uppercase mb-4">Oops!</h1>
|
||||
<p className="bg-white border-2 border-black p-4 font-bold shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
|
||||
{error}
|
||||
</p>
|
||||
<p className="mt-6 text-sm">Make sure you have deployed as 'Web App' and have a 'Directory' tab.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-md mx-auto min-h-screen bg-white shadow-2xl pb-2 border-x-4 border-black relative">
|
||||
<header className="p-4 bg-white border-b-4 border-black sticky top-0 z-10">
|
||||
<h1 className="text-3xl font-bold uppercase tracking-tighter leading-none mb-3">
|
||||
ST Yellow Pages
|
||||
</h1>
|
||||
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search name, category, or tag..."
|
||||
className="w-full p-3 border-2 border-black rounded-none focus:outline-none focus:ring-4 focus:ring-sky-200 text-lg shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] placeholder:text-gray-400 font-bold"
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseMove={handleMouseMove}
|
||||
className="flex overflow-x-auto gap-2 mt-4 pb-2 custom-scrollbar drag-scroll"
|
||||
>
|
||||
{categories.map(cat => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => {
|
||||
setSelectedCategory(cat);
|
||||
setSearchTerm('');
|
||||
}}
|
||||
className={`px-3 py-1 border-2 border-black whitespace-nowrap font-bold text-xs uppercase transition-colors
|
||||
${selectedCategory === cat ? 'bg-black text-white' : 'bg-white text-black'}`}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="p-4 space-y-4">
|
||||
{filteredListings.length === 0 ? (
|
||||
<div className="text-center py-20 bg-gray-50 border-2 border-dashed border-black">
|
||||
<p className="text-xl font-bold opacity-50 uppercase">No Entries Found</p>
|
||||
<p className="text-sm opacity-50 px-10 mt-2">Try adjusting your search or filters.</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredListings.map((item, idx) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`poster-card p-4 ${colors[idx % colors.length]}`}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div className="w-2/3">
|
||||
<h2 className="text-3xl font-bold leading-tight uppercase border-b-2 border-black inline-block mb-1">
|
||||
{item.name}
|
||||
</h2>
|
||||
<p className="text-lg font-bold uppercase block">{item.category}</p>
|
||||
</div>
|
||||
<div className="w-20 h-20 bg-white border-2 border-black p-1 flex items-center justify-center overflow-hidden flex-shrink-0">
|
||||
{item.imageUrl && item.imageUrl.startsWith('http') ? (
|
||||
<img src={item.imageUrl} alt={item.name} className="object-cover w-full h-full" />
|
||||
) : (
|
||||
<Character type={charTypes[idx % charTypes.length]} colorClass="text-black" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-base mb-3 line-clamp-3 leading-snug font-medium italic">
|
||||
"{item.description}"
|
||||
</p>
|
||||
|
||||
{item.tags && item.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-3">
|
||||
{item.tags.map(tag => (
|
||||
<span key={tag} className="text-[11px] bg-black text-white px-2 py-0.5 rounded-full font-bold uppercase">
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs font-bold mb-3 opacity-75 uppercase">
|
||||
Source: {item.source}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
onClick={() => handleCallClick(item.phone, item.id)}
|
||||
className={`border-2 border-black py-2 text-center font-bold uppercase text-lg shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] active:translate-y-0.5 transition-all
|
||||
${copyFeedback === item.id ? 'bg-green-400 text-black' : 'bg-white text-black'}`}
|
||||
>
|
||||
{copyFeedback === item.id ? 'Copied!' : 'Call'}
|
||||
</button>
|
||||
<a
|
||||
href={`https://wa.me/${item.whatsapp}`}
|
||||
target="_blank"
|
||||
className="bg-black text-white border-2 border-black py-2 text-center font-bold uppercase text-lg shadow-[2px_2px_0px_0px_rgba(255,255,255,0.2)] active:translate-y-0.5 transition-all"
|
||||
>
|
||||
Message
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</main>
|
||||
|
||||
<footer className="mt-8 px-4 py-8 border-t-4 border-black text-center bg-gray-50">
|
||||
{/* Optimized footer flex container: narrower gaps and nowrap to prevent wrapping */}
|
||||
<div className="flex flex-wrap justify-center gap-x-2 sm:gap-x-4 gap-y-2 mb-6 text-[11px] sm:text-sm font-bold uppercase tracking-tight">
|
||||
<a href="https://forms.gle/oNr7Ri9rEP5DkfBg7" target="_blank" className="border-b-2 border-black pb-0.5 hover:bg-black hover:text-white transition-all px-0.5 whitespace-nowrap">Have Referral?</a>
|
||||
<button
|
||||
onClick={(e) => { e.preventDefault(); setIsReadmeOpen(true); }}
|
||||
className="border-b-2 border-black pb-0.5 hover:bg-black hover:text-white transition-all px-0.5 uppercase font-bold whitespace-nowrap"
|
||||
>
|
||||
Readme
|
||||
</button>
|
||||
<a href="mailto:rick26.business@gmail.com" target="_blank" className="border-b-2 border-black pb-0.5 hover:bg-black hover:text-white transition-all px-0.5 whitespace-nowrap">Contact Me</a>
|
||||
|
||||
{/*
|
||||
<a href="https://www.google.com" target="_blank" className="border-b-2 border-black pb-0.5 hover:bg-black hover:text-white transition-all px-0.5 whitespace-nowrap">Terms</a> */}
|
||||
</div>
|
||||
|
||||
<div className="text-[10px] font-bold opacity-40 uppercase leading-relaxed tracking-widest">
|
||||
© {new Date().getFullYear()} ST Yellow Pages.<br/>
|
||||
All Rights Reserved.
|
||||
<div className="mt-1 opacity-100">
|
||||
Created by <a href="https://www.google.com" target="_blank" className="underline hover:text-black transition-colors font-bold">Rick</a> - Live v1.0
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{/* Neo-Brutalist README Modal */}
|
||||
{isReadmeOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 modal-overlay">
|
||||
<div className="bg-white border-4 border-black w-full max-w-sm shadow-[12px_12px_0px_0px_rgba(0,0,0,1)] animate-in fade-in zoom-in duration-200">
|
||||
<div className="p-4 bg-mustard border-b-4 border-black flex justify-between items-center">
|
||||
<h2 className="text-2xl font-bold uppercase tracking-tight">Readme</h2>
|
||||
{/*<button onClick={() => setIsReadmeOpen(false)} className="bg-white border-2 border-black w-8 h-8 font-bold hover:bg-black hover:text-white transition-colors">X</button>*/}
|
||||
</div>
|
||||
|
||||
<div className="p-6 max-h-[60vh] overflow-y-auto space-y-4 font-medium leading-relaxed">
|
||||
<section>
|
||||
<h3 className="font-bold uppercase text-lg border-b-2 border-black inline-block mb-2">About the App</h3>
|
||||
<p>Welcome to ST Yellow Pages. This app is designed for local neighbors to find trusted service providers easily.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 className="font-bold uppercase text-lg border-b-2 border-black inline-block mb-2">Terms and Agreement</h3>
|
||||
<p>By accessing or using the ST Yellow Pages, you acknowledge that you have read, understood, and agreed to be bound by our <a href="https://drive.google.com/file/d/1xYSHTs_f67WBM6ZitJn4cX-rlOX1CfMc/view?usp=sharing" target="_blank" className="underline font-bold">terms and agreement</a>.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 className="font-bold uppercase text-lg border-b-2 border-black inline-block mb-2">How to Use</h3>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li>Use the **Search Bar** to find specific names, categories, or tags.</li>
|
||||
<li>Click **Category Pills** to filter by specific service types.</li>
|
||||
<li>The **Call** button triggers a direct dial or copies the number.</li>
|
||||
<li>**Message** button opens a WhatsApp chat directly.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 className="font-bold uppercase text-lg border-b-2 border-black inline-block mb-2">The "Source"</h3>
|
||||
<p>We believe in community trust. The "Source" field shows which neighbor originally recommended this provider to ensure peace of mind.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 className="font-bold uppercase text-lg border-b-2 border-black inline-block mb-2">Privacy & Rights</h3>
|
||||
<p>All data is stored securely in our Google Sheet. We respect the privacy of both users and service providers.</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t-4 border-black bg-gray-50 flex justify-end">
|
||||
<button
|
||||
onClick={() => setIsReadmeOpen(false)}
|
||||
className="bg-black text-white px-8 py-2 font-bold uppercase tracking-widest shadow-[4px_4px_0px_0px_rgba(255,255,255,1)] hover:bg-white hover:text-black hover:border-2 hover:border-black transition-all active:translate-y-1"
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(<App />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user