`);w.document.close();toast('Local label summary created')} function shippingAction(txId,action){let t=txForConversation(txId),l=t&&conversationListing(txId);if(!t||!l)return;let now=new Date().toISOString();t.shipping={...(t.shipping||{})};if(action==='shipped'){t.shipping.shipped_at=now;t.shipping.status='shipped';t.timeline=[...(t.timeline||[]),{event:'Shipment confirmation',at:now,source:'user-recorded',tracking:t.shipping.tracking||''}];toast('Shipment marked shipped')}else{t.shipping.delivered_at=now;t.shipping.status='delivered';t.timeline=[...(t.timeline||[]),{event:'Delivery',at:now,source:'user-recorded'}];toast('Shipment marked delivered')}save('fs-transactions',transactions);render()} function handshake(l,event,extra={}){let item={event,at:new Date().toISOString(),...extra};l.handshake_timeline=[...(l.handshake_timeline||[]),item];return item} function transactionAction(id,action){let l=listings.find(x=>x.id===id);if(!l)return;let now=new Date().toISOString(),tx=listingTransaction(id);if(action==='bin'){if(tx&&tx.status==='pending'){toast('A BIN purchase is already pending');return}let name=prompt('Buyer name:');if(!name?.trim())return;let email=prompt('Buyer email:');if(!email||!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())){toast('Enter a valid buyer email');return}tx={id:'tx-'+Date.now(),listing_id:id,buyer_name:name.trim(),buyer_email:email.trim(),status:'pending',channel:'buy_it_now',created_at:now,updated_at:now,timeline:[]};tx.timeline.push({event:'BIN requested',at:now,buyer_name:tx.buyer_name,buyer_email:tx.buyer_email});transactions.push(tx);l.status='pending';handshake(l,'BIN requested',{buyer_name:tx.buyer_name,buyer_email:tx.buyer_email});toast('BIN purchase recorded — seller confirmation pending')}else{if(!tx)tx={id:'tx-'+Date.now(),listing_id:id,status:'pending',created_at:now,timeline:[]};let labels={confirm:'Sale confirmed',reject:'BIN rejected',dm:'Sold via DM',forum:'Sold on forum'},channels={confirm:'direct',reject:null,dm:'dm',forum:'forum'};tx.status=action==='reject'?'rejected':'confirmed';tx.channel=channels[action];tx.updated_at=now;tx.timeline=[...(tx.timeline||[]),{event:labels[action],at:now,channel:tx.channel}];if(!transactions.includes(tx))transactions.push(tx);l.status=action==='reject'?'available':'sold';handshake(l,labels[action],{channel:tx.channel});toast(labels[action])}save('fs-transactions',transactions);save('fs-listings',listings);render()} function disc(l){return `
${esc(l.model||'ITEM')}
`} function shell(t,s,a=''){return `
ForumSeller community marketplace

Marketplace for Buying and Selling Community Products

${t}

${s}
${a}
`} function card(x){let l=normalize(x),complete=Boolean(l.category&&l.title);return `
${imageMarkup(l)}

${esc(l.category)}${l.brand||l.model?' · '+esc([l.brand,l.model,l.variant].filter(Boolean).join(' · ')):''}

${esc(l.description||'No description')} · ${esc(l.seller)}${l.quantity?' · Qty '+esc(l.quantity):''}

Seller: ${l.seller_id==='you'?'Identity linked':'Community seller'} · Safety: verify details and agree payment and shipping before sending funds.

