106}
107
108export function getImageUrl(filename: string): string {
109 // For demo purposes, using placeholder images
110 const imageMap: Record<string, string> = {
111 'laptop-1.jpg': 'https://images.unsplash.com/photo-1496181133206-80ce9b88a853?w=500&h=500&fit=crop',
112 'phone-1.jpg': 'https://images.unsplash.com/photo-1511707171634-5f897ff02aa9?w=500&h=500&fit=crop',
113 'headphones-1.jpg': 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=500&h=500&fit=crop',
114 'watch-1.jpg': 'https://images.unsplash.com/photo-1523275335684-37898b6baf30?w=500&h=500&fit=crop',
115 'camera-1.jpg': 'https://images.unsplash.com/photo-1502920917128-1aa500764cbd?w=500&h=500&fit=crop',
116 'tablet-1.jpg': 'https://images.unsplash.com/photo-1544244015-0df4b3ffc6b0?w=500&h=500&fit=crop',
117 'speaker-1.jpg': 'https://images.unsplash.com/photo-1608043152269-423dbba4e7e1?w=500&h=500&fit=crop',
118 'keyboard-1.jpg': 'https://images.unsplash.com/photo-1541140532154-b024d705b90a?w=500&h=500&fit=crop',
119 };
120
121 return imageMap[filename] || `https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=500&h=500&fit=crop&q=80`;
122}
123
18 category: string;
19 brand: string;
20 imageUrl: string;
21 images: string[];
22 inStock: boolean;
23 stockQuantity: number;
92 slug: string;
93 description?: string;
94 imageUrl?: string;
95 productCount: number;
96}
8- **Homepage**: Hero section, product categories, featured products, testimonials
9- **Product Catalog**: Grid layout with filtering, sorting, and pagination
10- **Product Details**: Image gallery, reviews, related products
11- **Shopping Cart**: Item management with quantity controls
12- **Checkout**: Complete order flow with address and payment forms
24- **Database**: SQLite
25- **Styling**: TailwindCSS (Townie)
26- **Storage**: Val Town Blob for product images
27
28## Project Structure
27 };
28 highlights: string[];
29 imageUrl?: string;
30 website?: string;
31}
8 <script src="https://esm.town/v/std/catch"></script>
9 <link rel="stylesheet" href="/frontend/style.css">
10 <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📊</text></svg>">
11</head>
12<body class="bg-gradient-to-br from-blue-50 to-indigo-100 min-h-screen">
38 if (extension) {
39 const mimeTypes: Record<string, string> = {
40 'jpg': 'image/jpeg',
41 'jpeg': 'image/jpeg',
42 'png': 'image/png',
43 'gif': 'image/gif',
44 'pdf': 'application/pdf',
45 'doc': 'application/msword',
39 if (response.status > 400) {
40 const shortenedName = "Error (Forbidden)";
41 const image = "/assets/spotify.svg";
42 return { shortenedName, image };
43 } else if (response.status === 204) {
44 const shortenedName = "Currently Not Playing";
45 const image = "/assets/spotify.svg";
46 return { shortenedName, image };
47 }
48
49 const song = await response.json();
50 const image = song.item.album.images[0].url;
51 const artistNames = song.item.artists.map(a => a.name);
52 const link = song.item.external_urls.spotify;
65 formattedArtist,
66 artistLink,
67 image,
68 };
69 } catch (error) {
70 const shortenedName = "Error";
71 const image = "/assets/spotify.svg";
72 return { shortenedName, image };
73 }
74};
7 <script src="https://cdn.twind.style" crossorigin></script>
8 <script src="https://esm.town/v/std/catch"></script>
9 <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🛞</text></svg>">
10 <style>
11 /* Custom Protyre branding */
11});
12
13// Popular meme templates with AI-generated placeholder images
14const MEME_TEMPLATES: MemeTemplate[] = [
15 {
16 id: "drake",
17 name: "Drake Pointing",
18 imageUrl: "https://maxm-imggenurl.web.val.run/drake-meme-template-pointing-gesture-two-panels",
19 description: "Classic Drake pointing meme format"
20 },
22 id: "distracted-boyfriend",
23 name: "Distracted Boyfriend",
24 imageUrl: "https://maxm-imggenurl.web.val.run/distracted-boyfriend-meme-template-man-looking-back",
25 description: "Man looking back at another woman while girlfriend looks disapproving"
26 },
28 id: "woman-yelling-cat",
29 name: "Woman Yelling at Cat",
30 imageUrl: "https://maxm-imggenurl.web.val.run/woman-yelling-at-cat-meme-template-dinner-table",
31 description: "Woman pointing and yelling, confused cat at dinner table"
32 },
34 id: "expanding-brain",
35 name: "Expanding Brain",
36 imageUrl: "https://maxm-imggenurl.web.val.run/expanding-brain-meme-template-four-levels-enlightenment",
37 description: "Four levels of brain expansion showing increasing enlightenment"
38 },
40 id: "this-is-fine",
41 name: "This is Fine",
42 imageUrl: "https://maxm-imggenurl.web.val.run/this-is-fine-dog-meme-template-fire-coffee",
43 description: "Dog sitting in burning room with coffee saying this is fine"
44 },
46 id: "change-my-mind",
47 name: "Change My Mind",
48 imageUrl: "https://maxm-imggenurl.web.val.run/change-my-mind-meme-template-table-sign",
49 description: "Person sitting at table with sign"
50 }
11export default function MemeCanvas({ template, topText, bottomText }: MemeCanvasProps) {
12 const canvasRef = useRef<HTMLCanvasElement>(null);
13 const [imageLoaded, setImageLoaded] = useState(false);
14 const [isDownloading, setIsDownloading] = useState(false);
15
16 useEffect(() => {
17 drawMeme();
18 }, [template, topText, bottomText, imageLoaded]);
19
20 const drawMeme = () => {
33 ctx.fillRect(0, 0, canvas.width, canvas.height);
34
35 // Load and draw image
36 const img = new Image();
37 img.crossOrigin = 'anonymous';
38 img.onload = () => {
39 // Draw image to fit canvas
40 ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
41
42 // Draw text
44 drawText(ctx, bottomText, canvas.width / 2, canvas.height - 50, canvas.width - 20);
45
46 setImageLoaded(true);
47 };
48 img.onerror = () => {
61 drawText(ctx, bottomText, canvas.width / 2, canvas.height - 50, canvas.width - 20);
62
63 setImageLoaded(true);
64 };
65 img.src = template.imageUrl;
66 };
67
143
144 setIsDownloading(false);
145 }, 'image/png');
146 } catch (error) {
147 console.error('Error downloading meme:', error);
164 <button
165 onClick={downloadMeme}
166 disabled={isDownloading || !imageLoaded}
167 className="download-btn text-white font-bold py-3 px-6 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
168 >