33import { useState , useTransition } from "react" ;
44import { Loader2 , Link2 , Plus } from "lucide-react" ;
55import { fetchOG } from "@/lib/api-client" ;
6+ import { parseShare } from "@/lib/parse-share" ;
67import type { Product , Retailer } from "@/lib/types" ;
78import { RetailerIcon , retailerLabel } from "./retailer-icon" ;
89
@@ -15,6 +16,9 @@ interface PasteUrlPreviewProps {
1516// (some retailers do), the fields are blank and the creator fills them in
1617// manually — so adding a product never dead-ends.
1718export function PasteUrlPreview ( { onResolved } : PasteUrlPreviewProps ) {
19+ // What the creator pasted (kept intact so large "share text" stays visible).
20+ const [ rawInput , setRawInput ] = useState ( "" ) ;
21+ // The clean product URL extracted from it (editable in the resolved form).
1822 const [ url , setUrl ] = useState ( "" ) ;
1923 const [ pending , startTransition ] = useTransition ( ) ;
2024 const [ showForm , setShowForm ] = useState ( false ) ;
@@ -31,6 +35,7 @@ export function PasteUrlPreview({ onResolved }: PasteUrlPreviewProps) {
3135 const [ canonicalUrl , setCanonicalUrl ] = useState ( "" ) ;
3236
3337 const reset = ( ) => {
38+ setRawInput ( "" ) ;
3439 setUrl ( "" ) ;
3540 setShowForm ( false ) ;
3641 setAutofilled ( false ) ;
@@ -43,41 +48,37 @@ export function PasteUrlPreview({ onResolved }: PasteUrlPreviewProps) {
4348 } ;
4449
4550 const handlePaste = ( raw : string ) => {
46- setUrl ( raw ) ;
47- // Accept full retailer "share text", not just a bare URL. Apps share
48- // things like "Check out this product I found on Nykaa: <name> <url>" —
49- // pull the URL out of anywhere in the text.
50- const urlMatch = raw . match ( / h t t p s ? : \/ \/ [ ^ \s ] + / ) ;
51- if ( ! urlMatch ) {
51+ setRawInput ( raw ) ;
52+ // Accept full retailer "share text", not just a bare URL — parseShare pulls
53+ // out a clean http(s) link (upgrading scheme-less ones, trimming trailing
54+ // punctuation) plus a best-effort title and price.
55+ const parsed = parseShare ( { text : raw , url : raw } ) ;
56+ if ( ! parsed . productUrl ) {
57+ setUrl ( "" ) ;
5258 setShowForm ( false ) ;
5359 return ;
5460 }
55- const targetUrl = urlMatch [ 0 ] ;
56- // The text around the URL is usually the product name. Strip a
57- // "Check out … on <retailer>:" style lead-in if present.
58- let shareTitle = raw . replace ( targetUrl , "" ) . trim ( ) ;
59- const seg = shareTitle . match ( / ^ ( .* ?: ) \s * ( [ \s \S ] + ) $ / ) ;
60- if ( seg && / c h e c k o u t | f o u n d o n | s h a r e d | r e c o m m e n d | b u y t h i s | l o v i n g / i. test ( seg [ 1 ] ) ) {
61- shareTitle = seg [ 2 ] . trim ( ) ;
62- }
63- setUrl ( targetUrl ) ; // normalize the field to just the URL
61+ setUrl ( parsed . productUrl ) ;
6462
6563 startTransition ( async ( ) => {
6664 try {
67- const og = await fetchOG ( targetUrl ) ;
65+ const og = await fetchOG ( parsed . productUrl ) ;
6866 setRetailer ( og . retailer ) ;
6967 setCanonicalUrl ( og . canonicalUrl ?? "" ) ;
7068 // Prefer the clean title from the share text; fall back to OG. This
7169 // gives a usable title even when the server fetch is blocked.
72- setTitle ( shareTitle || og . title || "" ) ;
70+ setTitle ( parsed . title || og . title || "" ) ;
7371 if ( og . ok ) {
7472 setImageUrl ( og . imageUrl ?? "" ) ;
75- setPriceText ( og . priceText ?? "" ) ;
73+ setPriceText ( og . priceText || parsed . priceText || "" ) ;
74+ } else {
75+ setPriceText ( parsed . priceText || "" ) ;
7676 }
77- setAutofilled ( Boolean ( shareTitle || ( og . ok && ( og . title || og . imageUrl ) ) ) ) ;
77+ setAutofilled ( Boolean ( parsed . title || ( og . ok && ( og . title || og . imageUrl ) ) ) ) ;
7878 } catch {
79- setTitle ( shareTitle ) ;
80- setAutofilled ( Boolean ( shareTitle ) ) ;
79+ setTitle ( parsed . title ) ;
80+ setPriceText ( parsed . priceText || "" ) ;
81+ setAutofilled ( Boolean ( parsed . title ) ) ;
8182 } finally {
8283 setShowForm ( true ) ;
8384 }
@@ -103,15 +104,20 @@ export function PasteUrlPreview({ onResolved }: PasteUrlPreviewProps) {
103104 < label className = "block" >
104105 < span className = "block text-sm font-medium mb-2" > Paste a product link — or the whole “share” text from a shopping app</ span >
105106 < div className = "relative" >
106- < Link2 size = { 16 } className = "absolute left-3 top-1/2 -translate-y-1/2 text-muted" aria-hidden />
107- < input
108- type = "url"
109- value = { url }
107+ < Link2 size = { 16 } className = "absolute left-3 top-4 text-muted" aria-hidden />
108+ < textarea
109+ rows = { 2 }
110+ value = { rawInput }
110111 onChange = { ( e ) => handlePaste ( e . target . value ) }
111- placeholder = "https://www.myntra.com/…"
112- className = "w-full rounded-lg border border-rule bg-cream py-4 pl-10 pr-4 text-lg focus:outline-none focus:ring-2 focus:ring-accent focus:border-accent"
112+ placeholder = "Paste here — e.g. “Check out this product… https://www.myntra.com/…” "
113+ className = "w-full resize-y rounded-lg border border-rule bg-cream py-3 pl-10 pr-4 text-base leading-relaxed focus:outline-none focus:ring-2 focus:ring-accent focus:border-accent"
113114 />
114115 </ div >
116+ { url && (
117+ < p className = "mt-1 text-xs text-muted truncate" >
118+ Link detected: < span className = "text-ink" > { url } </ span >
119+ </ p >
120+ ) }
115121 </ label >
116122
117123 { pending && (
@@ -172,6 +178,17 @@ export function PasteUrlPreview({ onResolved }: PasteUrlPreviewProps) {
172178 </ div >
173179 </ div >
174180
181+ < input
182+ type = "url"
183+ value = { url }
184+ onChange = { ( e ) => {
185+ setUrl ( e . target . value ) ;
186+ setCanonicalUrl ( "" ) ; // a manual edit wins over the resolved link
187+ } }
188+ placeholder = "Product link"
189+ className = "w-full rounded-md border border-rule bg-cream px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-accent"
190+ />
191+
175192 < input
176193 type = "text"
177194 value = { note }
0 commit comments