${badge(l.status)} via ${esc(l.source)}${complete?'':' Details incomplete'}
${l.price?'$'+esc(l.price):'—'}
`} function filters(){return ``} function updateFeed(){let q=(document.getElementById('q')?.value||'').toLowerCase(),a=listings.filter(l=>{let x=normalize(l);return(!q||JSON.stringify(x).toLowerCase().includes(q))&&(!document.getElementById('filter-category')?.value||x.category===document.getElementById('filter-category').value)&&(!document.getElementById('filter-status')?.value||l.status===document.getElementById('filter-status').value)});let c=document.getElementById('count'),f=document.getElementById('feed-list');if(c)c.textContent=`${a.length} listings`;if(f)f.innerHTML=a.length?a.map(card).join(''):'
No matching productsTry widening your filters.
'} function feed(){if(!session.owner)return shell('Marketplace Feed','Find community product listings across Reddit, forums, and Discord in one place.')+'
The feed is owner-only until launch.Enable the owner session under Settings.
';return shell('Marketplace Feed','Find curated community listings across Reddit, forums, and Discord in one place.','')+`
${filters()}
Sort: Newest
`} function imageEditor(){let a=imageList(draft),urls=a.filter(x=>!x.startsWith('data:'));return `
${urls.map((x,i)=>`
`).join('')}
`} function importForm(){let d=normalize(draft);return `
${imageEditor()}
`} function imports(){return shell('Import a Post','Paste a link and the post you already wrote. We parse the product details.','')+`
♢ Imports are user-initiated. ForumSeller never crawls Reddit, forums, or Discord.

1. Add post details

${SOURCES.map(x=>``).join('')}

2. Review listing details

${importForm()}
`} function parsePost(){let t=document.getElementById('postText').value,w=t.match(/\b1[3-8]\d\s?g\b/i),p=t.match(/\$\s?\d+(?:\.\d+)?/),brand=['Innova','Discraft','Discmania','MVP','Apple','Samsung','Sony','Nike'].find(x=>new RegExp('\\b'+x+'\\b','i').test(t))||'',model=['Destroyer','Buzzz','Wave','iPhone','PlayStation','Teebird'].find(x=>new RegExp('\\b'+x+'\\b','i').test(t))||'',category=/\b(destroyer|buzzz|wave|disc|putter|driver)\b/i.test(t)?'Disc golf':'';draft={...draft,post:t,url:document.getElementById('threadUrl').value,category,brand,model,variant:w?`${w[0]}`:'',price:p?p[0].replace('$','').trim():'',title:`${brand} ${model}`.trim(),description:t};render();toast('Parser finished — review details')} function collect(){let urls=[...document.querySelectorAll('.image-url')].map(x=>x.value.trim()).filter(Boolean),files=imageList(draft).filter(x=>x.startsWith('data:'));draft={...draft,images:[...urls,...files],category:document.getElementById('f-category').value,title:document.getElementById('f-title').value.trim(),brand:document.getElementById('f-brand').value.trim(),model:document.getElementById('f-model').value.trim(),variant:document.getElementById('f-variant').value.trim(),condition:document.getElementById('f-condition').value.trim(),price_cents:document.getElementById('f-price').value?Math.round(Number(document.getElementById('f-price').value)*100):null,quantity:Math.max(1,Number(document.getElementById('f-quantity').value||1)),description:document.getElementById('f-description').value.trim(),shipping_estimate:document.getElementById('f-shipping').value.trim(),status:document.getElementById('f-status').value,origin:{platform:selectedSource.toLowerCase(),url:document.getElementById('threadUrl').value.trim()}}} function publish(){collect();if(!draft.category){toast('Choose a category before publishing');return}if(!(session.email||session.reddit?.username)){toast('Add a seller identity in Profile before publishing');return}listings.unshift({id:'fs-'+Date.now(),seller_id:'you',...draft,views:0,clicks_out:0,created_at:new Date().toISOString()});save('fs-listings',listings);draft={};view='listings';render();toast('Listing saved')} function crosspostText(format){let l=listings.find(x=>x.id===document.getElementById('cp-listing')?.value),platform=document.getElementById('cp-platform')?.value||'Reddit',title=document.getElementById('cp-title')?.value.trim()||'',description=document.getElementById('cp-description')?.value.trim()||'',price=document.getElementById('cp-price')?.value.trim()||'',url=document.getElementById('cp-url')?.value.trim()||'',images=[...document.querySelectorAll('#cp-images input:checked')].map(x=>x.value);if(!l)return '';let body=`${title}\n\n${description}\n\nPrice: ${price||'Contact seller'}\nListing URL: ${url||'Not provided'}`+(images.length?`\nImages:\n${images.join('\n')}`:'');if(format==='html')return `

