Upload files to "/"

This commit is contained in:
2026-04-29 13:13:21 +00:00
commit 7bc2596e35
2 changed files with 422 additions and 0 deletions

422
new 1.txt Normal file
View 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>