OpenTowniestyles.css11 matches
682background-color: var(--highlight);
683}
684.card-image {
685display: flex;
686align-items: center;
704}
705706.image-placeholder,
707.image-thumbnail {
708width: 40px;
709height: 40px;
711object-fit: cover;
712}
713.image-placeholder {
714background-color: var(--muted);
715}
722}
723724.image-row {
725display: flex;
726gap: var(--space-1);
727}
728.input-image {
729position: relative;
730border: 1px solid var(--muted);
731border-radius: 6px;
732}
733.remove-image-button {
734position: absolute;
735top: 0;
744opacity: 0;
745}
746.input-image:hover .remove-image-button {
747opacity: 1;
748}
749750.image-drop-overlay {
751position: fixed;
752top: 0;
761justify-content: center;
762}
763.image-drop-inner {
764padding: var(--space-2);
765background-color: var(--background);
766}
767768.transition, .input-box, .icon-button, .button, .remove-image-button {
769transition-property: color, background-color, border-color, opacity;
770transition-duration: 200ms;
OpenTownieProjectsRoute.tsx7 matches
48user: {
49username: string;
50profileImageUrl: string|null;
51};
52project: any;
55<div className="card">
5657{project.imageUrl ? (
58<img src={project.imageUrl} className="card-image" />
59) : user.profileImageUrl ? (
60<div className="card-image">
61<img
62src={user.profileImageUrl}
63width="48"
64height="48"
67</div>
68) : (
69<div className="card-image placeholder" />
70)}
71<div className="card-body">
OpenTownieInputBox.tsx46 matches
2import { useRef, useState, useEffect } from "https://esm.sh/react@18.2.0?dev";
3import { PlusIcon, ArrowUpIcon, Square, XIcon } from "./icons.tsx";
4import { processFiles } from "../utils/images.ts";
56export function InputBox ({
11running,
12error,
13images,
14setImages,
15} : {
16value: string;
20running: boolean;
21error: any;
22images: (string|null)[];
23setImages: (images: (string|null)[]) => void;
24}) {
25const form = useRef(null);
57autoFocus={true}
58/>
59<ImageRow images={images} setImages={setImages} />
60<div className="toolbar">
61<UploadButton
62disabled={running}
63images={images}
64setImages={setImages}
65/>
66<div className="spacer" />
88}
8990export function ImageDropContainer ({
91images,
92setImages,
93running,
94children,
95}: {
96images: (string|null)[];
97setImages: (images: (string|null)[]) => void;
98running: boolean;
99children: React.ReactNode;
100}) {
101const dragging = useImageDrop({ images, setImages, running });
102103return (
105{children}
106{dragging && (
107<div className="image-drop-overlay">
108<div className="image-drop-inner">
109Drop images here to upload
110</div>
111</div>
115}
116117export function useImageDrop ({ images, setImages, running }: {
118images: (string|null)[];
119setImages(images: (string|null)[]) => void;
120running: boolean;
121}) {
143setDragging(false);
144if (e.dataTransfer?.files && !running) {
145processFiles(Array.from(e.dataTransfer.files), images, setImages);
146}
147}
164}
165166function ImageRow ({ images, setImages }: {
167images: (string|null)[];
168setImages: (images: (string|null)[]) => void;
169}) {
170return (
171<div className="image-row">
172{images.map((image, i) => (
173<Thumbnail
174key={i}
175image={image}
176onRemove={() => {
177setImages([
178...images.slice(0, i),
179...images.slice(i + 1),
180]);
181}}
186}
187188function Thumbnail ({ image, onRemove }: {
189image: string|null;
190onRemove: () => void;
191}) {
192if (!image) return null;
193194return (
195<div className="input-image">
196<img
197src={image}
198alt="User uploaded image"
199className="image-thumbnail"
200/>
201<button
202type="button"
203title="Remove image"
204className="remove-image-button"
205onClick={onRemove}
206>
212213function UploadButton ({
214images,
215setImages,
216disabled,
217}: {
218images: (string|null)[];
219setImages: (images: (string|null)[]) => void;
220disabled: boolean;
221}) {
226<button
227type="button"
228title="Upload image"
229disabled={disabled}
230onClick={e => {
234<PlusIcon />
235<div className="sr-only">
236Upload image
237</div>
238</button>
243onChange={e => {
244if (e.target.files) {
245processFiles(Array.from(e.target.files), images, setImages);
246}
247}}
OpenTownieimages.ts12 matches
12export const PROMPT_IMAGE_LIMIT = 5;
34export const processFiles = async (files: File[], images: (string | null)[], setImages: (images: (string | null)[]) => void) => {
5const imageFiles = files.filter(file => file.type.startsWith('image/'));
6const filesToProcess = imageFiles.slice(0, PROMPT_IMAGE_LIMIT - images.filter(Boolean).length);
78if (filesToProcess.length === 0) return;
910const newImages = [...images, ...Array(filesToProcess.length).fill(null)];
11setImages(newImages);
1213const processedImages = await Promise.all(
14filesToProcess.map(async (file) => {
15return await readFileAsDataURL(file);
17);
1819const updatedImages = [...images];
20processedImages.forEach((dataUrl, index) => {
21updatedImages[images.length + index] = dataUrl;
22});
2324setImages(updatedImages.slice(0, PROMPT_IMAGE_LIMIT));
25};
2630reader.onload = () => {
31const result = reader.result as string;
32console.log("Image loaded, size:", result.length, "bytes");
33resolve(result);
34};
OpenTowniefavicon.http.tsx1 match
13return new Response(svg, {
14headers: {
15"Content-Type": "image/svg+xml",
16},
17});
OpenTownieChatRoute.tsx13 matches
10import { useUsageStats } from "../hooks/useUsageStats.ts";
11import { Messages } from "./Messages.tsx";
12import { InputBox, ImageDropContainer } from "./InputBox.tsx";
13import { PreviewFrame } from "./PreviewFrame.tsx";
14import { BranchSelect } from "./BranchSelect.tsx";
64}) {
65const { token, anthropicApiKey } = useAuth();
66const [images, setImages] = useState<(string|null)[]>([]);
67const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
68const { audio } = useContext(AppContext);
84bearerToken: token,
85selectedFiles,
86images,
87soundEnabled: audio,
88});
108109return (
110<ImageDropContainer
111running={running}
112images={images}
113setImages={setImages}>
114<div
115className="chat-container container">
131onSubmit={e => {
132handleSubmit(e);
133setImages([]);
134}}
135onCancel={handleStop}
136running={running}
137error={error}
138images={images}
139setImages={setImages}
140/>
141</div>
149rel="norefferer"
150className="block-link text-link lockup">
151{project.imageUrl ? (
152<img src={project.imageUrl} className="image-thumbnail" />
153) : (
154<div className="image-placeholder" />
155)}
156<div>
173</div>
174<pre hidden>{JSON.stringify(messages, null, 2)}</pre>
175</ImageDropContainer>
176);
177}
2title: "Knee Slide Cursor"
3description: "A fun, interactive soccer-themed cursor effect that reacts to your mouse movements! Featuring a smooth knee-slide animation and goal celebration messages."
4imageUrl: "https://chatgpt.com/backend-api/public_content/enc/eyJpZCI6Im1fNjgwZDhkYjBiNzdjODE5MTkxOTM3Y2ZlODk3NThjM2E6ZmlsZV8wMDAwMDAwMGY4NDg2MjMwOTU5NDRkOTE0ZTFhMTk3ZCIsInRzIjoiNDg0OTIxIiwicCI6InB5aSIsInNpZyI6ImE2MTRjMjZlMTA1ODM0ZmFkZGQzZDU4YTcyMDdjZmYzMzAyZGEzMGNhZWI0ZGEyMjI2YWM5ZGFiNDM4MTdlNjIiLCJ2IjoiMCIsImdpem1vX2lkIjpudWxsfQ=="
5author: "dcm31"
6tags: ["javascript","react","cursor-effects","val-town","soccer","interactive","tes"]
2title: "usmnt_world_cup_roster_tracker"
3description: "US Men's 2026 World Cup roster probability tracker"
4imageUrl: "https://chatgpt.com/backend-api/public_content/enc/eyJpZCI6Im1fNjgxMzk5N2RmMWUwODE5MWE0NGY2ZmE1MTIxOGYyODU6ZmlsZV8wMDAwMDAwMGM4YmM2MWY3YWM2MTgzYzIzMmFjMDZhZiIsInRzIjoiNDg1MDMxIiwicCI6InB5aSIsInNpZyI6IjVhMGZmNzc1ZDYxNTA3MjQ2NzAyYTk4MjAwYmI0YjY4NTA1OTcwOTA0YjExODBhYzNkZjA2ZTdlZjg0YmMwYzgiLCJ2IjoiMCIsImdpem1vX2lkIjpudWxsfQ=="
5author: "dcm31"
6tags: ["val-town"]
obedientTurquoiseGoldfishREADME.md38 matches
56}
5758#image-upload-section {
59background-color: rgba(255, 255, 255, 0.8);
60padding: 20px;
68}
6970#image-upload-section h2 {
71margin-bottom: 15px;
72color: #557492;
73}
7475#image-upload-section p {
76margin-bottom: 15px;
77color: #666;
78}
7980#image-upload {
81opacity: 0;
82position: absolute;
120}
121122#image-container {
123display: none;
124background-color: rgba(255, 255, 255, 0.8);
133}
134135#uploaded-image {
136max-width: 100%;
137max-height: 500px;
177}
178179#cropped-image-container {
180display: none;
181background-color: rgba(255, 255, 255, 0.8);
190}
191192#cropped-image {
193max-width: 100%;
194border-radius: 10px;
215216@media (max-width: 768px) {
217#image-upload-section {
218padding: 15px;
219}
248249@media (max-width: 480px) {
250#image-upload-section {
251padding: 10px;
252}
282</header>
283<main>
284<section id="image-upload-section">
285<h2>Upload Your Image</h2>
286<p>Select an image to crop</p>
287<div class="file-upload-wrapper">
288<span class="file-upload-text">Choose File</span>
289<span class="file-upload-button">Browse</span>
290<input type="file" id="image-upload" accept="image/*">
291</div>
292</section>
293<section id="image-container">
294<img id="uploaded-image" src="#" alt="Uploaded Image">
295<button id="crop-button">Crop Image</button>
296<button id="download-button">Download Cropped Image</button>
297</section>
298<section id="cropped-image-container">
299<h2>Cropped Image</h2>
300<img id="cropped-image" src="#" alt="Cropped Image">
301</section>
302</main>
307308<script>
309const imageUpload = document.getElementById('image-upload');
310const imageContainer = document.getElementById('image-container');
311const uploadedImage = document.getElementById('uploaded-image');
312const cropButton = document.getElementById('crop-button');
313const croppedImageContainer = document.getElementById('cropped-image-container');
314const croppedImage = document.getElementById('cropped-image');
315const downloadButton = document.getElementById('download-button');
316317let cropper;
318319imageUpload.addEventListener('change', (event) => {
320const file = event.target.files[0];
321324325reader.onload = (e) => {
326uploadedImage.src = e.target.result;
327imageContainer.style.display = 'block';
328croppedImageContainer.style.display = 'none';
329downloadButton.style.display = 'none';
330333}
334335cropper = new Cropper(uploadedImage, {
336aspectRatio: 1,
337viewMode: 1,
350if (cropper) {
351const canvas = cropper.getCroppedCanvas();
352croppedImage.src = canvas.toDataURL();
353croppedImageContainer.style.display = 'block';
354downloadButton.style.display = 'inline-block';
355}
359const canvas = document.createElement('canvas');
360const ctx = canvas.getContext('2d');
361const img = new Image();
362363img.onload = () => {
364canvas.width = img.width;
365canvas.height = img.height;
366ctx.drawImage(img, 0, 0, img.width, img.height);
367368const link = document.createElement('a');
369link.href = canvas.toDataURL('image/png');
370link.download = 'cropped_image.png';
371document.body.appendChild(link);
372link.click();
374};
375376img.src = croppedImage.src;
377});
378</script>
74- 'name': The race name.
75- 'description': 1-2 sentences max.
76- 'styleHint': 3-5 descriptive keywords for image generation.
77- 'effectColor': A hex color code (#RRGGBB format) associated with the race.
78- 'borderAnimationHint': OPTIONAL. A single keyword suggesting a subtle border animation style for the active card. Choose from 'shimmer', 'pulse', or 'none'. If unsure, use 'none'.
273/* --- Animated Border Styles --- */
274@keyframes border-shimmer { /* Gradient sweep */
2750% { border-image-source: linear-gradient(90deg, transparent 0%, var(--effect-color) 50%, transparent 100%); }
27625% { border-image-source: linear-gradient(90deg, transparent 25%, var(--effect-color) 75%, transparent 100%); }
27750% { border-image-source: linear-gradient(90deg, transparent 50%, var(--effect-color) 100%); }
27875% { border-image-source: linear-gradient(90deg, var(--effect-color) 0%, transparent 25%); }
279100% { border-image-source: linear-gradient(90deg, var(--effect-color) 50%, transparent 100%); }
280}
281@keyframes border-pulse { /* Simple fade in/out */
288border-width: 2px;
289border-style: solid;
290border-image-slice: 1;
291border-image-source: linear-gradient(90deg, transparent 0%, var(--effect-color) 50%, transparent 100%); /* Initial state */
292animation: border-shimmer var(--border-anim-duration) linear infinite;
293}
298299300/* --- Other Styles (Images, Content, Effects - mostly unchanged) --- */
301.race-card img { pointer-events: none; height: 60%; border-radius: 16px 16px 0 0; width: 100%; object-fit: cover; display: block; background-color: #333;}
302.race-card .card-content { pointer-events: none; padding: 20px; display: flex; flex-direction: column; gap: 12px; flex-grow: 1; position: relative; z-index: 2; background: inherit; border-radius: 0 0 16px 16px;} /* Ensure content bg matches card */
448449const prompt = encodeURIComponent(\`disney fantasy character portrait, \${safeStyleHint}, \${safeName}, detailed illustration, cinematic lighting, high fantasy art style\`);
450const imageUrl = \`http://maxm-imggenurl.web.val.run//\${prompt}.jpg\`; // Using Pollinations.ai as example
451const fallbackColor = safeEffectColor.substring(1);
452const fallbackUrl = \`https://placehold.co/400x400/\${fallbackColor}/1A1A1E?text=\${encodeURIComponent(safeName)}\`;
453454card.innerHTML = \`
455<img src="\${imageUrl}" alt="Image of \${safeName}" loading="lazy" onerror="this.onerror=null; this.src='\${fallbackUrl}'; console.warn('Image failed for \${safeName}. Using fallback.');">
456<div class="\${EFFECT_CONTAINER_CLASS}"><div class="energy-surge"></div></div>
457<div class="card-content">