1/** @jsxImportSource https://esm.sh/react@18.2.0 */
2import React, { useState, useEffect } from "https://esm.sh/react@18.2.0?deps=react@18.2.0";
3import { downloadImageAsBase64, setupClipboardListener } from "../../shared/types.ts";
45interface ImageSearchResult {
6url: string;
7thumbnail: string;
10}
1112interface ImageSearchModalProps {
13isOpen: boolean;
14onClose: () => void;
15onSelectImage: (imageUrl: string) => void;
16searchQuery?: string;
17}
1819export function ImageSearchModal({ isOpen, onClose, onSelectImage, searchQuery = '' }: ImageSearchModalProps) {
20const [query, setQuery] = useState(searchQuery);
21const [results, setResults] = useState<ImageSearchResult[]>([]);
22const [loading, setLoading] = useState(false);
23const [downloadingIndex, setDownloadingIndex] = useState<number | null>(null);
27useEffect(() => {
28if (isOpen) {
29const cleanup = setupClipboardListener((imageData: string) => {
30// Directly use the pasted image
31onSelectImage(imageData);
32onClose();
33});
34return cleanup;
35}
36}, [isOpen, onSelectImage, onClose]);
3738const searchImages = async (searchTerm: string) => {
39if (!searchTerm.trim()) return;
40
43
44try {
45const results: ImageSearchResult[] = [];
46
47// Try multiple sources for better media coverage
84tmdbData.results.slice(0, 8).forEach((item: any) => {
85if (item.poster_path) {
86const posterUrl = `https://image.tmdb.org/t/p/w500${item.poster_path}`;
87const thumbnailUrl = `https://image.tmdb.org/t/p/w300${item.poster_path}`;
88results.push({
89url: posterUrl,
100}
101
102// 3. If we still don't have enough results, add some smart themed placeholder images
103if (results.length < 8) {
104const remainingSlots = 12 - results.length;
120
121} catch (err) {
122console.error('Image search failed:', err);
123setError('Failed to search for images. Please try again.');
124
125// Generate themed placeholder results as final fallback
126const searchTermClean = searchTerm.replace(/[^a-zA-Z0-9\s]/g, '');
127const fallbackResults: ImageSearchResult[] = Array.from({ length: 8 }, (_, i) => ({
128url: `https://via.placeholder.com/400x600/e2e8f0/64748b?text=${encodeURIComponent(searchTermClean)}`,
129thumbnail: `https://via.placeholder.com/200x300/e2e8f0/64748b?text=${encodeURIComponent(searchTermClean)}`,
143setQuery(searchQuery);
144// Auto-search when modal opens with a search query
145searchImages(searchQuery);
146}
147}, [isOpen, searchQuery]);
149const handleSearch = (e: React.FormEvent) => {
150e.preventDefault();
151searchImages(query);
152};
153154const handleImageSelect = async (imageUrl: string, index: number) => {
155// Show loading state for this specific image
156setDownloadingIndex(index);
157
158try {
159// Download and convert the image to base64
160const base64Image = await downloadImageAsBase64(imageUrl, 500); // 500KB max
161
162if (base64Image) {
163// Pass the base64 data URL instead of the original URL
164onSelectImage(base64Image);
165onClose();
166} else {
167// If download failed, ask user if they want to use the URL anyway
168const useUrl = confirm(
169'Unable to download and store this image locally (may be due to server restrictions). ' +
170'Would you like to use the image URL instead? Note: it may not work offline.'
171);
172
173if (useUrl) {
174onSelectImage(imageUrl);
175onClose();
176}
177}
178} catch (error) {
179console.error('Error processing image:', error);
180
181// Ask user if they want to use the URL as fallback
182const useUrl = confirm(
183'Error downloading image. Would you like to use the image URL instead? ' +
184'Note: it may not work offline.'
185);
186
187if (useUrl) {
188onSelectImage(imageUrl);
189onClose();
190}
202<div className="px-6 py-4 border-b border-gray-700">
203<div className="flex items-center justify-between">
204<h2 className="text-lg font-semibold text-gray-100">Search for Cover Image</h2>
205<button
206type="button"
223value={query}
224onChange={(e) => setQuery(e.target.value)}
225placeholder="Search for images (e.g., book title, movie name, game title)"
226className="w-full px-3 py-2 border border-gray-600 bg-gray-800 text-gray-100 placeholder-gray-400 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
227autoFocus
240<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3-3m0 0l-3 3m3-3v12" />
241</svg>
242Images are downloaded and stored locally for offline viewing
243</div>
244<div className="mt-1 text-xs text-gray-400 text-center">
245๐ Or paste an image from clipboard (Ctrl/Cmd+V)
246</div>
247</div>
263<div className="text-center py-8">
264<div className="w-8 h-8 border-4 border-gray-600 border-t-blue-400 rounded-full animate-spin mx-auto mb-4"></div>
265<p className="text-gray-400">Searching for images...</p>
266</div>
267)}
274</svg>
275</div>
276<p className="text-gray-400">No images found. Try a different search term.</p>
277</div>
278)}
285</svg>
286</div>
287<p className="text-gray-400">Enter a search term to find cover images</p>
288</div>
289)}
291{results.length > 0 && (
292<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
293{results.map((image, index) => (
294<div
295key={index}
297downloadingIndex === index ? 'opacity-50 pointer-events-none' : ''
298}`}
299onClick={() => handleImageSelect(image.url, index)}
300>
301<div className="relative aspect-[3/4] bg-gray-100">
302<img
303src={image.thumbnail}
304alt={image.title}
305className="w-full h-full object-cover"
306onError={(e) => {
307const target = e.target as HTMLImageElement;
308target.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjMwMCIgdmlld0JveD0iMCAwIDIwMCAzMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMzAwIiBmaWxsPSIjRjNGNEY2Ii8+Cjx0ZXh0IHg9IjEwMCIgeT0iMTUwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzlDQTNBRiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+SW1hZ2UgTm90IEZvdW5kPC90ZXh0Pgo8L3N2Zz4=';
309}}
310/>
331</div>
332<div className="p-2 bg-gray-800">
333<p className="text-xs text-gray-600 truncate" title={image.title}>
334{image.title}
335</p>
336<p className="text-xs text-gray-400">
337via {image.source}
338</p>
339</div>
347<div className="px-6 py-4 border-t border-gray-700 bg-gray-700 flex justify-between items-center">
348<p className="text-xs text-gray-400">
349Click on any image to select it as your cover
350</p>
351<button
pathfinder__5712n7-boreAddMediaModal.tsx24 matches
8onClose: () => void;
9onAdd: (media: Omit<MediaItem, 'id' | 'user_id' | 'created_at' | 'updated_at'>) => void;
10onOpenImageSearch: (searchQuery: string, onSelectImage: (imageUrl: string) => void) => void;
11}
1241];
4243export function AddMediaModal({ isOpen, onClose, onAdd, onOpenImageSearch }: AddMediaModalProps) {
44const [formData, setFormData] = useState({
45title: '',
59
60const [saving, setSaving] = useState(false);
61const [imageError, setImageError] = useState(false);
62
63// Setup clipboard listener when modal is open
64useEffect(() => {
65if (isOpen) {
66const cleanup = setupClipboardListener((imageData: string) => {
67setFormData(prev => ({ ...prev, cover_url: imageData }));
68setImageError(false);
69});
70return cleanup;
72}, [isOpen]);
73
74const handleImageUrlChange = (url: string) => {
75setFormData(prev => ({ ...prev, cover_url: url }));
76setImageError(false);
77};
78
79const handleImageError = () => {
80setImageError(true);
81};
8283const handleImageSelect = (imageUrl: string) => {
84handleImageUrlChange(imageUrl);
85};
86
162</div>
163
164{/* Cover Image URL */}
165<div>
166<label htmlFor="cover_url" className="block text-sm font-medium text-gray-300 mb-1">
167Cover Image URL (optional)
168</label>
169<div className="space-y-2">
173id="cover_url"
174value={formData.cover_url}
175onChange={(e) => handleImageUrlChange(e.target.value)}
176placeholder="https://example.com/cover-image.jpg"
177className="flex-1 px-3 py-2 border border-gray-600 bg-gray-700 text-gray-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent placeholder-gray-400"
178/>
179<button
180type="button"
181onClick={() => onOpenImageSearch(formData.title || '', handleImageSelect)}
182className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
183title="Search for images"
184>
185๐
187</div>
188<div className="text-xs text-gray-400 text-center">
189๐ Paste an image from clipboard (Ctrl/Cmd+V) or search above
190</div>
191{formData.cover_url && (
196alt="Cover preview"
197className={`w-16 h-20 object-cover rounded border-2 ${
198imageError ? 'border-red-300 bg-red-50' : 'border-gray-300'
199}`}
200onError={handleImageError}
201onLoad={() => setImageError(false)}
202/>
203{imageError && (
204<div className="absolute inset-0 flex items-center justify-center bg-red-50 rounded text-red-500 text-xs">
205Invalid URL
210)}
211<p className="text-xs text-gray-500">
212Add a square image URL to represent this media item in the grid, or click the search button to find one
213</p>
214</div>
10onUpdate: (media: MediaItem) => void;
11onClose: () => void;
12onOpenImageSearch: (searchQuery: string, onSelectImage: (imageUrl: string) => void) => void;
13}
1453};
5455export function MediaDetailModal({ group, status, items, onUpdate, onClose, onOpenImageSearch }: MediaDetailModalProps) {
56const [editingItem, setEditingItem] = useState<MediaItem | null>(null);
57const [editForm, setEditForm] = useState({
74};
7576const handleImageSelect = (imageUrl: string) => {
77setEditForm(prev => ({ ...prev, cover_url: imageUrl }));
78};
79
120useEffect(() => {
121if (editingItem) {
122const cleanup = setupClipboardListener((imageData: string) => {
123setEditForm(prev => ({ ...prev, cover_url: imageData }));
124});
125return cleanup;
174
175<div>
176<label className="block text-sm font-medium text-gray-300 mb-1">Cover Image URL</label>
177<div className="space-y-2">
178<div className="flex space-x-2">
186<button
187type="button"
188onClick={() => onOpenImageSearch(editingItem?.title || '', handleImageSelect)}
189className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
190title="Search for images"
191>
192๐
194</div>
195<div className="text-xs text-gray-400 text-center">
196๐ Paste an image from clipboard (Ctrl/Cmd+V) or search above
197</div>
198{editForm.cover_url && (
203className="w-12 h-16 object-cover rounded border border-gray-300"
204onError={(e) => {
205const target = e.target as HTMLImageElement;
206target.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA0OCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjY0IiBmaWxsPSIjRjNGNEY2Ii8+Cjx0ZXh0IHg9IjI0IiB5PSIzMiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiM5Q0EzQUYiIHRleHQtYW5jaG9yPSJtaWRkbGUiPkVycm9yPC90ZXh0Pgo8L3N2Zz4=';
207}}
208/>
301<div className="flex items-start justify-between">
302<div className="flex space-x-3 flex-1">
303{/* Cover Image or Type Icon */}
304<div className="flex-shrink-0">
305{item.cover_url ? (
309className="w-12 h-16 object-cover rounded border border-gray-300"
310onError={(e) => {
311const target = e.target as HTMLImageElement;
312target.style.display = 'none';
313const fallback = target.parentElement?.querySelector('.fallback-icon') as HTMLElement;
pathfinder__5712n7-boreMediaGrid.tsx3 matches
60};
61
62// Render media items as image collage with overlap
63const renderMediaCollage = (items: MediaItem[]) => {
64if (items.length === 0) {
89className="w-full h-full object-cover rounded"
90onError={(e) => {
91// Fallback to type icon if image fails to load
92const target = e.target as HTMLImageElement;
93target.style.display = 'none';
94const fallback = target.nextElementSibling as HTMLElement;
13
14<!-- Favicon and Icons -->
15<link rel="icon" type="image/svg+xml" href="/frontend/favicon.svg">
16<link rel="apple-touch-icon" href="/frontend/favicon.svg">
17
pathfinder__5712n7-boremanifest.json2 matches
13"src": "/frontend/favicon.svg",
14"sizes": "any",
15"type": "image/svg+xml",
16"purpose": "any maskable"
17}
22"src": "/frontend/screenshot-mobile.jpg",
23"sizes": "390x844",
24"type": "image/jpeg",
25"form_factor": "narrow",
26"label": "Task and habit tracking on mobile"
mcp-servermain.ts4 matches
18date: string;
19doc: string;
20image: string;
21tags: string;
22title: string;
66sequences: item.sequences,
67date: item.date,
68image: item.image,
69backlinks: item.backlinks,
70category: extractPostCategory(item),
89(page.sequences?.length > 0 ? `- sequences: ${page.sequences.map(seq => `[Sequence ${seq.id} on ${seq.topic}](${SITE_URL}/sequences#${seq.id})`).join(", ")}` : null),
90(page.book ? `- book_id: ${page.book}` : null),
91(page.image ? `- image: ${page.image}` : null),
92(page.score ? `- relevance: ${page.score.toFixed(3)}` : null),
93].filter((a) => a !== null).join("\n");
299- sequences: a comma-separated set of markdown-formatted (title and URL) sequences that the post is part of, if any.
300- backklinks: a comma-separated set of post URLs that link to this post, if any.
301- image: the URL of the feature image associated with the post, if any.
302- relevance: a score indicating how relevant the post is to the search query, defaults to 1 if not present.
303- category: available categories are: blog, notes, exercise, replies, and page.`,
reactHonoStarterindex.html1 match
6<title>React Hono Val Town Starter</title>
7<link rel="stylesheet" href="/frontend/style.css">
8<link rel="icon" href="/frontend/favicon.svg" type="image/svg+xml">
9</head>
10<body>
sqliteExplorerAppREADME.md1 match
3View and interact with your Val Town SQLite data. It's based off Steve's excellent [SQLite Admin](https://www.val.town/v/stevekrouse/sqlite_admin?v=46) val, adding the ability to run SQLite queries directly in the interface. This new version has a revised UI and that's heavily inspired by [LibSQL Studio](https://github.com/invisal/libsql-studio) by [invisal](https://github.com/invisal). This is now more an SPA, with tables, queries and results showing up on the same page.
45
67## Install
blob_adminREADME.md1 match
3This is a lightweight Blob Admin interface to view and debug your Blob data.
45
67To use this, fork it to your account.