blog2025-06-03-newsletter-25.md2 matches
79We rebuilt [Val Town Search](https://www.val.town/x/maxm/vtProjectSearch) for the multi-file vals. It's much faster and gives exact matches. (It runs on [Val Town SQLite](https://docs.val.town/std/sqlite/), and uses `like`.) It searches across public val source code, usernames, and our docs.
8081<img src="https://imagedelivery.net/iHX6Ovru0O7AjmyT5yZRoA/b29b046d-8662-48de-5d8f-a02f2795d700/public" style="max-width:80%; border: 1.5px solid #cfcfcf; padding-top:10px; padding-bottom: 20px; padding-left: 10px; padding-right: 10px; border-radius: 3px; margin-bottom:20px; display:block; margin-left: auto; margin-right: auto" />
8283Search was built [100% open-source & in userspace](https://www.val.town/x/maxm/vtProjectSearch). With one-click, you can remix it to your account: it'll start reading from the public val firehose API, and index all public val code into your [Val Town SQLite](https://docs.val.town/std/sqlite/). If you want to make an improvement to search, or build something on top of that index, please do! The power of userspace 💪
154- [**cmknz**](https://www.val.town/u/cmknz) made [maine-bills-tax](https://www.val.town/x/cmknz/maine-bills-tax), which summarizes all of the current pending bills that could impact taxes in the State of Maine.
155- [**kamalnrf**](https://www.val.town/u/kamalnrf) made [MailGoat](https://www.val.town/x/kamalnrf/MailGoat), which automatically organizes your emails with custom labels.
156- [**Wolf**](https://www.val.town/u/wolf) made [instagramScraping](https://www.val.town/x/wolf/instagramScraping), which extracts image and caption data from Instagram posts.
157- [**colingourlay**](https://www.val.town/u/colingourlay) made [supernote-crosswords](https://www.val.town/x/colingourlay/supernote-crosswords), which uploads daily crosswords to your Supernote.
158- [**humbl**](https://www.val.town/u/humbl) made [figma-variables-manager](https://www.val.town/x/humbl/figma-variables-manager), a Figma plugin that syncs variables from external sources like Notion and Coda.
blog2025-04-08-migration.md2 matches
6---
78
910We migrated our blog to Val Town 🥳
79## A fast migration
8081How did we migrate 65 blog posts – with images, videos, and intricate formatting and styles – to an entirely new blog in a single day?
8283We didn't. We left them where they are, and proxy to them.
3This val sends weekly messages about the change in user count in Clerk.
45
67## How it works
plugineloopBackendusers.ts3 matches
86if (data.avatar !== undefined) {
87// Handle avatar upload
88if (data.avatar.startsWith("data:image")) {
89// Extract base64 data
90const base64Data = data.avatar.split(",")[1];
91const imageData = atob(base64Data);
92
93// Save to blob storage
94const avatarKey = `avatar_${user.id}_${Date.now()}.png`;
95await blob.set(avatarKey, imageData);
96
97const avatarUrl = `https://toowired.val.run/blob/${avatarKey}`;
7your workspace.
89
1011Read the full guide here: https://docs.val.town/integrations/slack/bot/
fib-bonusindex-running.html1 match
82}
83.grid-pattern {
84background-image: linear-gradient(
85rgba(0, 0, 0, 0.03) 1px,
86transparent 1px
fib-bonusindex.html1 match
82}
83.grid-pattern {
84background-image: linear-gradient(
85rgba(0, 0, 0, 0.03) 1px,
86transparent 1px
16loginApiUrl: "https://example.com/api/login", // 登录接口的 URL
17checkinApiUrl: "https://example.com/api/checkin", // 打卡接口的 URL
18captchaImageUrl: "https://example.com/captcha.jpg", // 验证码图片的 URL (如果存在)
19checkinSuccessText: "打卡成功", // 打卡成功的标志性文本
20};
53*/
54async function solveCaptcha() {
55if (!config.captchaImageUrl) {
56console.log("未配置验证码图片 URL, 跳过验证码识别.");
57return null;
61const solver = new TwoCaptcha.Solver(Deno.env.get("TWOCAPTCHA_API_KEY"));
62console.log("正在识别验证码...");
63const res = await solver.imageCaptcha({
64body: await (await fetch(config.captchaImageUrl)).arrayBuffer(),
65});
66console.log(`验证码识别结果: ${res.data}`);
102#god-ray-container { background: radial-gradient(circle, rgba(20,20,35,0.8) 0%, rgba(10,10,20,0.95) 60%); }
103#god-ray-animator { position: relative; width: 1px; height: 1px; animation: rotate-container 20s linear infinite; }
104.god-ray { position: absolute; top: 0; left: 0; width: 4px; height: 150vmax; transform-origin: top center; background-image: linear-gradient(to bottom, rgba(255,255,255,0.8), rgba(255,255,255,0)); animation: ray-pulse 4s ease-in-out infinite alternate; }
105@keyframes rotate-container { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
106@keyframes ray-pulse {
TownieInputBox.tsx47 matches
3import { Link } from "react-router";
4import { PlusIcon, ArrowUpIcon, Square, XIcon } from "./icons.tsx";
5import { processFiles } from "../utils/images.ts";
67export function InputBox ({
12running,
13error,
14images,
15setImages,
16} : {
17value: string;
21running: boolean;
22error: any;
23images: (string|null)[];
24setImages: (images: (string|null)[]) => void;
25}) {
26const form = useRef(null);
62/>
63</div>
64<ImageRow images={images} setImages={setImages} />
65<div className="toolbar">
66<UploadButton
67disabled={running}
68images={images}
69setImages={setImages}
70/>
71<div className="spacer" />
94}
9596export function ImageDropContainer ({
97images,
98setImages,
99running,
100children,
101}: {
102images: (string|null)[];
103setImages: (images: (string|null)[]) => void;
104running: boolean;
105children: React.ReactNode;
106}) {
107const dragging = useImageDrop({ images, setImages, running });
108109return (
111{children}
112{dragging && (
113<div className="image-drop-overlay">
114<div className="image-drop-inner">
115Drop images here to upload
116</div>
117</div>
121}
122123export function useImageDrop ({ images, setImages, running }: {
124images: (string|null)[];
125setImages(images: (string|null)[]) => void;
126running: boolean;
127}) {
149setDragging(false);
150if (e.dataTransfer?.files && !running) {
151processFiles(Array.from(e.dataTransfer.files), images, setImages);
152}
153}
165document.removeEventListener("drop", onDrop);
166}
167}, [images, setImages, running]);
168169return dragging;
170}
171172function ImageRow ({ images, setImages }: {
173images: (string|null)[];
174setImages: (images: (string|null)[]) => void;
175}) {
176return (
177<div className="image-row">
178{images.map((image, i) => (
179<Thumbnail
180key={i}
181image={image}
182onRemove={() => {
183setImages([
184...images.slice(0, i),
185...images.slice(i + 1),
186]);
187}}
192}
193194function Thumbnail ({ image, onRemove }: {
195image: string|null;
196onRemove: () => void;
197}) {
198if (!image) return null;
199200return (
201<div className="input-image">
202<img
203src={image}
204alt="User uploaded image"
205className="image-thumbnail"
206/>
207<button
208type="button"
209title="Remove image"
210className="remove-image-button"
211onClick={onRemove}
212>
218219function UploadButton ({
220images,
221setImages,
222disabled,
223}: {
224images: (string|null)[];
225setImages: (images: (string|null)[]) => void;
226disabled: boolean;
227}) {
232<button
233type="button"
234title="Upload image"
235disabled={disabled}
236onClick={e => {
240<PlusIcon />
241<div className="sr-only">
242Upload image
243</div>
244</button>
249onChange={e => {
250if (e.target.files) {
251processFiles(Array.from(e.target.files), images, setImages);
252}
253}}