${esc(title)}

${esc(description).replace(/\n/g,'
')}

Price: ${esc(price||'Contact seller')}

Listing URL: ${esc(url||'Not provided')}

${images.map(x=>`Listing image`).join('')}
`;return `${platform} cross-post template\n\n${body}`} function crosspostPanel(){let a=listings.filter(x=>x.seller_id==='you'),l=listings.find(x=>x.id===selectedCrosspost)||a[0],x=l?normalize(l):{},images=l?.images||[];return `

Cross-posting

Export a user-editable template only. ForumSeller never auto-posts or crawls external communities.

${a.length?`
${images.length?images.map((im,i)=>``).join(''):'No user-provided images available.'}

Reddit can use a copied template or an available user-authorized API. Facebook Marketplace generally requires user posting or an approved partner integration; ForumSeller does not fabricate API calls. Verified-seller badge and protected-purchase work remain deferred to Wave 2.

Posted records: ${['Reddit','Facebook Marketplace','Another forum'].map(p=>`${esc(p)}: ${l?.crosspost?.posted?.[p]?new Date(l.crosspost.posted[p]).toLocaleString():'Not recorded'}`).join(' · ')}

`:'
No owned listings yetSave a listing before exporting it.
'}
`} function myListings(){let a=listings.filter(x=>x.seller_id==='you');return shell('My Listings','Your seller dashboard.','')+`
${a.length?`
${a.map(l=>{let x=normalize(l),t=listingTransaction(l.id),s=t?.shipping||{};return ``}).join('')}
ListingCategoryStatusViewsShippingActions
${esc(x.category)}${badge(l.status)}${l.views||0}${t?`Est. ${esc(s.ship_by||'Not set')}
Actual ${esc(s.shipped_at?new Date(s.shipped_at).toISOString().slice(0,10):'Not shipped')}`:'No sale recorded'}
${t?``:''}
`:'
No listings yetImport your first community post.
'}${crosspostPanel()}
`} function detail(l){let x=normalize(l),fields=[['Category',x.category],['Brand',x.brand],['Model',x.model],['Variant',x.variant],['Condition',x.condition],['Quantity',x.quantity],['Price',x.price?'$'+x.price:'Not specified'],['Shipping estimate',x.shipping_estimate||'Not specified'],['Source platform',x.source]];return shell(esc(x.title||[x.brand,x.model].filter(Boolean).join(' ')||'Listing details'),`${esc(x.category)} · imported from ${esc(x.source)}`,'')+`
${imageMarkup(x)}
${badge(x.status)}${x.price?'$'+esc(x.price):'Price not specified'}
${fields.map(f=>`
${esc(f[0])}${esc(f[1]||'Not specified')}
`).join('')}

${esc(x.description||'No description provided.')}

Listing details

Seller: ${esc(x.seller)}

Viewed ${x.views||0} times

${x.url?`

Original thread: ${esc(x.source)} link

`:''}
`} function authProfile(){let signed=session.signed_in,linked=session.reddit?.username;return shell('Seller Profile','Build trust with clear details and recorded handshakes.')+`

Sign in with email

${signed?`

${esc(session.email)}

`:`
`}${authNotice}

Link your Reddit account

Use your Reddit username to identify your seller profile.

${linked?`
Linked as u/${esc(linked)}
Identity scope only. ForumSeller never posts to Reddit for you.
`:`
`}

Seller status: Not verified yet

Verified-seller badge and protected purchase arrive in Wave 2 (deferred).

`} function watchlist(){return shell('Watchlist','Products you are keeping an eye on.')+'
♡Your watchlist is emptySave a product from its listing page when you find one.
'} function fmt(d){return d?new Date(d).toLocaleString([], {dateStyle:'medium',timeStyle:'short'}):''} function txForConversation(id){return transactions.find(x=>x.id===id)||null} function conversationListing(id){let t=txForConversation(id);return t?listings.find(x=>x.id===t.listing_id):listings.find(x=>x.id===id)} function ensureTxForListing(id,name){let t=transactions.find(x=>x.listing_id===id);if(t)return t;let now=new Date().toISOString();t={id:'tx-'+Date.now(),listing_id:id,buyer_name:name||'Buyer',buyer_email:'',status:'pending',channel:'dm',created_at:now,updated_at:now,timeline:[{event:'Conversation opened',at:now,source:'user-recorded'}]};transactions.push(t);save('fs-transactions',transactions);return t} function timelineHtml(t){let names=['BIN requested','Seller response','Payment confirmation','Shipment confirmation','Delivery','Buyer receipt'],events=t?.timeline||[];return `
${names.map(n=>{let e=events.find(x=>x.event===n),extra=e?.source==='user-recorded'?' · user-recorded':'';return `
${e?esc(n):'Awaiting '+esc(n)}${e?' ✓':''}${e?fmt(e.at)+extra:'Not recorded yet'}${e?.note?`
${esc(e.note)}
`:''}${e?.url?``:''}
`}).join('')}
`} function messages(){let convs=transactions.filter(t=>t.channel==='dm'||messagesData.some(m=>m.tx_id===t.id));if(!selectedConversation&&convs[0])selectedConversation=convs[0].id;let t=txForConversation(selectedConversation),l=t?conversationListing(t.id):null;return shell('Messages','User-recorded conversations and handshakes linked to marketplace listings.','')+`
◌ Messages are recorded by you in this browser. ForumSeller never sends, reads, or scrapes Reddit, forum, or Discord messages.
${!convs.length?`
No conversation records yetRecord a buyer or seller conversation to connect messages with a listing.
`:`

Conversations

${convs.map(x=>{let q=conversationListing(x.id),last=messagesData.filter(m=>m.tx_id===x.id).slice(-1)[0];return `

${esc(q?.title||'Listing conversation')}

${esc(x.buyer_name||'Buyer')} · ${esc(x.channel||'direct')}

${last?esc(last.text):'No messages recorded yet'}
${fmt(last?.at||x.created_at)}

`}).join('')}
${l?`

${esc(l.title||'Listing')}

Participant: ${esc(t.buyer_name||'Buyer')} · Seller: ${esc(seller(l.seller_id))}

Listing context: ${esc(l.category||'')} · ${l.price_cents!=null?'$'+(l.price_cents/100).toFixed(2):'Price not specified'} · ${badge(l.status)}

${shippingPanel(l,t)}

Conversation

${messagesData.filter(m=>m.tx_id===t.id).map(m=>`
${esc(m.author_name||m.author)}

${esc(m.text)}

${fmt(m.at)} · user-recorded
`).join('')||'
No messages recorded. Add the first note below.
'}

Handshake timeline

${timelineHtml(t)}

Seller controls

`:'
Select a conversation
'}
`}
`} function settings(){return shell('Settings','Control how your marketplace experience works.')+`

Import preferences

Turn this off to preview the owner-only launch state.


Cross-posting

Cross-posting is always user-initiated. Generate, edit, copy, or download a plain-text/HTML export; ForumSeller never auto-posts or crawls.

Reddit supports a copied template or an available user-authorized API. Facebook Marketplace generally requires user posting or an approved partner integration. No API calls are fabricated here. Verified-seller badge and protected-purchase work remain deferred to Wave 2.


Your imported posts, messages, handshakes, and drafts remain in this browser. ForumSeller never crawls external communities.

`} function render(){document.querySelectorAll('.nav button').forEach(b=>b.classList.toggle('active',b.dataset.view===view));let a=document.getElementById('app');a.innerHTML=view==='feed'?feed():view==='imports'?imports():view==='listings'?myListings():view==='profile'?authProfile():view==='watchlist'?watchlist():view==='messages'?messages():settings();window.scrollTo(0,0);if(view==='feed')setTimeout(updateFeed,0)} function addTimelineEvent(id,event){let t=txForConversation(id);if(!t)return;let note=prompt(`Optional note for ${event}:`);if(note===null)return;let url=prompt('Optional source URL (leave blank if none):')||'';t.timeline=[...(t.timeline||[]),{event,at:new Date().toISOString(),note, url,source:'user-recorded'}];t.updated_at=new Date().toISOString();save('fs-transactions',transactions);render();toast(`${event} recorded`)} document.addEventListener('click',e=>{let b=e.target.closest('button');if(!b)return; if(b.dataset.view){view=b.dataset.view;render()} else if(b.dataset.source){selectedSource=b.dataset.source;render()} else if(b.dataset.parse)parsePost() else if(b.dataset.removeImage){collect();draft.images.splice(Number(b.dataset.removeImage),1);render()} else if(b.dataset.publish)publish() else if(b.dataset.detail){let l=listings.find(x=>x.id===b.dataset.detail);if(l){l.views=(l.views||0)+1;save('fs-listings',listings);document.getElementById('app').innerHTML=detail(l)}} else if('crosspost' in b.dataset){selectedCrosspost=b.dataset.crosspost;render();document.querySelector('.card[style*="margin-top:18px"]')?.scrollIntoView({behavior:'smooth'})} else if('cpGenerate' in b.dataset||'cpFormat' in b.dataset){let format=b.dataset.cpFormat||'text',o=document.getElementById('cp-output');if(o){o.value=crosspostText(format);o.dataset.format=format}} else if('cpCopy' in b.dataset){let o=document.getElementById('cp-output');if(!o?.value){toast('Generate an export first');return}navigator.clipboard?.writeText(o.value).then(()=>toast('Export copied')).catch(()=>{o.select();document.execCommand('copy');toast('Export copied')})} else if('cpDownload' in b.dataset){let o=document.getElementById('cp-output'),format=o?.dataset.format||'text';if(!o?.value){toast('Generate an export first');return}let blob=new Blob([o.value],{type:format==='html'?'text/html':'text/plain'}),a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=`forumseller-export.${format==='html'?'html':'txt'}`;a.click();URL.revokeObjectURL(a.href);toast('Export downloaded')} else if('cpPosted' in b.dataset){let l=listings.find(x=>x.id===document.getElementById('cp-listing')?.value),p=document.getElementById('cp-platform')?.value;if(!l||!p)return;l.crosspost={...(l.crosspost||{}),posted:{...(l.crosspost?.posted||{}),[p]:new Date().toISOString()}};save('fs-listings',listings);render();toast(`${p} marked Posted`)} else if(b.dataset.sold){let l=listings.find(x=>x.id===b.dataset.sold);if(l){l.status='sold';save('fs-listings',listings);render();toast('Marked sold')}} else if(b.dataset.newConvo){let choices=listings.map(l=>`${l.id} — ${l.title||'Untitled listing'}`).join('\n');let id=prompt('Enter the listing ID for this conversation:\n\n'+choices);if(!id||!listings.some(l=>l.id===id.trim())){toast('Enter a valid listing ID');return}let name=prompt('Participant name:')||'Buyer';let t=ensureTxForListing(id.trim(),name.trim());t.channel='dm';t.buyer_name=name.trim();save('fs-transactions',transactions);selectedConversation=t.id;render()} else if(b.dataset.conversation){selectedConversation=b.dataset.conversation;render()} else if(b.dataset.sendMsg){let text=document.getElementById('message-text').value.trim();if(!text){toast('Write a message first');return}let t=txForConversation(b.dataset.sendMsg);let author=document.getElementById('message-author').value,name=document.getElementById('participant-name').value.trim()|| (author==='seller'?'Seller':'Buyer');messagesData.push({id:'msg-'+Date.now(),tx_id:t.id,author,author_name:name,text,at:new Date().toISOString(),source:'user-recorded'});if(author==='buyer')t.buyer_name=name;save('fs-messages',messagesData);save('fs-transactions',transactions);render();toast('Message recorded')} else if(b.dataset.event)addTimelineEvent(b.dataset.tx,b.dataset.event) else if(b.dataset.saveShipping){saveShipping(b.dataset.saveShipping,b.dataset.tx)} else if(b.dataset.createLabel){createLabel(b.dataset.createLabel,b.dataset.tx)} else if(b.dataset.shippingAction){shippingAction(b.dataset.tx,b.dataset.shippingAction)} else if(b.dataset.shippingFocus){selectedConversation=b.dataset.shippingFocus;view='messages';render()} else if(b.dataset.dmSale){let t=txForConversation(b.dataset.dmSale),l=t&&conversationListing(t.id);if(!t||!l)return;l.status='sold';t.status='confirmed';t.channel='dm';t.updated_at=new Date().toISOString();t.timeline=[...(t.timeline||[]),{event:'Sold via DM',at:t.updated_at,source:'user-recorded'}];save('fs-listings',listings);save('fs-transactions',transactions);render();toast('Sold via DM recorded')} else if(b.dataset.manual){let t=txForConversation(b.dataset.manual);if(!t)return;let flair=prompt('Optional forum flair text:')||'',url=prompt('Optional source URL:')||'',kind=prompt('Record as "Forum flair" or "Buyer confirmation":','Buyer confirmation')||'Buyer confirmation';t.timeline=[...(t.timeline||[]),{event:kind,at:new Date().toISOString(),note:flair,url,source:'user-recorded'}];t.manual_source={flair,url};save('fs-transactions',transactions);render();toast(`${kind} recorded`)} else if(b.dataset.magic){let email=document.getElementById('auth-email').value.trim();if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)){toast('Enter a valid email address');return}let token=Math.random().toString(36).slice(2)+Date.now().toString(36);outbox.push({type:'magic_link',to:email,token,created_at:new Date().toISOString()});save('fs-outbox',outbox);authNotice=`
Capture mode: the magic link was recorded, not sent.
`;render()} else if(b.dataset.openLink){let email=document.getElementById('auth-email')?.value||outbox.filter(x=>x.type==='magic_link').slice(-1)[0]?.to;session.email=email;session.signed_in=true;save('fs-session',session);authNotice='';toast('Signed in');render()} else if(b.dataset.reddit){let username=document.getElementById('reddit-user').value.trim().replace(/^u\//,'');if(!/^[A-Za-z0-9_-]{1,30}$/.test(username)){toast('Enter a valid Reddit username');return}session.reddit={username,linked_at:new Date().toISOString()};outbox.push({type:'reddit_oauth_identity',username});save('fs-session',session);save('fs-outbox',outbox);toast(`Linked as u/${username}`);render()} else if(b.dataset.signout){delete session.email;delete session.signed_in;delete session.reddit;save('fs-session',session);toast('Signed out');render()} else if(b.dataset.clear){listings=seed;outbox=[];transactions=[];messagesData=[];session={owner:true};save('fs-listings',listings);save('fs-outbox',outbox);save('fs-transactions',transactions);save('fs-messages',messagesData);save('fs-session',session);toast('Local app data cleared');render()} }); document.addEventListener('change',e=>{if(e.target.id==='cp-listing'){selectedCrosspost=e.target.value;render()}else if(e.target.id==='image-files'){collect();[...e.target.files].forEach(file=>{let r=new FileReader();r.onload=()=>{draft.images=[...(draft.images||[]),r.result];render()};r.readAsDataURL(file)})}else if(e.target.id==='owner-session'){session.owner=e.target.checked;save('fs-session',session);view='feed';render()}else if(e.target.closest('.filters'))updateFeed()}); document.addEventListener('input',e=>{if(e.target.closest('.filters'))updateFeed()}); render();