From 2cbb0f63e7bb035246ca2bb760113fa6f53d5414 Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Sat, 5 Sep 2026 07:29:20 -0700 Subject: [PATCH] fix(builder): preserve explicit HTTP URLs (#3403) --- .../src/components/input/url-input.test.tsx | 37 +++++++++++++++++++ apps/web/src/components/input/url-input.tsx | 16 ++++---- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/input/url-input.test.tsx b/apps/web/src/components/input/url-input.test.tsx index 8f8a2b26e..664b63cf1 100644 --- a/apps/web/src/components/input/url-input.test.tsx +++ b/apps/web/src/components/input/url-input.test.tsx @@ -56,6 +56,43 @@ describe("URLInput", () => { }); }); + it.each(["http://other.example/path", "HTTP://other.example/path"])( + "preserves an explicitly pasted HTTP URL: %s", + (url) => { + const onChange = vi.fn(); + renderInput({ url: "https://example.com", label: "Company" }, onChange); + + fireEvent.change(screen.getByRole("textbox"), { target: { value: url } }); + + expect(onChange).toHaveBeenCalledWith({ url, label: "Company" }); + }, + ); + + it("shows an existing HTTP URL with its matching prefix", () => { + renderInput({ url: "http://example.com/path", label: "" }); + + expect((screen.getByRole("textbox") as HTMLInputElement).value).toBe("example.com/path"); + expect(screen.getByText("http://")).toBeDefined(); + }); + + it("preserves HTTP while editing the host or path", () => { + const onChange = vi.fn(); + renderInput({ url: "http://example.com/path", label: "Company" }, onChange); + + fireEvent.change(screen.getByRole("textbox"), { target: { value: "example.com/new-path" } }); + + expect(onChange).toHaveBeenCalledWith({ url: "http://example.com/new-path", label: "Company" }); + }); + + it("allows switching an existing HTTP URL to HTTPS by pasting", () => { + const onChange = vi.fn(); + renderInput({ url: "http://example.com", label: "" }, onChange); + + fireEvent.change(screen.getByRole("textbox"), { target: { value: "https://example.com" } }); + + expect(onChange).toHaveBeenCalledWith({ url: "https://example.com", label: "" }); + }); + it("emits an empty url string when cleared", () => { const onChange = vi.fn(); renderInput({ url: "https://example.com", label: "" }, onChange); diff --git a/apps/web/src/components/input/url-input.tsx b/apps/web/src/components/input/url-input.tsx index dca2fded1..56d4e7e35 100644 --- a/apps/web/src/components/input/url-input.tsx +++ b/apps/web/src/components/input/url-input.tsx @@ -15,15 +15,16 @@ import { Label } from "@reactive-resume/ui/components/label"; import { Popover, PopoverContent, PopoverTrigger } from "@reactive-resume/ui/components/popover"; import { cn } from "@reactive-resume/utils/style"; -const PREFIX = "https://"; +const DEFAULT_PREFIX = "https://"; +const HTTP_PREFIX = /^https?:\/\//i; function stripPrefix(url: string) { - return url.startsWith(PREFIX) ? url.slice(PREFIX.length) : url; + return url.replace(HTTP_PREFIX, ""); } -function ensurePrefix(url: string) { +function ensurePrefix(url: string, prefix: string) { if (url === "") return ""; - return url.startsWith(PREFIX) ? url : PREFIX + url; + return HTTP_PREFIX.test(url) ? url : prefix + url; } type Props = Omit, "value" | "onChange"> & { @@ -33,14 +34,15 @@ type Props = Omit({ value, onChange, hideLabelButton, ...props }: Props) { + const prefix = value.url.match(HTTP_PREFIX)?.[0] ?? DEFAULT_PREFIX; const handleUrlChange = useCallback( (e: React.ChangeEvent) => { onChange({ ...value, - url: ensurePrefix(e.target.value), + url: ensurePrefix(e.target.value, prefix), }); }, - [onChange, value], + [onChange, value, prefix], ); const handleLabelChange = useCallback( @@ -55,7 +57,7 @@ export function URLInput({ value, onChange, hideLabelBut return ( - {PREFIX} + {prefix}