100 rating,
101 amenities: hotelAmenities,
102 imageUrl: `https://placehold.co/600x400/png?text=Hotel+${i+1}`,
103 weeklyPrice,
104 nightlyPrice
12 rating: number;
13 amenities: string[];
14 imageUrl: string;
15 weeklyPrice: number;
16 nightlyPrice: number;
18
19 <!-- Favicon -->
20 <link rel="icon" href="/frontend/assets/favicon.ico" type="image/x-icon">
21
22 <!-- Preload key assets -->
50 src: "/frontend/assets/icon-192.png",
51 sizes: "192x192",
52 type: "image/png"
53 },
54 {
55 src: "/frontend/assets/icon-512.png",
56 sizes: "512x512",
57 type: "image/png"
58 }
59 ]
458 <title>Smart Quiz Challenge</title>
459 <meta name="viewport" content="width=device-width, initial-scale=1">
460 <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🧠</text></svg>">
461 <style>
462 body {
77 return <p>{message.content}</p>;
78
79 case MessageType.IMAGE:
80 return (
81 <img
82 src={`/api/blob/${message.content}`}
83 alt="Shared image"
84 className="message-image"
85 onClick={() => window.open(`/api/blob/${message.content}`, '_blank')}
86 />
279 };
280
281 // Handle image upload
282 const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
283 const files = e.target.files;
284 if (!files || files.length === 0) return;
286 const file = files[0];
287 const formData = new FormData();
288 formData.append('image', file);
289
290 try {
291 const response = await fetch('/api/upload/image', {
292 method: 'POST',
293 body: formData
297
298 if (data.key) {
299 // Send image message
300 if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
301 wsRef.current.send(JSON.stringify({
302 type: 'message',
303 messageType: MessageType.IMAGE,
304 content: data.key
305 }));
307 }
308 } catch (error) {
309 console.error('Error uploading image:', error);
310 }
311
356 <input
357 type="file"
358 accept="image/*"
359 style={{ display: 'none' }}
360 onChange={handleImageUpload}
361 />
362 📷
167}
168
169.message-image {
170 max-width: 300px;
171 max-height: 300px;
88});
89
90// Upload image
91app.post("/api/upload/image", async c => {
92 try {
93 const formData = await c.req.formData();
94 const file = formData.get("image") as File;
95
96 if (!file) {
97 return c.json({ error: "No image provided" }, 400);
98 }
99
100 const buffer = await file.arrayBuffer();
101 const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
102 const key = `chat_image_${Date.now()}`;
103
104 // Store image in blob storage
105 await blob.set(key, base64);
106
107 return c.json({ key });
108 } catch (error) {
109 console.error("Image upload error:", error);
110 return c.json({ error: "Failed to upload image" }, 500);
111 }
112});
136});
137
138// Get blob content (image or voice)
139app.get("/api/blob/:key", async c => {
140 try {
148 // Determine content type based on key prefix
149 let contentType = "application/octet-stream";
150 if (key.startsWith("chat_image_")) {
151 contentType = "image/jpeg";
152 } else if (key.startsWith("chat_voice_")) {
153 contentType = "audio/webm";
3 TEXT = 'text',
4 VOICE = 'voice',
5 IMAGE = 'image',
6}
7
26 username: string;
27 type: MessageType;
28 content: string; // Text content or blob key for voice/image
29 timestamp: string; // ISO date string
30}
6- Send and receive text messages in real-time
7- Record and share voice messages
8- Upload and share images
9- See who's currently online
10
14- **Frontend** (`/frontend/components/App.tsx`): React application for the chat interface
15- **Database**: SQLite for storing users, messages, and file references
16- **Storage**: Val Town blob storage for images and voice recordings
17
18## How to Use
212. Send text messages by typing in the input field and pressing Enter
223. Record voice messages by clicking the microphone button
234. Upload images by clicking the image button
245. See who's online in the user list on the side panel