Help
RSS
API
Feed
Maltego
Contact
Domain > chairexchange.co.uk
×
More information on this domain is in
AlienVault OTX
Is this malicious?
Yes
No
DNS Resolutions
Date
IP Address
2026-01-20
3.168.73.121
(
ClassC
)
2026-02-23
18.161.6.125
(
ClassC
)
Port 80
HTTP/1.1 301 Moved PermanentlyServer: CloudFrontDate: Mon, 23 Feb 2026 17:20:32 GMTContent-Type: text/htmlContent-Length: 167Connection: keep-aliveLocation: https://chairexchange.co.uk/X-Cache: Redirect from cloudfrontVia: 1.1 0a166b53605851fe961f5a2952e5a748.cloudfront.net (CloudFront)X-Amz-Cf-Pop: HIO52-P1X-Amz-Cf-Id: Hw-prChR7Ie_ujVqTmhofBbzL8Z7IvQg53D7spSVLzuvlRXx2Up-pg html>head>title>301 Moved Permanently/title>/head>body>center>h1>301 Moved Permanently/h1>/center>hr>center>CloudFront/center>/body>/html>
Port 443
HTTP/1.1 200 OKContent-Type: text/htmlContent-Length: 148349Connection: keep-aliveDate: Mon, 23 Feb 2026 17:20:34 GMTLast-Modified: Wed, 04 Feb 2026 09:26:02 GMTETag: 945aac2ee951ed138579c31336e168e8x-amz-server-side-encryption: AES256Accept-Ranges: bytesServer: AmazonS3X-Cache: Miss from cloudfrontVia: 1.1 1cbc126937aab64e42a05f9bf2f8daee.cloudfront.net (CloudFront)X-Amz-Cf-Pop: HIO52-P1X-Amz-Cf-Id: F7V60-N-NviKVcFQoMRsCgtQxwAQIR82gh6hQOtEc8BRHjRt6dZlqg !DOCTYPE html>html>head> meta charsetUTF-8> meta nameviewport contentwidthdevice-width, initial-scale1.0> title idpageTitle>Chair Exchange - Rent Salon Chairs, Stations & Beauty Rooms UK/title> meta namedescription idpageDescription contentFind salon spaces, chairs, stations, nail desks and beauty rooms available to rent across the UK. Browse listings for barber chairs, hairdresser stations, beauty treatment rooms and more β all with flexible rental options in your city.> meta namekeywords contentchair rental UK, salon chair rental, barber chair rental, beauty room hire, nail desk rental, hairdresser station, treatment room hire, salon space rental> link relcanonical idpageCanonical hrefhttps://chairexchange.co.uk> link relicon hrefhttps://d3mpftpygc0vej.cloudfront.net/favicon.png> link relstylesheet hrefhttps://unpkg.com/leaflet@1.9.4/dist/leaflet.css /> script srchttps://unpkg.com/leaflet@1.9.4/dist/leaflet.js>/script> script srchttps://js.stripe.com/v3/>/script> script srchttps://cdn.jsdelivr.net/npm/amazon-cognito-identity-js@6.3.7/dist/amazon-cognito-identity.min.js>/script> !-- Google Analytics --> script async srchttps://www.googletagmanager.com/gtag/js?idG-XXXXXXXXXX>/script> script> window.dataLayer window.dataLayer || ; function gtag(){dataLayer.push(arguments);} gtag(js, new Date()); gtag(config, G-XXXXXXXXXX); /script> style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Arial, sans-serif; background: #000; color: #fff; } nav { background: #000; padding: 0; display: flex; justify-content: space-between; align-items: center; border-bottom: 3px solid #222; position: sticky; top: 0; z-index: 100; min-height: 70px; } nav .logo { padding: 0 2rem; font-size: 1.1rem; color: #fff; display: flex; align-items: center; gap: 0.75rem; background: #000; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; text-decoration: none; } nav .logo img { height: 50px; width: auto; } nav .nav-links { display: flex; align-items: center; margin-left: auto; } nav .nav-links a { color: #999; text-decoration: none; font-weight: 600; cursor: pointer; padding: 1.5rem 2rem; display: flex; align-items: center; transition: all 0.3s; font-size: 0.95rem; } nav .nav-links a:first-child { border-left: none; } nav .nav-links a:hover { background: #111; color: #fff; } nav .nav-links a.active { color: #fff; border-bottom-color: #BF0A30; background: #0a0a0a; } nav .nav-links a.btn-login { background: #002868; color: #fff; } nav .nav-links a.btn-login:hover { background: #003a8c; } nav .nav-links a.btn-post { background: #BF0A30; color: #fff; border-radius: 0; margin: 0; } nav .nav-links a.btn-post:hover { background: #a00828; } .hamburger { display: none; flex-direction: column; gap: 6px; cursor: pointer; padding: 1rem; } .hamburger span { height: 4px; width: 30px; border-radius: 3px; transition: 0.3s; display: block; } .hamburger span:nth-child(1) { background: #002868; } .hamburger span:nth-child(2) { background: #fff; } .hamburger span:nth-child(3) { background: #BF0A30; } .mobile-menu { display: none; position: fixed; top: 60px; left: 0; right: 0; background: #111; padding: 2rem; border-bottom: 1px solid #222; z-index: 99; } .mobile-menu a { display: block; color: #fff; padding: 1rem 0; text-decoration: none; border-bottom: 1px solid #222; } .search-bar { background: #111; padding: 0; border-bottom: 1px solid #222; } .filter-toggle { display: none; background: #BF0A30; color: #fff; border: none; padding: 1rem; width: 100%; font-weight: 700; font-size: 1.1rem; cursor: pointer; margin: 0; border-radius: 0; text-align: center; } .filter-toggle:hover { background: #a00828; } .search-container { max-width: 1400px; margin: 0 auto; display: grid; grid-template-columns: 2fr 1fr 1fr auto auto; gap: 1rem; padding: 2rem; padding-top: 1rem; } .search-container input, .search-container select { background: #000; border: 2px solid #333; color: #fff; padding: 1rem 1.25rem; border-radius: 8px; font-size: 1.1rem; font-weight: 500; } .search-container input::placeholder { color: #666; } .btn-search { background: #BF0A30; color: #fff; border: none; padding: 1rem 2.5rem; border-radius: 8px; cursor: pointer; font-weight: 700; font-size: 1.1rem; } .btn-search:hover { background: #a00828; } .container { max-width: 1400px; margin: 0 auto; padding: 2rem; } .results-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 1px solid #222; gap: 1rem; } .results-count { color: #999; font-size: 0.95rem; } .view-toggle { display: flex; gap: 0.5rem; } .view-toggle button { background: #000; color: #999; border: 2px solid #333; padding: 0.75rem 1.5rem; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 0.95rem; transition: all 0.3s; display: flex; align-items: center; gap: 0.5rem; } .view-toggle button:hover { border-color: #BF0A30; color: #fff; } .view-toggle button.active { background: #BF0A30; color: #fff; border-color: #BF0A30; } .btn-post { background: #002868; color: #fff; padding: 1rem 2rem; border: none; border-radius: 8px; cursor: pointer; font-weight: 700; font-size: 1.1rem; } #mapView { display: none; height: 600px; border-radius: 12px; overflow: hidden; border: 3px solid #222; position: relative; } #mapView .no-results { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; z-index: 1000; background: rgba(0,0,0,0.8); padding: 2rem; border-radius: 12px; } #mapView .no-results div:first-child { font-size: 4rem; margin-bottom: 1rem; } #mapView .no-results h3 { color: #999; font-size: 1.5rem; margin-bottom: 0.5rem; } #mapView .no-results p { color: #666; } .listings { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 1.5rem; } .listing-card { background: #111; border: 3px solid #222; border-radius: 12px; overflow: hidden; transition: 0.3s; cursor: pointer; display: flex; flex-direction: column; height: 100%; } .listing-card:hover { border-color: #BF0A30; transform: translateY(-4px); } .listing-card.featured { border-color: #FFD700; box-shadow: 0 0 20px rgba(255, 215, 0, 0.3); } .listing-image { width: 100%; height: 240px; background: #222; position: relative; overflow: hidden; } .listing-image img { width: 100%; height: 100%; object-fit: cover; } .listing-image .placeholder { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; font-size: 3rem; color: #333; } .listing-price { position: absolute; top: 1rem; right: 1rem; background: #BF0A30; color: #fff; padding: 0.75rem 1.5rem; border-radius: 8px; font-weight: 800; font-size: 1.4rem; box-shadow: 0 4px 12px rgba(0,0,0,0.5); } .listing-content { padding: 1.5rem; flex: 1; display: flex; flex-direction: column; } .listing-title { font-size: 1.5rem; font-weight: 800; margin-bottom: 0.75rem; color: #fff; } .listing-location { color: #999; font-size: 1.05rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; font-weight: 500; } .listing-description { color: #ccc; font-size: 1rem; line-height: 1.7; margin-bottom: 1rem; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; flex: 1; min-height: 4.5rem; } .listing-meta { display: flex; justify-content: space-between; align-items: center; padding-top: 1rem; border-top: 1px solid #222; } .listing-type { background: #002868; color: #fff; padding: 0.5rem 1rem; border-radius: 20px; font-size: 0.9rem; font-weight: 700; } .listing-contact { color: #999; font-size: 0.95rem; font-weight: 500; } .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 1000; overflow-y: auto; } .modal-content { background: #111; max-width: 600px; margin: 3rem auto; padding: 2rem; border-radius: 8px; border: 1px solid #222; position: relative; } .modal-content h2 { color: #fff; margin-bottom: 1.5rem; } .modal-content input, .modal-content textarea, .modal-content select { width: 100%; background: #000; border: 2px solid #333; color: #fff; padding: 1rem; margin-bottom: 1rem; border-radius: 8px; font-size: 1.05rem; } .modal-content textarea { resize: vertical; font-family: inherit; } .close { position: absolute; top: 1rem; right: 1rem; font-size: 2.5rem; cursor: pointer; color: #fff; background: none; border: none; z-index: 10; text-shadow: 0 0 10px rgba(0,0,0,0.8); font-weight: 300; line-height: 1; padding: 0; } .close:hover { color: #BF0A30; } #cookieConsent { display: none; position: fixed; bottom: 0; left: 0; right: 0; background: #111; border-top: 3px solid #BF0A30; padding: 1.5rem; z-index: 2000; } #cookieConsent .container { max-width: 1400px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; gap: 2rem; } #cookieConsent p { margin: 0; color: #ccc; } #cookieConsent button { background: #BF0A30; color: #fff; border: none; padding: 0.75rem 2rem; border-radius: 8px; cursor: pointer; font-weight: 700; white-space: nowrap; } #cookieConsent button:hover { background: #a00828; } #cookieConsent a { color: #BF0A30; } .spinner { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 2000; } .spinner::after { content: ; width: 60px; height: 60px; border: 6px solid #222; border-top-color: #BF0A30; border-radius: 50%; animation: spin 0.8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } footer { background: #111; border-top: 3px solid #222; padding: 1.5rem; margin-top: 4rem; } footer .container { max-width: 1400px; margin: 0 auto; display: grid; grid-template-columns: repeat(2, 1fr); gap: 2rem; } footer h3 { color: #BF0A30; margin-bottom: 0.75rem; font-size: 1rem; } footer a { color: #999; text-decoration: none; display: block; margin-bottom: 0.4rem; font-size: 0.9rem; } footer a:hover { color: #fff; } footer .copyright { grid-column: 1/-1; text-align: center; color: #666; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #222; font-size: 0.85rem; } .listing-detail-modal .modal-content { max-width: 700px; position: relative; padding: 0; overflow: hidden; } .listing-detail-image { width: 100%; height: 280px; background: #222; overflow: hidden; position: relative; } .listing-detail-image img { width: 100%; height: 100%; object-fit: cover; } .listing-detail-content { padding: 2rem; } .listing-detail-price { color: #BF0A30; font-size: 1.5rem; font-weight: 800; margin-bottom: 0.75rem; } .listing-detail-title { font-size: 1.5rem; font-weight: 800; margin-bottom: 0.75rem; } .listing-detail-location { color: #999; font-size: 1rem; margin-bottom: 1rem; } .listing-detail-description { color: #ccc; font-size: 1rem; line-height: 1.6; margin-bottom: 1.5rem; } .listing-detail-contact { background: #BF0A30; color: #fff; padding: 0.75rem 1.5rem; border-radius: 8px; display: inline-block; font-weight: 700; font-size: 1rem; } @media (max-width: 768px) { nav { flex-wrap: wrap; } nav .logo { padding: 1rem 1.5rem; font-size: 1rem; } .nav-links { display: none !important; } .hamburger { display: flex !important; margin-right: 1.5rem; } .filter-toggle { display: block; } .search-container { grid-template-columns: 1fr; padding: 0 1rem; display: none; padding-top: 1.5rem; padding-bottom: 1.5rem; } .search-container.show { display: grid; } .container { padding: 1rem; } .listings { grid-template-columns: 1fr; } .listing-card { margin-bottom: 1rem; } .listing-price { font-size: 1.1rem; padding: 0.5rem 1rem; } .listing-title { font-size: 1.3rem; } .listing-meta { flex-direction: column; gap: 0.5rem; align-items: flex-start; } .results-header { flex-direction: column; gap: 1rem; align-items: flex-start; } .view-toggle { width: 100%; } .view-toggle button { flex: 1; justify-content: center; } .btn-post { width: 100%; } .modal-content { margin: 1rem; padding: 1.5rem; max-width: calc(100% - 2rem); } h2 { font-size: 1.5rem !important; } .listing-detail-image { height: 200px; margin-bottom: 1rem; } .listing-detail-content { padding: 1.5rem; } .listing-detail-price { font-size: 1.3rem; margin-bottom: 0.5rem; } .listing-detail-title { font-size: 1.3rem; margin-bottom: 0.5rem; } .listing-detail-location { font-size: 1rem; margin-bottom: 1rem; } .listing-detail-description { font-size: 0.95rem; line-height: 1.6; margin-bottom: 1rem; } .listing-detail-contact { padding: 0.75rem 1.5rem; font-size: 1rem; } } /style>/head>body> nav> a href#barbers classlogo onclickshowCategory(barbers); return false;>img srchttps://d3mpftpygc0vej.cloudfront.net/images/logo.png altChair Exchange Logo>/a> div classhamburger onclicktoggleMenu()> span>/span> span>/span> span>/span> /div> div classnav-links> a href#barbers idbarbersTab onclickshowCategory(barbers)>Barbers/a> a href#hair idhairTab onclickshowCategory(hair)>Hairdresser/a> a href#nails idnailsTab onclickshowCategory(nails)>Nails/a> a href#beauty idbeautyTab onclickshowCategory(beauty)>Beauty/a> a href#offers idoffersTab onclickshowSection(offers)>Offers/a> a hrefmembers.html>Our Members/a> a href# idloginBtn classbtn-login>Login / Register/a> a href# idprofileBtn classbtn-login styledisplay:none; onclickshowProfile(); return false;>Account/a> a href# idpostAdBtn classbtn-post>Create Ad/a> a href# idlogoutBtn classbtn-login styledisplay:none; onclicklogout()>Logout/a> /div> /nav> div classmobile-menu idmobileMenu> a href#barbers onclickshowCategory(barbers)>Barbers/a> a href#hair onclickshowCategory(hair)>Hairdresser/a> a href#nails onclickshowCategory(nails)>Nails/a> a href#beauty onclickshowCategory(beauty)>Beauty/a> a href#offers onclickshowSection(offers)>Offers/a> a hrefmembers.html>Our Members/a> a href# idmobileLogin>Login / Register/a> a href# idmobileProfile styledisplay:none; onclickshowProfile()>Account/a> a href# idmobilePostAd>Post Ad/a> a href# idmobileLogout styledisplay:none; onclicklogout()>Logout/a> /div> div classsearch-bar> button classfilter-toggle onclicktoggleFilters()>π Show Filters/button> div classsearch-container idsearchContainer> select idseatCategoryFilter onchangesearchListings()> option valueall>All Workspace Types/option> option valuechair>Chair/option> option valuestation>Station/option> option valuenaildesk>Nail Desk/option> option valuetreatmentroom>Treatment Room/option> option valuestudio>Studio / Room/option> option valueholiday>Holiday Cover/option> option valuebusiness>Business for Sale/option> option valuefulltime>Full Time Job/option> option valueparttime>Part Time Job/option> /select> input typetext idsearchPostcode placeholderPostcode (e.g. SW1A 1AA)> select idsearchRadius> option value5>5 miles/option> option value10>10 miles/option> option value25>25 miles/option> option value50>50 miles/option> option value100>100 miles/option> /select> button classbtn-search onclicksearchListings()>Search/button> button classbtn-search onclickclearFilters() stylebackground: #333;>Clear/button> /div> /div> div classcontainer idseatsSection> div classresults-header> div classresults-count idseatsCount>0 listings found/div> div classview-toggle> button onclicktoggleView(list) idlistViewBtn classactive> svg width16 height16 viewBox0 0 16 16 fillcurrentColor> path dM0 2h16v2H0V2zm0 5h16v2H0V7zm0 5h16v2H0v-2z/> /svg> List /button> button onclicktoggleView(map) idmapViewBtn> svg width16 height16 viewBox0 0 16 16 fillcurrentColor> path dM8 0C5.2 0 3 2.2 3 5c0 3.5 5 11 5 11s5-7.5 5-11c0-2.8-2.2-5-5-5zm0 7.5c-1.4 0-2.5-1.1-2.5-2.5S6.6 2.5 8 2.5s2.5 1.1 2.5 2.5S9.4 7.5 8 7.5z/> /svg> Map /button> /div> button classbtn-post idpostSeatBtn styledisplay:none;>+ Create Ad/button> /div> h2 stylecolor: #fff; font-size: 2rem; margin-bottom: 1.5rem; border-bottom: 3px solid #BF0A30; padding-bottom: 0.5rem;>πΊ Workspaces Available/h2> div idmapView> div classno-results idmapNoResults styledisplay:none;> div>πΊοΈ/div> h3>No listings on map/h3> p>Try adjusting your filters or search area/p> /div> /div> div classlistings idseatsGrid>/div> /div> div classcontainer idoffersSection styledisplay:none;> div classresults-header> div classresults-count idoffersCount>0 listings found/div> button classbtn-post idpostOfferBtn styledisplay:none;>+ Create Ad/button> /div> h2 stylecolor: #fff; font-size: 2rem; margin-bottom: 1.5rem; border-bottom: 3px solid #002868; padding-bottom: 0.5rem;>π Product Offers/h2> div classlistings idoffersGrid>/div> /div> div classcontainer idmyAdsSection styledisplay:none;> div classresults-header> div classresults-count idmyAdsCount>0 listings found/div> /div> h2 stylecolor: #fff; font-size: 2rem; margin-bottom: 1.5rem; border-bottom: 3px solid #002868; padding-bottom: 0.5rem;>π My Ads/h2> div classlistings idmyAdsGrid>/div> /div> !-- Listing Detail Modal --> div idlistingDetailModal classmodal listing-detail-modal> div classmodal-content> span classclose onclickcloseModal(listingDetailModal)>×/span> div idlistingDetailContent>/div> /div> /div> !-- Edit Listing Modal --> div ideditModal classmodal stylez-index: 1001;> div classmodal-content> span classclose onclickcloseModal(editModal)>×/span> h2>Edit Listing/h2> input typehidden ideditListingId> select ideditPrimaryCategory stylemargin-bottom: 1rem; onchangeupdateEditWorkspaceOptions()> option value>Select Category/option> option valuebarbers>Barbers/option> option valuehair>Hairdresser/option> option valuenails>Nails/option> option valuebeauty>Beauty/option> /select> select ideditSeatCategory> option value>Select Workspace Type/option> option valuechair>Chair/option> option valuestation>Station/option> option valuenaildesk>Nail Desk/option> option valuetreatmentroom>Treatment Room/option> option valuestudio>Studio / Room/option> option valueholiday>Holiday Cover/option> option valuebusiness>Business for Sale/option> option valuefulltime>Full Time Job/option> option valueparttime>Part Time Job/option> /select> input typetext ideditShopName placeholderShop/Company Name> input typetext ideditRegion placeholderPostcode (e.g. SW1A 1AA)> div styledisplay: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem;> input typenumber ideditPrice placeholderPrice stylemargin-bottom: 0;> select ideditPriceUnit stylemargin-bottom: 0;> option valueweek>Per Week/option> option valueday>Per Day/option> option valuehour>Per Hour/option> /select> /div> textarea ideditDescription placeholderDescription... rows5>/textarea> input typetext ideditContact placeholderContact (Phone/Email)> label stylecolor: #999; font-size: 0.9rem; margin-bottom: 0.5rem; display: block;>Update Photo/label> input typefile ideditImageUpload acceptimage/jpeg,image/jpg,image/png onchangehandleEditImageUpload(event) stylemargin-bottom: 1rem;> button typebutton classbtn-search onclickuseEditPlaceholder() stylepadding: 0.5rem 1rem; font-size: 0.9rem; background: #333; margin-bottom: 1rem;>Use Category Placeholder/button> div ideditImagePreview stylemargin-bottom: 1rem; display: none;> img ideditPreviewImg stylemax-width: 100%; max-height: 200px; border-radius: 8px;> /div> button classbtn-search onclickupdateListing()>Update Listing/button> /div> /div> !-- Profile Modal --> div idprofileModal classmodal> div classmodal-content> span classclose onclickcloseModal(profileModal)>×/span> h2>My Account/h2> div styleborder-bottom: 2px solid #222; margin-bottom: 1.5rem;> button onclickshowProfileTab(details) idprofileDetailsTab classbtn-search stylemargin-right: 0.5rem; margin-bottom: 1rem; background: #002868;>Profile/button> button onclickshowProfileTab(ads) idprofileAdsTab classbtn-search stylemargin-bottom: 1rem; background: #333;>My Ads/button> /div> div idprofileDetailsSection> form onsubmitupdateProfile(); return false; stylemargin-bottom: 1rem;> label stylecolor: #999; font-size: 0.9rem;>Profile Picture/label> div idprofilePicturePreview stylemargin-bottom: 1rem; display: none;> img idprofilePictureImg stylewidth: 120px; height: 120px; border-radius: 50%; object-fit: cover; border: 3px solid #BF0A30;> /div> input typefile idprofilePictureUpload acceptimage/jpeg,image/jpg,image/png onchangehandleProfilePictureUpload(event) stylemargin-bottom: 1rem;> label stylecolor: #999; font-size: 0.9rem;>Name/label> input typetext idprofileName stylemargin-bottom: 0.5rem;> label stylecolor: #999; font-size: 0.9rem;>Shop/Company Name/label> input typetext idprofileShopName stylemargin-bottom: 0.5rem;> label stylecolor: #999; font-size: 0.9rem;>Location (Postcode)/label> input typetext idprofileLocation placeholdere.g. SW1A 1AA stylemargin-bottom: 0.5rem;> label stylecolor: #999; font-size: 0.9rem;>Contact Details/label> input typetext idprofileContact placeholderPhone/Email stylemargin-bottom: 0.5rem;> label stylecolor: #999; font-size: 0.9rem;>Services Offered/label> select idprofileServicesCategory onchangeupdateProfileServices() stylemargin-bottom: 0.5rem;> option value>Select Service Category/option> option valuebarber>Barber Services/option> option valuehair>Hair Salon Services/option> option valuenails>Nail Services/option> option valuebeauty>Beauty Services/option> /select> div idprofileServicesCheckboxes styledisplay: none; background: #000; border: 2px solid #333; padding: 1rem; border-radius: 8px; margin-bottom: 0.5rem; max-height: 200px; overflow-y: auto;> /div> button typesubmit classbtn-search stylepadding: 0.75rem 1.5rem; font-size: 1rem;>Save Profile/button> /form> div stylemargin-bottom: 1rem;> label stylecolor: #999; font-size: 0.9rem;>Email/label> div stylecolor: #fff; font-size: 1.1rem; font-weight: 600; margin-bottom: 1rem; idprofileEmail>/div> /div> div stylemargin-bottom: 1rem;> label stylecolor: #999; font-size: 0.9rem;>Account Type/label> div stylecolor: #fff; font-size: 1.1rem; font-weight: 600; margin-bottom: 1rem; idprofileType>/div> /div> h3 stylecolor: #fff; margin-top: 2rem; margin-bottom: 1rem;>Change Password/h3> form onsubmitchangePassword(); return false;> input typeemail idprofileEmailHidden autocompleteusername styleposition: absolute; left: -9999px;> input typepassword idcurrentPassword placeholderCurrent Password autocompletecurrent-password> input typepassword idnewPassword placeholderNew Password autocompletenew-password> input typepassword idconfirmPassword placeholderConfirm New Password autocompletenew-password> button typesubmit classbtn-search>Update Password/button> /form> div stylemargin-top: 2rem; padding: 1rem; background: #000; border: 1px solid #333; border-radius: 8px;> button onclickcloseAccount() classbtn-search stylebackground: #BF0A30; width: 100%; margin-bottom: 0.5rem;>Close Account/button> p stylecolor: #666; font-size: 0.85rem; text-align: center; margin: 0;>Permanently delete your account, profile, and all listings/p> /div> /div> div idprofileAdsSection styledisplay:none;> div idprofileAdsGrid styledisplay: grid; grid-template-columns: 1fr; gap: 1rem; max-height: 60vh; overflow-y: auto;>/div> /div> /div> /div> !-- Login Modal --> div idloginModal classmodal> div classmodal-content> span classclose onclickcloseModal(loginModal)>×/span> h2 idauthTitle>Register to Post Listings/h2> form onsubmitlogin(); return false;> input typeemail iduserEmail placeholderEmail autocompleteemail> input typetext iduserName placeholderYour Name styledisplay:none;> input typetext iduserShopName placeholderShop/Company Name styledisplay:none;> input typetext iduserLocation placeholderPostcode (e.g. SW1A 1AA) styledisplay:none;> input typetext iduserContact placeholderContact (Phone/Email) styledisplay:none;> select iduserType styledisplay:none; onchangetoggleShopTypeDropdown()> option value>I am a.../option> option valuebarber>Shop/Salon Owner/option> option valueretailer>Product Seller/option> /select> select idsignupShopType styledisplay:none; onchangeupdateSignupServices()> option value>Shop Type/option> option valuebarber>Barber/option> option valuehair>Hair Salon/option> option valuenails>Nails/option> option valuebeauty>Beauty/option> /select> div idsignupServicesSection styledisplay:none; margin-bottom: 1rem;> label stylecolor: #999; font-size: 0.9rem; display: block; margin-bottom: 0.5rem;>Services Offered/label> div idsignupServicesCheckboxes stylebackground: #000; border: 2px solid #333; padding: 1rem; border-radius: 8px; max-height: 150px; overflow-y: auto;> /div> /div> div idsignupPhotoSection styledisplay:none; margin-bottom: 1rem;> label stylecolor: #999; font-size: 0.9rem; display: block; margin-bottom: 0.5rem;>Profile Photo (Optional)/label> input typefile idsignupProfilePicture acceptimage/jpeg,image/jpg,image/png onchangehandleSignupPhotoUpload(event) stylemargin-bottom: 0.5rem;> div idsignupPhotoPreview styledisplay: none; margin-bottom: 0.5rem;> img idsignupPhotoImg stylewidth: 100px; height: 100px; border-radius: 50%; object-fit: cover; border: 3px solid #BF0A30;> /div> /div> input typepassword iduserPassword placeholderPassword autocompletecurrent-password> label styledisplay:none; color: #ccc; font-size: 0.9rem; margin-top: 1rem; text-align: center; idtermsLabel> input typecheckbox idtermsCheckbox stylewidth: auto; margin-right: 0.5rem;> I agree to the a hrefterms.html target_blank stylecolor: #BF0A30;>Terms & Conditions/a> /label> button typesubmit classbtn-search idauthBtn stylewidth: 100%;>Login/button> p styletext-align: center; margin-top: 0.5rem; margin-bottom: 0;>a href# onclickforgotPassword(); return false; idforgotPasswordLink stylecolor: #666; font-size: 0.9rem;>Forgot Password?/a>/p> /form> p styletext-align: center; margin-top: 1rem; color: #999; idauthSwitchText>Already have an account?/p> p styletext-align: center;>a href# onclicktoggleAuthMode(); return false; idauthSwitchLink stylecolor: #BF0A30;>Login/a>/p> /div> /div> !-- Post Listing Modal --> div idpostModal classmodal> div classmodal-content> span classclose onclickcloseModal(postModal)>×/span> h2>Post a Listing/h2> select idlistingType styledisplay:none;> option value>Select Type/option> option valueseat>Seat Available/option> option valueoffer>Product Offer/option> /select> select idadType onchangeupdateAdPrice() styledisplay:none;> option value>Select Ad Type/option> option valuebasic>Basic Listing (Β£15 - 30 days)/option> option valuefeatured>Featured Listing (Β£35 - 30 days)/option> option valuespotlight>City Spotlight (Β£75 - 7 days)/option> /select> div idadPriceInfo stylecolor: #BF0A30; font-weight: 700; margin-bottom: 1rem; display: none;>/div> select idprimaryCategory stylemargin-bottom: 1rem; onchangeupdateWorkspaceOptions()> option value>Select Category/option> option valuebarbers>Barbers/option> option valuehair>Hairdresser/option> option valuenails>Nails/option> option valuebeauty>Beauty/option> /select> select idseatCategory> option value>Select Workspace Type/option> option valuechair>Chair/option> option valuestation>Station/option> option valuenaildesk>Nail Desk/option> option valuetreatmentroom>Treatment Room/option> option valuestudio>Studio / Room/option> option valueholiday>Holiday Cover/option> option valuebusiness>Business for Sale/option> option valuefulltime>Full Time Job/option> option valueparttime>Part Time Job/option> /select> select idfeaturedOption stylemargin-bottom: 1rem; onchangeupdateFeaturedPrice()> option valuestandard>Standard Listing (FREE)/option> option valuefeatured>Featured Listing (Β£1/day - Gold Border + Star Icon)/option> /select> div idfeaturedPriceInfo stylecolor: #FFD700; font-weight: 700; margin-bottom: 1rem; display: none;>/div> select idproductCategory styledisplay:none; margin-bottom: 1rem;> option value>Select Product Category/option> option valuetools>Tools & Equipment/option> option valueproducts>Hair & Beauty Products/option> option valuefurniture>Furniture & Fixtures/option> option valueclothing>Uniforms & Clothing/option> option valuetechnology>Technology & Software/option> option valuetraining>Training & Courses/option> option valueother>Other/option> /select> select idofferType styledisplay:none; margin-bottom: 1rem;> option value>Type of Offer/option> option valuediscount>Percentage Discount (e.g. 10% off)/option> option valuebogof>Buy One Get One Free/option> option valuesale>Sale Price/option> option valuebundle>Bundle Deal/option> option valueclearance>Clearance/option> option valuenew>New Product Launch/option> option valueother>Other Offer/option> /select> input typetext idshopName placeholderShop/Company Name> input typetext idregion placeholderPostcode (e.g. SW1A 1AA)> div idpriceContainer styledisplay: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem;> input typenumber idprice placeholderPrice stylemargin-bottom: 0;> select idpriceUnit stylemargin-bottom: 0;> option valueweek>Per Week/option> option valueday>Per Day/option> option valuehour>Per Hour/option> /select> /div> input typenumber idproductPrice placeholderPrice (Β£) styledisplay:none; margin-bottom: 1rem;> select idexpiryDays stylemargin-bottom: 1rem; onchangeupdateFeaturedPrice()> option value>Ad Duration/option> option value3>3 Days/option> option value7>7 Days/option> option value10>10 Days/option> option value14>14 Days/option> option value30>30 Days/option> /select> label stylecolor: #999; font-size: 0.9rem; margin-bottom: 0.5rem; display: block;>Photo (JPG/PNG, max 5MB)/label> input typefile idimageUpload acceptimage/jpeg,image/jpg,image/png captureenvironment onchangehandleImageUpload(event) stylemargin-bottom: 1rem;> button typebutton classbtn-search onclickusePlaceholder() stylepadding: 0.5rem 1rem; font-size: 0.9rem; background: #333; margin-bottom: 1rem;>Use Category Placeholder/button> div idimagePreview stylemargin-bottom: 1rem; display: none;> img idpreviewImg stylemax-width: 100%; max-height: 200px; border-radius: 8px;> /div> input typetext idorderUrl placeholderOrder URL (for offers only) styledisplay:none;> textarea iddescription placeholderDescription... rows5>/textarea> input typetext idcontact placeholderContact (Phone/Email)> label stylecolor: #ccc; font-size: 0.9rem; margin-top: 1rem; display: block; text-align: center;> input typecheckbox idlistingTermsCheckbox stylewidth: auto; margin-right: 0.5rem;> I agree to the a hrefterms.html target_blank stylecolor: #BF0A30;>Terms & Conditions/a> and confirm I own/have permission for all uploaded images /label> button classbtn-search idsubmitListingBtn onclickpostListing() stylewidth: 100%;>Post Listing/button> /div> /div> !-- Cookie Consent --> div idcookieConsent> div classcontainer> p>We use essential cookies to provide our services. By continuing to use Chair Exchange, you accept our use of cookies. a hrefprivacy.html target_blank>Privacy Policy/a>/p> button onclickacceptCookies()>Accept/button> /div> /div> !-- Loading Spinner --> div idspinner classspinner>/div> !-- Footer --> footer> div classcontainer> div> h3>Chair Exchange/h3> a hrefabout.html>About Us/a> a hrefcontact.html>Contact/a> a hreffaq.html>FAQ/a> /div> div> h3>Legal/h3> a hrefterms.html>Terms & Conditions/a> a hrefprivacy.html>Privacy Policy/a> /div> div classcopyright> Β© 2026 Chair Exchange. A Delimiter Ltd and Chet Kaya Product. All rights reserved. /div> /div> /footer> script> let currentUser JSON.parse(localStorage.getItem(currentUser)); let seats ; let offers ; let filteredSeats ; let filteredOffers ; let currentSection seats; let currentCategory barbers; let currentSeatFilter all; let uploadedImage null; let editUploadedImage null; let profilePicture null; let map null; let markers ; const stripe Stripe(pk_live_51RcoSpGWJl4ogn6QXeb82AE8EsgDt1vDfaeZ4oVE2n7V4GyabPSVaC1kmS67BfPOJTsIbG4YHSiYKCc2LRfAS5Uq00SaifBCwC); const API_BASE https://lb66oyprze.execute-api.eu-west-2.amazonaws.com/prod; function showSpinner() { document.getElementById(spinner).style.display block; } function hideSpinner() { document.getElementById(spinner).style.display none; } // Category configuration - easily add new categories here const categories { barbers: { name: Barbers, title: Barber Chair & Station Rental UK | Chair Exchange, description: Find barber chairs and stations to rent across the UK. Browse flexible rental options for self-employed barbers in established barbershops., keywords: barber chair rental UK, barber station hire, self-employed barber, barbershop chair, workspaces: chair, station, studio, holiday, business, fulltime, parttime }, hair: { name: Hairdresser, title: Hair Salon Chair & Station Rental UK | Chair Exchange, description: Rent hair salon chairs and styling stations across the UK. Flexible rental options for freelance hairdressers and stylists., keywords: hair salon chair rental, hairdresser chair hire, salon station rental UK, workspaces: chair, station, studio, holiday, business, fulltime, parttime }, nails: { name: Nails, title: Nail Desk Rental UK | Chair Exchange, description: Find nail desks to rent across the UK. Browse flexible rental options for nail technicians in salons and beauty spaces., keywords: nail desk rental UK, nail table hire, nail technician desk, manicure station, workspaces: naildesk, treatmentroom, studio, holiday, business, fulltime, parttime }, beauty: { name: Beauty, title: Beauty Room Rental UK | Chair Exchange, description: Rent beauty treatment rooms across the UK. Flexible rental options for beauticians, therapists and aestheticians., keywords: beauty room rental UK, treatment room hire, therapy room rental, beautician room, workspaces: treatmentroom, studio, holiday, business, fulltime, parttime } // Future categories can be added here: // aesthetics: { name: Aesthetics, title: ..., description: ..., keywords: ... }, // massage: { name: Massage, title: ..., description: ..., keywords: ... } }; const servicesByCategory { barber: Haircuts, Beard Trims, Hot Towel Shaves, Hair Coloring, Skin Fades, Buzz Cuts, Hair Styling, Scalp Treatments, hair: Haircuts, Hair Coloring, Highlights, Balayage, Perms, Hair Extensions, Blow Dry, Hair Treatments, nails: Manicure, Pedicure, Gel Nails, Acrylic Nails, Nail Art, French Polish, Nail Extensions, Nail Repair, beauty: Facials, Waxing, Threading, Eyelash Extensions, Eyebrow Tinting, Makeup, Massage, Skin Treatments, retailer: Hair Products, Styling Tools, Barber Equipment, Salon Furniture, Beauty Supplies, Professional Training, Wholesale Distribution }; const servicesByType { barber: servicesByCategory.barber, retailer: servicesByCategory.retailer }; function updateProfileServices() { const category document.getElementById(profileServicesCategory).value; const container document.getElementById(profileServicesCheckboxes); if (!category) { container.style.display none; return; } const services servicesByCategorycategory; container.innerHTML services.map(s > ` label styledisplay: block; color: #fff; margin-bottom: 0.5rem; cursor: pointer;> input typecheckbox value${s} stylewidth: auto; margin-right: 0.5rem; onchangesaveProfileServicesSelection()>${s} /label> `).join(); container.style.display block; } function saveProfileServicesSelection() { const checkboxes document.querySelectorAll(#profileServicesCheckboxes inputtypecheckbox:checked); const selected Array.from(checkboxes).map(cb > cb.value); document.getElementById(profileServicesCategory).dataset.selected selected.join(, ); } let signupProfilePicture null; function handleSignupPhotoUpload(event) { const file event.target.files0; if (!file) return; if (file.size > 5 * 1024 * 1024) { alert(File size must be less than 5MB); event.target.value ; return; } if (!image/jpeg, image/jpg, image/png.includes(file.type)) { alert(Only JPG and PNG files are allowed); event.target.value ; return; } const reader new FileReader(); reader.onload function(e) { const img new Image(); img.onload function() { const canvas document.createElement(canvas); const size 400; canvas.width size; canvas.height size; const ctx canvas.getContext(2d); const scale Math.max(size / img.width, size / img.height); const x (size - img.width * scale) / 2; const y (size - img.height * scale) / 2; ctx.drawImage(img, x, y, img.width * scale, img.height * scale); signupProfilePicture canvas.toDataURL(image/jpeg, 0.85); document.getElementById(signupPhotoImg).src signupProfilePicture; document.getElementById(signupPhotoPreview).style.display block; }; img.src e.target.result; }; reader.readAsDataURL(file); } function toggleShopTypeDropdown() { const type document.getElementById(userType).value; const shopTypeDropdown document.getElementById(signupShopType); const section document.getElementById(signupServicesSection); if (type barber) { shopTypeDropdown.style.display block; section.style.display none; } else if (type retailer) { shopTypeDropdown.style.display none; const container document.getElementById(signupServicesCheckboxes); const services servicesByCategory.retailer; container.innerHTML services.map(s > ` label styledisplay: block; color: #fff; margin-bottom: 0.5rem; cursor: pointer;> input typecheckbox value${s} stylewidth: auto; margin-right: 0.5rem;>${s} /label> `).join(); section.style.display block; } else { shopTypeDropdown.style.display none; section.style.display none; } } function updateSignupServices() { const shopType document.getElementById(signupShopType).value; const container document.getElementById(signupServicesCheckboxes); const section document.getElementById(signupServicesSection); if (!shopType) { section.style.display none; return; } const services servicesByCategoryshopType; container.innerHTML services.map(s > ` label styledisplay: block; color: #fff; margin-bottom: 0.5rem; cursor: pointer;> input typecheckbox value${s} stylewidth: auto; margin-right: 0.5rem;>${s} /label> `).join(); section.style.display block; } // Cognito Configuration const poolData { UserPoolId: eu-west-2_TlhrjcZfY, ClientId: 5anjk4jtrsqa2m1rcej7m2hk3k }; const userPool new AmazonCognitoIdentity.CognitoUserPool(poolData); // Cookie consent function acceptCookies() { localStorage.setItem(cookieConsent, true); document.getElementById(cookieConsent).style.display none; } function checkCookieConsent() { if (!localStorage.getItem(cookieConsent)) { document.getElementById(cookieConsent).style.display block; } } checkCookieConsent(); // Handle URL routing and SEO function handleRoute() { const hash window.location.hash.slice(1) || barbers; if (categorieshash) { showCategory(hash); updateSEO(hash); } else if (hash offers) { showSection(offers); updateSEO(offers); } else { showCategory(barbers); updateSEO(barbers); } } function updateSEO(category) { if (category offers) { document.getElementById(pageTitle).textContent Product Offers for Beauty & Wellness Professionals | Chair Exchange; document.getElementById(pageDescription).content Browse product offers, equipment and supplies for beauty and wellness professionals across the UK.; document.getElementById(pageCanonical).href https://chairexchange.co.uk#offers; } else if (categoriescategory) { const cat categoriescategory; document.getElementById(pageTitle).textContent cat.title; document.getElementById(pageDescription).content cat.description; document.getElementById(pageCanonical).href `https://chairexchange.co.uk#${category}`; } // Update Open Graph tags let ogTitle document.querySelector(metapropertyog:title); let ogDesc document.querySelector(metapropertyog:description); let ogUrl document.querySelector(metapropertyog:url); if (!ogTitle) { ogTitle document.createElement(meta); ogTitle.setAttribute(property, og:title); document.head.appendChild(ogTitle); } if (!ogDesc) { ogDesc document.createElement(meta); ogDesc.setAttribute(property, og:description); document.head.appendChild(ogDesc); } if (!ogUrl) { ogUrl document.createElement(meta); ogUrl.setAttribute(property, og:url); document.head.appendChild(ogUrl); } ogTitle.setAttribute(content, document.getElementById(pageTitle).textContent); ogDesc.setAttribute(content, document.getElementById(pageDescription).content); ogUrl.setAttribute(content, document.getElementById(pageCanonical).href); } window.addEventListener(hashchange, handleRoute); window.addEventListener(load, handleRoute); function toggleView(view) { const listView document.getElementById(seatsGrid); const mapView document.getElementById(mapView); const listBtn document.getElementById(listViewBtn); const mapBtn document.getElementById(mapViewBtn); if (view map) { listView.style.display none; mapView.style.display block; listBtn.classList.remove(active); mapBtn.classList.add(active); initMap(); } else { listView.style.display grid; mapView.style.display none; listBtn.classList.add(active); mapBtn.classList.remove(active); } } function initMap() { if (!map) { map L.map(mapView).setView(54.5, -2.5, 6); L.tileLayer(https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png, { attribution: © OpenStreetMap contributors }).addTo(map); } markers.forEach(m > map.removeLayer(m)); markers ; const iconMap { chair: πΊ, station: πͺ, naildesk: π , treatmentroom: π, studio: π , holiday: ποΈ, business: πΌ, fulltime: πΌ, parttime: β° }; filteredSeats.forEach(listing > { if (listing.coords) { const icon L.icon({ iconUrl: https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png, shadowUrl: https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png, iconSize: 25, 41, iconAnchor: 12, 41, popupAnchor: 1, -34, shadowSize: 41, 41 }); const marker L.marker(listing.coords.lat, listing.coords.lon, { icon }) .bindPopup(` div stylecolor: #000; cursor: pointer; onclickshowListingDetail(${listing.id})> strong>${listing.shopName}/strong>br> ${listing.region}br> Β£${listing.price}/${listing.priceUnit || week}br> ${listing.contact}br> span stylecolor: #BF0A30; font-weight: 600;>Click for details β/span> /div> `) .addTo(map); markers.push(marker); } }); const noResults document.getElementById(mapNoResults); if (markers.length > 0) { const group L.featureGroup(markers); map.fitBounds(group.getBounds().pad(0.1)); noResults.style.display none; } else { map.setView(54.5, -2.5, 6); noResults.style.display block; } } function handleImageUpload(event) { const file event.target.files0; if (file) { if (file.size > 5 * 1024 * 1024) { alert(File size must be less than 5MB); event.target.value ; return; } if (!image/jpeg, image/jpg, image/png.includes(file.type)) { alert(Only JPG and PNG files are allowed); event.target.value ; return; } const reader new FileReader(); reader.onload function(e) { const img new Image(); img.onload function() { const canvas document.createElement(canvas); const maxWidth 1200; const maxHeight 800; let width img.width; let height img.height; if (width > maxWidth || height > maxHeight) { if (width > height) { height (height / width) * maxWidth; width maxWidth; } else { width (width / height) * maxHeight; height maxHeight; } } canvas.width width; canvas.height height; const ctx canvas.getContext(2d); ctx.drawImage(img, 0, 0, width, height); uploadedImage canvas.toDataURL(image/jpeg, 0.85); document.getElementById(previewImg).src uploadedImage; document.getElementById(imagePreview).style.display block; }; img.src e.target.result; }; reader.readAsDataURL(file); } } function usePlaceholder() { const listingType document.getElementById(listingType).value; let category; if (listingType offer) { category document.getElementById(productCategory).value; if (!category) { alert(Please select a product category first); document.getElementById(productCategory).focus(); return; } } else { category document.getElementById(primaryCategory).value; if (!category) { alert(Please select a category first); document.getElementById(primaryCategory).focus(); return; } } const placeholders { barbers: https://barber-marketplace-imgs-prod-205930644955.s3.eu-west-2.amazonaws.com/placeholders/Barber.png, hair: https://barber-marketplace-imgs-prod-205930644955.s3.eu-west-2.amazonaws.com/placeholders/Hair%20Salon.png, nails: https://barber-marketplace-imgs-prod-205930644955.s3.eu-west-2.amazonaws.com/placeholders/Nails.png, beauty: https://barber-marketplace-imgs-prod-205930644955.s3.eu-west-2.amazonaws.com/placeholders/Beauty.png, tools: https://barber-marketplace-imgs-prod-205930644955.s3.eu-west-2.amazonaws.com/placeholders/Tools.png, products: https://barber-marketplace-imgs-prod-205930644955.s3.eu-west-2.amazonaws.com/placeholders/Products.png, furniture: https://barber-marketplace-imgs-prod-205930644955.s3.eu-west-2.amazonaws.com/placeholders/Furniture.png, clothing: https://barber-marketplace-imgs-prod-205930644955.s3.eu-west-2.amazonaws.com/placeholders/Clothing.png, technology: https://barber-marketplace-imgs-prod-205930644955.s3.eu-west-2.amazonaws.com/placeholders/Technology.png, training: https://barber-marketplace-imgs-prod-205930644955.s3.eu-west-2.amazonaws.com/placeholders/Training.png, other: https://barber-marketplace-imgs-prod-205930644955.s3.eu-west-2.amazonaws.com/placeholders/Products.png }; uploadedImage placeholderscategory; document.getElementById(previewImg).src uploadedImage; document.getElementById(imagePreview).style.display block; } function handleEditImageUpload(event) { const file event.target.files0; if (file) { if (file.size > 5 * 1024 * 1024) { alert(File size must be less than 5MB); event.target.value ; return; } if (!image/jpeg, image/jpg, image/png.includes(file.type)) { alert(Only JPG and PNG files are allowed); event.target.value ; return; } const reader new FileReader(); reader.onload function(e) { const img new Image(); img.onload function() { const canvas document.createElement(canvas); const maxWidth 1200; const maxHeight 800; let width img.width; let height img.height; if (width > maxWidth || height > maxHeight) { if (width > height) { height (height / width) * maxWidth; width maxWidth; } else { width (width / height) * maxHeight; height maxHeight; } } canvas.width width; canvas.height height; const ctx canvas.getContext(2d); ctx.drawImage(img, 0, 0, width, height); editUploadedImage canvas.toDataURL(image/jpeg, 0.85); document.getElementById(editPreviewImg).src editUploadedImage; document.getElementById(editImagePreview).style.display block; }; img.src e.target.result; }; reader.readAsDataURL(file); } } function useEditPlaceholder() { const category document.getElementById(editPrimaryCategory).value; if (!category) { alert(Please select a category first); document.getElementById(editPrimaryCategory).focus(); return; } const placeholders { barbers: https://barber-marketplace-imgs-prod-205930644955.s3.eu-west-2.amazonaws.com/placeholders/Barber.png, hair: https://barber-marketplace-imgs-prod-205930644955.s3.eu-west-2.amazonaws.com/placeholders/Hair%20Salon.png, nails: https://barber-marketplace-imgs-prod-205930644955.s3.eu-west-2.amazonaws.com/placeholders/Nails.png, beauty: https://barber-marketplace-imgs-prod-205930644955.s3.eu-west-2.amazonaws.com/placeholders/Beauty.png }; editUploadedImage placeholderscategory; document.getElementById(editPreviewImg).src editUploadedImage; document.getElementById(editImagePreview).style.display block; } function filterByCategory(category) { if (category all) { filteredSeats ...seats; filteredOffers ...offers; } else if (chair, station, naildesk, treatmentroom, studio, holiday, business, fulltime, parttime.includes(category)) { filteredSeats seats.filter(s > s.category category); } else { filteredOffers offers.filter(o > o.productCategory category); } renderListings(); } function filterByPrimaryCategory(category) { if (category all) { filteredSeats ...seats; filteredOffers ...offers; } else { filteredSeats seats.filter(s > s.primaryCategory category); filteredOffers offers.filter(o > o.primaryCategory category); } renderListings(); if (map) initMap(); } function showCategory(category) { currentCategory category; currentSection seats; document.getElementById(seatsSection).style.display block; document.getElementById(offersSection).style.display none; Object.keys(categories).forEach(cat > { const tab document.getElementById(cat + Tab); if (tab) tab.classList.remove(active); }); const activeTab document.getElementById(category + Tab); if (activeTab) activeTab.classList.add(active); document.getElementById(offersTab).classList.remove(active); filterByPrimaryCategory(category); document.getElementById(mobileMenu).style.display none; } function showSection(section) { currentSection section; document.getElementById(seatsSection).style.display section seats ? block : none; document.getElementById(offersSection).style.display section offers ? block : none; Object.keys(categories).forEach(cat > { const tab document.getElementById(cat + Tab); if (tab) tab.classList.remove(active); }); document.getElementById(offersTab).classList.toggle(active, section offers); if (section offers) { filteredOffers ...offers; renderListings(); } document.getElementById(mobileMenu).style.display none; } async function loadListings() { showSpinner(); try { const response await fetch(`${API_BASE}/listings`); const data await response.json(); // Filter out expired listings const now new Date(); const activeListings data.filter(l > { if (!l.expiryDate) return true; const day, month, year l.expiryDate.split(/); const expiry new Date(year, month - 1, day); expiry.setHours(23, 59, 59, 999); return expiry > now; }); seats activeListings.filter(l > l.type seat); offers activeListings.filter(l > l.type offer); filteredSeats ...seats; filteredOffers ...offers; renderListings(); // Check for shared listing link after data is loaded const params new URLSearchParams(window.location.search); const sharedListingId params.get(listing); if (sharedListingId) { setTimeout(() > { showListingDetail(sharedListingId); // Clean URL after modal opens window.history.replaceState({}, document.title, window.location.pathname + window.location.hash); }, 100); } } catch (err) { console.error(Error loading listings:, err); alert(Failed to load listings. Please refresh the page.); } finally { hideSpinner(); } } function init() { if (currentUser) { fetch(`${API_BASE}/profile/${encodeURIComponent(currentUser.email)}`) .then(res > res.json()) .then(data > { if (data.name) { currentUser { ...currentUser, ...data }; localStorage.setItem(currentUser, JSON.stringify(currentUser)); } }) .catch(err > console.log(Profile fetch:, err)); document.getElementById(loginBtn).style.display none; document.getElementById(profileBtn).style.display block; document.getElementById(profileBtn).textContent currentUser.name; document.getElementById(mobileLogin).style.display none; document.getElementById(mobileProfile).style.display block; document.getElementById(mobileProfile).textContent currentUser.name; document.getElementById(logoutBtn).style.display block; document.getElementById(mobileLogout).style.display block; if (currentUser.type barber) { document.getElementById(postSeatBtn).style.display block; document.getElementById(postOfferBtn).style.display none; } else { document.getElementById(postSeatBtn).style.display none; document.getElementById(postOfferBtn).style.display block; } } // Check for Stripe success redirect const params new URLSearchParams(window.location.search); const sessionId params.get(session_id); const listingId params.get(listing_id); const renewId params.get(renew); if (sessionId && listingId) { // Retrieve pending listing from localStorage const pendingListing localStorage.getItem(pendingListing_ + listingId); if (pendingListing) { const listing JSON.parse(pendingListing); listing.featured true; saveListing(listing).then((result) > { if (result) { alert(Payment successful! Your featured listing has been posted.); } }); localStorage.removeItem(pendingListing_ + listingId); // Clean URL window.history.replaceState({}, document.title, window.location.pathname + window.location.hash); } } // Check for renewal link if (renewId) { setTimeout(() > { const listing ...seats, ...offers.find(l > l.id renewId); if (listing && listing.postedBy currentUser?.email) { if (confirm(`Renew ${listing.shopName} for 30 more days?`)) { renewListing(renewId); } } window.history.replaceState({}, document.title, window.location.pathname + window.location.hash); }, 500); } loadListings(); handleRoute(); } function toggleMenu() { const menu document.getElementById(mobileMenu); menu.style.display menu.style.display block ? none : block; } function showListingDetail(id) { const listing ...seats, ...offers.find(l > l.id id); if (!listing) { console.error(Listing not found:, id); console.log(Available IDs:, ...seats, ...offers.map(l > l.id)); alert(Listing not found. It may have expired or been removed.); return; } const categoryLabels { chair: listing.primaryCategory barbers ? πΊ Barber Chair : πΊ Chair, station: πͺ Station, naildesk: π Nail Desk, treatmentroom: ποΈ Treatment Room, studio: π Studio / Room, holiday: ποΈ Holiday Cover, fulltime: πΌ Full Time Job, parttime: β° Part Time Job, business: πΌ Business for Sale, business: πΌ Business for Sale }; document.getElementById(listingDetailContent).innerHTML ` span classclose onclickcloseModal(listingDetailModal)>×/span> ${listing.image ? `div classlisting-detail-image>img src${listing.image} alt${listing.shopName}>/div>` : } div classlisting-detail-content> ${listing.featured ? div stylebackground: #FFD700; color: #000; padding: 0.5rem 1rem; border-radius: 8px; display: inline-block; font-weight: 800; margin-bottom: 1rem;>β FEATURED LISTING/div> : } div classlisting-detail-title>${listing.shopName}/div> div classlisting-detail-location>π ${listing.region}/div> ${listing.primaryCategory ? `div styledisplay: inline-block; background: #333; color: #fff; padding: 0.4rem 0.8rem; border-radius: 20px; font-size: 0.9rem; margin-bottom: 0.75rem; text-transform: capitalize;>${listing.primaryCategory}/div>` : } ${listing.category ? `div styledisplay: inline-block; background: #002868; color: #fff; padding: 0.4rem 0.8rem; border-radius: 20px; font-size: 0.9rem; margin-bottom: 1rem; margin-left: 0.5rem;>${categoryLabelslisting.category || listing.category}/div>` : } div classlisting-detail-price>${listing.category business ? Price on Enquiry : Β£ + listing.price + / + (listing.priceUnit || week)}/div> div classlisting-detail-description>${listing.description}/div> ${listing.expiryDate ? `div stylecolor: #666; margin-bottom: 1.5rem; font-size: 0.9rem;>Listing expires: ${listing.expiryDate}/div>` : } div stylebackground: #000; border: 2px solid #222; border-radius: 8px; padding: 1.5rem; margin-bottom: 1rem;> div stylecolor: #fff; font-size: 1.1rem; font-weight: 700; margin-bottom: 1rem;>π ${listing.contact}/div> div styledisplay: flex; gap: 0.75rem;> button onclickshareListing(${listing.id}) styleflex: 1; background: #002868; color: #fff; padding: 0.75rem; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 0.95rem;>π Share/button> button onclickreportListing(${listing.id}, ${listing.shopName}) styleflex: 1; background: #333; color: #fff; padding: 0.75rem; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 0.95rem;>π© Report/button> /div> /div> /div> `; document.getElementById(listingDetailModal).style.display block; } function shareListing(id) { const url `${window.location.origin}${window.location.pathname}?listing${id}`; navigator.clipboard.writeText(url).then(() > { alert(β Link copied to clipboard!); }).catch(() > { prompt(Copy this link:, url); }); } async function reportListing(id, shopName) { const reason prompt(Why are you reporting this listing?\n\nCommon reasons:\n- Inappropriate content\n- Misleading information\n- Spam\n- Copyright violation\n- Other); if (!reason || !reason.trim()) return; try { await fetch(`${API_BASE}/report`, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ listingId: id, shopName: shopName, reason: reason.trim(), reportedBy: currentUser?.email || Anonymous, url: `${window.location.origin}${window.location.pathname}?listing${id}` }) }); alert(β Report submitted. We\ll review this listing within 24 hours.); } catch (err) { console.error(Report error:, err); alert(β Unable to submit report. Please email info@delimiter.co.uk); } } function renderListings() { const seatsGrid document.getElementById(seatsGrid); const offersGrid document.getElementById(offersGrid); document.getElementById(seatsCount).textContent `${filteredSeats.length} seats found`; document.getElementById(offersCount).textContent `${filteredOffers.length} offers found`; seatsGrid.innerHTML filteredSeats.length ? filteredSeats.map(l > ` div classlisting-card${l.featured ? featured : } onclickshowListingDetail(${l.id})> div classlisting-image> ${l.image ? `img src${l.image} alt${l.shopName}>` : div classplaceholder>π/div>} div classlisting-price>${l.category business ? On Enquiry : Β£ + l.price + / + (l.priceUnit || week)}/div> ${l.featured ? div styleposition: absolute; top: 1rem; left: 1rem; background: #FFD700; color: #000; padding: 0.5rem 1rem; border-radius: 8px; font-weight: 800; font-size: 0.9rem;>β FEATURED/div> : } /div> div classlisting-content> div classlisting-title>${l.shopName}/div> div classlisting-location>π ${l.region}/div> ${l.primaryCategory ? `div styledisplay: inline-block; background: #333; color: #fff; padding: 0.3rem 0.8rem; border-radius: 15px; font-size: 0.85rem; margin-bottom: 0.5rem; text-transform: capitalize; width: fit-content;>${l.primaryCategory}/div>` : } div classlisting-description>${l.description}/div> div classlisting-meta> span classlisting-type stylebackground: ${l.category chair ? #002868 : l.category station ? #002868 : l.category naildesk ? #9b59b6 : l.category treatmentroom ? #e91e63 : l.category studio ? #ff9800 : l.category holiday ? #9b59b6 : l.category fulltime ? #16a085 : l.category parttime ? #27ae60 : #28a745}; color: #fff;>${l.category chair ? (l.primaryCategory barbers ? πΊ Barber Chair : πΊ Chair) : l.category station ? πͺ Station : l.category naildesk ? π Nail Desk : l.category treatmentroom ? ποΈ Treatment Room : l.category studio ? π Studio / Room : l.category holiday ? ποΈ Holiday Cover : l.category fulltime ? πΌ Full Time Job : l.category parttime ? β° Part Time Job : πΌ Business for Sale}/span> span classlisting-contact stylecolor: #fff;>π ${l.contact}/span> /div> ${l.expiryDate ? `div stylecolor: #999; font-size: 0.85rem; margin-top: 0.5rem;>Expires: ${l.expiryDate}/div>` : } /div> /div> `).join() : div stylegrid-column: 1/-1; text-align: center; padding: 4rem 2rem;>div stylefont-size: 4rem; margin-bottom: 1rem;>π/div>h3 stylecolor: #999; font-size: 1.5rem; margin-bottom: 0.5rem;>No listings found/h3>p stylecolor: #666;>Try adjusting your filters or search in a different area/p>/div>; offersGrid.innerHTML filteredOffers.length ? filteredOffers.map(l > ` div classlisting-card> div classlisting-image> ${l.image ? `img src${l.image} alt${l.shopName}>` : div classplaceholder>π/div>} div classlisting-price>Β£${l.price}/div> /div> div classlisting-content> div classlisting-title>${l.shopName}/div> div classlisting-location>π ${l.region}/div> ${l.primaryCategory ? `div styledisplay: inline-block; background: #333; color: #fff; padding: 0.3rem 0.8rem; border-radius: 15px; font-size: 0.85rem; margin-bottom: 0.5rem; text-transform: capitalize; width: fit-content;>${l.primaryCategory}/div>` : } div classlisting-description>${l.description}/div> div classlisting-meta> span classlisting-type stylebackground: #BF0A30;>Product Offer/span> span classlisting-contact stylecolor: #fff;>π§ ${l.contact}/span> /div> ${l.orderUrl ? `a href${l.orderUrl} target_blank styledisplay: block; background: #BF0A30; color: #fff; text-align: center; padding: 0.75rem; border-radius: 8px; text-decoration: none; margin-top: 1rem; font-weight: 600;>Order Now β/a>` : } ${l.expiryDate ? `div stylecolor: #999; font-size: 0.85rem; margin-top: 0.5rem;>Expires: ${l.expiryDate}/div>` : } /div> /div> `).join() : div stylegrid-column: 1/-1; text-align: center; padding: 4rem 2rem;>div stylefont-size: 4rem; margin-bottom: 1rem;>π/div>h3 stylecolor: #999; font-size: 1.5rem; margin-bottom: 0.5rem;>No offers found/h3>p stylecolor: #666;>Check back soon for new product offers/p>/div>; } async function getPostcodeCoords(postcode) { const cleanPostcode postcode.replace(/\s/g, ); try { const response await fetch(`https://api.postcodes.io/postcodes/${cleanPostcode}`); const data await response.json(); if (data.status 200 && data.result) { return { lat: data.result.latitude, lon: data.result.longitude }; } } catch (err) { console.error(Postcode lookup error:, err); } return null; } function calculateDistance(lat1, lon1, lat2, lon2) { const R 3959; // Earth radius in miles const dLat (lat2 - lat1) * Math.PI / 180; const dLon (lon2 - lon1) * Math.PI / 180; const a Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon/2) * Math.sin(dLon/2); const c 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); return R * c; } async function searchListings() { const postcode document.getElementById(searchPostcode).value.trim().toUpperCase(); const workspaceType document.getElementById(seatCategoryFilter).value; const radius parseInt(document.getElementById(searchRadius).value); let searchCoords null; if (postcode) { searchCoords await getPostcodeCoords(postcode); if (!searchCoords) { alert(Invalid postcode. Please enter a valid UK postcode.); return; } } const filterWithDistance async (listings) > { const results ; for (const l of listings) { let matchPostcode true; if (postcode && searchCoords && l.region) { if (!l.coords) { l.coords await getPostcodeCoords(l.region); } if (l.coords) { const distance calculateDistance( searchCoords.lat, searchCoords.lon, l.coords.lat, l.coords.lon ); matchPostcode distance radius; } else { matchPostcode false; } } const matchCategory l.primaryCategory currentCategory; const matchWorkspace workspaceType all || l.category workspaceType; if (matchPostcode && matchCategory && (l.type offer || matchWorkspace)) { results.push(l); } } return results; }; filteredSeats await filterWithDistance(seats); filteredOffers await filterWithDistance(offers); renderListings(); if (map) initMap(); } function clearFilters() { document.getElementById(searchPostcode).value ; document.getElementById(seatCategoryFilter).value all; document.getElementById(searchRadius).value 5; filterByPrimaryCategory(currentCategory); } function updateAdPrice() { const adType document.getElementById(adType).value; const info document.getElementById(adPriceInfo); const prices { basic: Β£15 for 30 days, featured: Β£35 for 30 days - Highlighted at top of search, spotlight: Β£75 for 7 days - City spotlight position }; if (adType && pricesadType) { info.textContent pricesadType; info.style.display block; } else { info.style.display none; } } function updateFeaturedPrice() { const featured document.getElementById(featuredOption).value; const days document.getElementById(expiryDays).value; const info document.getElementById(featuredPriceInfo); const btn document.getElementById(submitListingBtn); if (featured featured && days) { const cost parseInt(days); info.textContent `Total Cost: Β£${cost} (Β£1 Γ ${days} days)`; info.style.display block; btn.textContent Continue to Payment; } else { info.style.display none; btn.textContent Post Listing; } } document.getElementById(loginBtn).onclick openAuthModal; document.getElementById(mobileLogin).onclick openAuthModal; document.getElementById(postAdBtn).onclick () > { if (!currentUser) { alert(Please login to post an ad); openAuthModal(); return; } resetPostForm(); if (currentUser.type barber) { document.getElementById(listingType).value seat; document.getElementById(seatCategory).style.display block; document.getElementById(featuredOption).style.display block; document.getElementById(priceContainer).style.display grid; document.getElementById(productPrice).style.display none; } else { document.getElementById(listingType).value offer; document.getElementById(seatCategory).style.display none; document.getElementById(primaryCategory).style.display none; document.getElementById(productCategory).style.display block; document.getElementById(offerType).style.display block; document.getElementById(orderUrl).style.display block; document.getElementById(priceContainer).style.display none; document.getElementById(productPrice).style.display block; document.getElementById(featuredOption).style.display none; document.getElementById(featuredOption).value featured; updateFeaturedPrice(); } if (currentUser.shopName) document.getElementById(shopName).value currentUser.shopName; if (currentUser.location) document.getElementById(region).value currentUser.location; if (currentUser.contact) document.getElementById(contact).value currentUser.contact; if (currentUser.profilePicture) { uploadedImage currentUser.profilePicture; document.getElementById(previewImg).src uploadedImage; document.getElementById(imagePreview).style.display block; } document.getElementById(postModal).style.display block; }; document.getElementById(mobilePostAd).onclick () > { if (!currentUser) { alert(Please login to post an ad); openAuthModal(); return; } resetPostForm(); if (currentUser.type barber) { document.getElementById(listingType).value seat; document.getElementById(seatCategory).style.display block; document.getElementById(featuredOption).style.display block; document.getElementById(priceContainer).style.display grid; document.getElementById(productPrice).style.display none; } else { document.getElementById(listingType).value offer; document.getElementById(seatCategory).style.display none; document.getElementById(primaryCategory).style.display none; document.getElementById(productCategory).style.display block; document.getElementById(offerType).style.display block; document.getElementById(orderUrl).style.display block; document.getElementById(priceContainer).style.display none; document.getElementById(productPrice).style.display block; document.getElementById(featuredOption).style.display none; document.getElementById(featuredOption).value featured; updateFeaturedPrice(); } if (currentUser.shopName) document.getElementById(shopName).value currentUser.shopName; if (currentUser.location) document.getElementById(region).value currentUser.location; if (currentUser.contact) document.getElementById(contact).value currentUser.contact; if (currentUser.profilePicture) { uploadedImage currentUser.profilePicture; document.getElementById(previewImg).src uploadedImage; document.getElementById(imagePreview).style.display block; } document.getElementById(postModal).style.display block; }; document.getElementById(postSeatBtn).onclick () > { if (currentUser.type ! barber) { alert(Only Spaces & Jobs advertisers can post workspace listings); return; } resetPostForm(); document.getElementById(listingType).value seat; document.getElementById(seatCategory).style.display block; if (currentUser.shopName) document.getElementById(shopName).value currentUser.shopName; if (currentUser.location) document.getElementById(region).value currentUser.location; if (currentUser.contact) document.getElementById(contact).value currentUser.contact; if (currentUser.profilePicture) { uploadedImage currentUser.profilePicture; document.getElementById(previewImg).src uploadedImage; document.getElementById(imagePreview).style.display block; } document.getElementById(postModal).style.display block; }; document.getElementById(postOfferBtn).onclick () > { if (currentUser.type ! retailer) { alert(Only Products & Offers advertisers can post product offers); return; } resetPostForm(); document.getElementById(listingType).value offer; document.getElementById(seatCategory).style.display none; document.getElementById(primaryCategory).style.display none; document.getElementById(productCategory).style.display block; document.getElementById(offerType).style.display block; document.getElementById(orderUrl).style.display block; document.getElementById(priceContainer).style.display none; document.getElementById(productPrice).style.display block; document.getElementById(featuredOption).style.display none; document.getElementById(featuredOption).value featured; if (currentUser.shopName) document.getElementById(shopName).value currentUser.shopName; if (currentUser.location) document.getElementById(region).value currentUser.location; if (currentUser.contact) document.getElementById(contact).value currentUser.contact; if (currentUser.profilePicture) { uploadedImage currentUser.profilePicture; document.getElementById(previewImg).src uploadedImage; document.getElementById(imagePreview).style.display block; } updateFeaturedPrice(); document.getElementById(postModal).style.display block; }; function updateEditWorkspaceOptions() { const category document.getElementById(editPrimaryCategory).value; const seatCategory document.getElementById(editSeatCategory); if (!category || !categoriescategory) { seatCategory.innerHTML ` option value>Select Workspace Type/option> option valuechair>Chair/option> option valuestation>Station/option> option valuenaildesk>Nail Desk/option> option valuetreatmentroom>Treatment Room/option> option valuestudio>Studio / Room/option> option valueholiday>Holiday Cover/option> option valuebusiness>Business for Sale/option> option valuefulltime>Full Time Job/option> option valueparttime>Part Time Job/option> `; return; } const workspaces categoriescategory.workspaces; const workspaceLabels { chair: Chair, station: Station, naildesk: Nail Desk, treatmentroom: Treatment Room, studio: Studio / Room, holiday: Holiday Cover, business: Business for Sale, fulltime: Full Time Job, parttime: Part Time Job }; seatCategory.innerHTML option value>Select Workspace Type/option> + workspaces.map(w > `option value${w}>${workspaceLabelsw}/option>`).join(); } function showMyAds() { if (!currentUser) { alert(Please login first); return; } document.getElementById(seatsSection).style.display none; document.getElementById(offersSection).style.display none; document.getElementById(myAdsSection).style.display block; Object.keys(categories).forEach(cat > { const tab document.getElementById(cat + Tab); if (tab) tab.classList.remove(active); }); document.getElementById(offersTab).classList.remove(active); document.getElementById(mobileMenu).style.display none; const myListings ...seats, ...offers.filter(l > l.postedBy currentUser.email); const now new Date(); const activeListings myListings.filter(l > { if (!l.expiryDate) return true; const day, month, year l.expiryDate.split(/); const expiry new Date(year, month - 1, day); return expiry > now; }); document.getElementById(myAdsCount).textContent `${activeListings.length} listings found`; const myAdsGrid document.getElementById(myAdsGrid); myAdsGrid.innerHTML activeListings.length ? activeListings.map(l > ` div classlisting-card${l.featured ? featured : }> div classlisting-image> ${l.image ? `img src${l.image} alt${l.shopName}>` : div classplaceholder>π/div>} div classlisting-price>${l.category business ? On Enquiry : Β£ + l.price + / + (l.priceUnit || week)}/div> ${l.featured ? div styleposition: absolute; top: 1rem; left: 1rem; background: #FFD700; color: #000; padding: 0.5rem 1rem; border-radius: 8px; font-weight: 800; font-size: 0.9rem;>β FEATURED/div> : } /div> div classlisting-content> div classlisting-title>${l.shopName}/div> div classlisting-location>π ${l.region}/div> ${l.primaryCategory ? `div styledisplay: inline-block; background: #333; color: #fff; padding: 0.3rem 0.8rem; border-radius: 15px; font-size: 0.85rem; margin-bottom: 0.5rem; text-transform: capitalize; width: fit-content;>${l.primaryCategory}/div>` : } div classlisting-description>${l.description}/div> div styledisplay: flex; gap: 0.5rem; margin-top: 1rem;> button classbtn-search onclickeditListing(${l.id}) styleflex: 1; padding: 0.75rem;>Edit/button> button classbtn-search onclickdeleteListing(${l.id}) styleflex: 1; padding: 0.75rem; background: #BF0A30;>Delete/button> /div> ${l.expiryDate ? `div stylecolor: #999; font-size: 0.85rem; margin-top: 0.5rem;>Expires: ${l.expiryDate}/div>` : } /div> /div> `).join() : p stylecolor: #666; text-align: center; grid-column: 1/-1;>You have no active listings./p>; } function editListing(id) { const listing ...seats, ...offers.find(l > l.id id); if (!listing) return; document.getElementById(editListingId).value id; document.getElementById(editPrimaryCategory).value listing.primaryCategory || ; document.getElementById(editSeatCategory).value listing.category || ; document.getElementById(editShopName).value listing.shopName; document.getElementById(editRegion).value listing.region; document.getElementById(editPrice).value listing.price; document.getElementById(editPriceUnit).value listing.priceUnit || week; document.getElementById(editDescription).value listing.description; document.getElementById(editContact).value listing.contact; editUploadedImage null; document.getElementById(editImageUpload).value ; if (listing.image) { document.getElementById(editPreviewImg).src listing.image; document.getElementById(editImagePreview).style.display block; } else { document.getElementById(editImagePreview).style.display none; } updateEditWorkspaceOptions(); document.getElementById(editModal).style.display block; } async function updateListing() { const id document.getElementById(editListingId).value; const listing ...seats, ...offers.find(l > l.id id); if (!listing) return; listing.primaryCategory document.getElementById(editPrimaryCategory).value; listing.category document.getElementById(editSeatCategory).value; listing.shopName document.getElementById(editShopName).value; listing.region document.getElementById(editRegion).value; listing.price document.getElementById(editPrice).value; listing.priceUnit document.getElementById(editPriceUnit).value; listing.description document.getElementById(editDescription).value; listing.contact document.getElementById(editContact).value; // Handle image update if (editUploadedImage) { if (!editUploadedImage.startsWith(https://barber-marketplace-imgs)) { try { const presignedResponse await fetch(`${API_BASE}/upload`, { method: GET }); const { uploadUrl, publicUrl } await presignedResponse.json(); const base64Data editUploadedImage.split(,)1; const byteCharacters atob(base64Data); const byteNumbers new Array(byteCharacters.length); for (let i 0; i byteCharacters.length; i++) { byteNumbersi byteCharacters.charCodeAt(i); } const byteArray new Uint8Array(byteNumbers); const blob new Blob(byteArray, { type: image/jpeg }); await fetch(uploadUrl, { method: PUT, body: blob, headers: { Content-Type: image/jpeg } }); listing.image publicUrl; } catch (err) { console.error(Image upload error:, err); } } else { listing.image editUploadedImage; } } try { const response await fetch(`${API_BASE}/listings/${id}`, { method: PUT, headers: { Content-Type: application/json }, body: JSON.stringify(listing) }); if (response.ok) { closeModal(editModal); await loadListings(); showMyAds(); alert(Listing updated successfully!); } else { throw new Error(Failed to update listing); } } catch (err) { console.error(Error updating listing:, err); alert(Error updating listing. Please try again.); } } async function deleteListing(id) { if (!confirm(Are you sure you want to delete this listing?)) return; try { const response await fetch(`${API_BASE}/listings/${id}`, { method: DELETE }); if (response.ok) { await loadListings(); showMyAds(); alert(Listing deleted successfully!); } else { throw new Error(Failed to delete listing); } } catch (err) { console.error(Error deleting listing:, err); alert(Error deleting listing. Please try again.); } } async function renewListing(id) { try { const listing ...seats, ...offers.find(l > l.id id); if (!listing) return; const newExpiry new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toLocaleDateString(en-GB); listing.expiryDate newExpiry; const response await fetch(`${API_BASE}/listings/${id}`, { method: PUT, headers: { Content-Type: application/json }, body: JSON.stringify(listing) }); if (response.ok) { await loadListings(); alert(`Listing renewed until ${newExpiry}`); } else { throw new Error(Failed to renew listing); } } catch (err) { console.error(Error renewing listing:, err); alert(Error renewing listing. Please try again.); } } function updateWorkspaceOptions() { const category document.getElementById(primaryCategory).value; const seatCategory document.getElementById(seatCategory); if (!category || !categoriescategory) { seatCategory.innerHTML ` option value>Select Workspace Type/option> option valuechair>Chair/option> option valuestation>Station/option> option valuenaildesk>Nail Desk/option> option valuetreatmentroom>Treatment Room/option> option valuestudio>Studio / Room/option> option valueholiday>Holiday Cover/option> option valuebusiness>Business for Sale/option> option valuefulltime>Full Time Job/option> option valueparttime>Part Time Job/option> `; return; } const workspaces categoriescategory.workspaces; const workspaceLabels { chair: Chair, station: Station, naildesk: Nail Desk, treatmentroom: Treatment Room, studio: Studio / Room, holiday: Holiday Cover, business: Business for Sale, fulltime: Full Time Job, parttime: Part Time Job }; seatCategory.innerHTML option value>Select Workspace Type/option> + workspaces.map(w > `option value${w}>${workspaceLabelsw}/option>`).join(); } function closeModal(id) { document.getElementById(id).style.display none; if (id postModal) { resetPostForm(); } } function resetPostForm() { document.getElementById(listingType).value ; document.getElementById(seatCategory).value ; document.getElementById(primaryCategory).value ; document.getElementById(featuredOption).value standard; document.getElementById(shopName).value ; document.getElementById(region).value ; document.getElementById(price).value ; document.getElementById(priceUnit).value week; document.getElementById(expiryDays).value ; document.getElementById(description).value ; document.getElementById(contact).value ; document.getElementById(orderUrl).value ; document.getElementById(imageUpload).value ; document.getElementById(imagePreview).style.display none; document.getElementById(featuredPriceInfo).style.display none; uploadedImage null; } function toggleFilters() { const container document.getElementById(searchContainer); const btn document.querySelector(.filter-toggle); container.classList.toggle(show); btn.textContent container.classList.contains(show) ? β Hide Filters : π Show Filters; } let isSignupMode false; function toggleAuthMode() { isSignupMode !isSignupMode; document.getElementById(authTitle).textContent isSignupMode ? Register to Post Listings : Login; document.getElementById(signupPhotoSection).style.display isSignupMode ? block : none; document.getElementById(userName).style.display isSignupMode ? block : none; document.getElementById(userShopName).style.display isSignupMode ? block : none; document.getElementById(userLocation).style.display isSignupMode ? block : none; document.getElementById(userContact).style.display isSignupMode ? block : none; document.getElementById(userType).style.display isSignupMode ? block : none; document.getElementById(signupShopType).style.display none; document.getElementById(signupServicesSection).style.display none; document.getElementById(termsLabel).style.display isSignupMode ? block : none; document.getElementById(authBtn).textContent isSignupMode ? Create Account : Login; document.getElementById(forgotPasswordLink).style.display isSignupMode ? none : block; document.getElementById(authSwitchText).textContent isSignupMode ? Already have an account? : Dont have an account?; document.getElementById(authSwitchLink).textContent isSignupMode ? Login : Create Account; } function openAuthModal() { isSignupMode false; signupProfilePicture null; document.getElementById(authTitle).textContent Login; document.getElementById(signupPhotoSection).style.display none; document.getElementById(userName).style.display none; document.getElementById(userShopName).style.display none; document.getElementById(userLocation).style.display none; document.getElementById(userContact).style.display none; document.getElementById(userType).style.display none; document.getElementById(signupShopType).style.display none; document.getElementById(termsLabel).style.display none; document.getElementById(authBtn).textContent Login; document.getElementById(forgotPasswordLink).style.display block; document.getElementById(authSwitchText).textContent Dont have an account?; document.getElementById(authSwitchLink).textContent Create Account; document.getElementById(loginModal).style.display block; } function forgotPassword() { const email document.getElementById(userEmail).value.trim().toLowerCase().replace(/\s+/g, ); if (!email) { alert(Please enter your email address); return; } const userData { Username: email, Pool: userPool }; const cognitoUser new AmazonCognitoIdentity.CognitoUser(userData); cognitoUser.forgotPassword({ onSuccess: () > { const code prompt(Enter the verification code sent to your email:); if (!code) return; const newPassword prompt(Enter your new password (min 8 characters):); if (!newPassword || newPassword.length 8) { alert(Password must be at least 8 characters); return; } cognitoUser.confirmPassword(code, newPassword, { onSuccess: () > alert(Password reset successfully! You can now login.), onFailure: (err) > alert(Error: + err.message) }); }, onFailure: (err) > alert(Error: + err.message) }); } function login() { const email document.getElementById(userEmail).value.trim().toLowerCase().replace(/\s+/g, ); const password document.getElementById(userPassword).value; if (!email || !password) { alert(Please enter both email and password); return; } if (!email.includes(@) || !email.includes(.)) { alert(Please enter a valid email address); return; } if (isSignupMode) { const name document.getElementById(userName).value.trim(); const shopName document.getElementById(userShopName).value.trim(); const location document.getElementById(userLocation).value.trim(); const contact document.getElementById(userContact).value.trim(); const type document.getElementById(userType).value; const servicesCheckboxes document.querySelectorAll(#signupServicesCheckboxes inputtypecheckbox:checked); const services Array.from(servicesCheckboxes).map(cb > cb.value).join(, ); const termsAccepted document.getElementById(termsCheckbox).checked; if (!name) { alert(Please enter your name); return; } if (!shopName) { alert(Please enter your shop/company name); return; } if (!location) { alert(Please enter your location); return; } if (!contact) { alert(Please enter your contact details); return; } if (!type) { alert(Please select your account type); return; } if (password.length 8) { alert(Password must be at least 8 characters); return; } if (!termsAccepted) { alert(Please accept the Terms & Conditions); return; } // Store profile data for after verification localStorage.setItem(pendingProfile_ + email, JSON.stringify({ name, shopName, location, contact, type, services, profilePicture: signupProfilePicture })); // Cognito Sign Up const attributeList new AmazonCognitoIdentity.CognitoUserAttribute({ Name: email, Value: email }), new AmazonCognitoIdentity.CognitoUserAttribute({ Name: name, Value: name }), new AmazonCognitoIdentity.CognitoUserAttribute({ Name: custom:userType, Value: type }) ; userPool.signUp(email, password, attributeList, null, (err, result) > { if (err) { if (err.code UsernameExistsException) { alert(Account already exists. Please login or use Forgot Password if you need to reset.); } else { alert(Signup error: + err.message); } return; } alert(Verification code sent to your email. Please check your inbox.); // Store password for auto-login after verification document.getElementById(userPassword).value password; // Show verification code input document.getElementById(authTitle).textContent Verify Email; document.getElementById(userName).style.display none; document.getElementById(userShopName).style.display none; document.getElementById(userLocation).style.display none; document.getElementById(userContact).style.display none; document.getElementById(userType).style.display none; document.getElementById(userEmail).disabled true; document.getElementById(userPassword).style.display none; document.getElementById(termsLabel).style.display none; document.getElementById(userPassword).insertAdjacentHTML(afterend, input typetext idverificationCode placeholderVerification Code stylemargin-bottom: 1rem;>); const form document.querySelector(#loginModal form); form.onsubmit (e) > { e.preventDefault(); verifyEmail(email); }; document.getElementById(authBtn).textContent Verify; document.getElementById(authSwitchText).style.display none; document.querySelector(#loginModal p:last-child).style.display none; }); } else { // Cognito Sign In const authenticationData { Username: email, Password: password }; const authenticationDetails new AmazonCognitoIdentity.AuthenticationDetails(authenticationData); const userData { Username: email, Pool: userPool }; const cognitoUser new AmazonCognitoIdentity.CognitoUser(userData); cognitoUser.authenticateUser(authenticationDetails, { onSuccess: (result) > { cognitoUser.getUserAttributes((err, attributes) > { if (err) { alert(Error getting user data: + err.message); return; } const name attributes.find(attr > attr.Name name)?.Value || email.split(@)0; const type attributes.find(attr > attr.Name custom:userType)?.Value || barber; currentUser { name, email, type }; localStorage.setItem(currentUser, JSON.stringify(currentUser)); closeModal(loginModal); init(); }); }, onFailure: (err) > { alert(Login error: + err.message); } }); } } function verifyEmail(email) { const code document.getElementById(verificationCode).value; if (!code) { alert(Please enter verification code); return; } const userData { Username: email, Pool: userPool }; const cognitoUser new AmazonCognitoIdentity.CognitoUser(userData); cognitoUser.confirmRegistration(code, true, (err, result) > { if (err) { alert(Verification error: + err.message); return; } alert(Email verified successfully! Logging you in...); // Auto-login after verification const password document.getElementById(userPassword).value; const authenticationData { Username: email, Password: password }; const authenticationDetails new AmazonCognitoIdentity.AuthenticationDetails(authenticationData); cognitoUser.authenticateUser(authenticationDetails, { onSuccess: (result) > { cognitoUser.getUserAttributes((err, attributes) > { if (err) { location.reload(); return; } const name attributes.find(attr > attr.Name name)?.Value || email.split(@)0; const type attributes.find(attr > attr.Name custom:userType)?.Value || barber; currentUser { name, email, type }; localStorage.setItem(currentUser, JSON.stringify(currentUser)); // Create profile from pending data const pendingProfile localStorage.getItem(pendingProfile_ + email); if (pendingProfile) { const profileData JSON.parse(pendingProfile); fetch(`${API_BASE}/profile/${encodeURIComponent(email)}`, { method: PUT, headers: { Content-Type: application/json }, body: JSON.stringify(profileData) }).then(() > { localStorage.removeItem(pendingProfile_ + email); closeModal(loginModal); location.reload(); }).catch(() > { closeModal(loginModal); location.reload(); }); } else { closeModal(loginModal); location.reload(); } }); }, onFailure: (err) > { location.reload(); } }); }); } function logout() { const cognitoUser userPool.getCurrentUser(); if (cognitoUser) { cognitoUser.signOut(); } localStorage.removeItem(currentUser); currentUser null; location.reload(); } function showProfileTab(tab) { if (tab details) { document.getElementById(profileDetailsSection).style.display block; document.getElementById(profileAdsSection).style.display none; document.getElementById(profileDetailsTab).style.background #002868; document.getElementById(profileAdsTab).style.background #333; } else { document.getElementById(profileDetailsSection).style.display none; document.getElementById(profileAdsSection).style.display block; document.getElementById(profileDetailsTab).style.background #333; document.getElementById(profileAdsTab).style.background #002868; loadMyAdsInProfile(); } } function loadMyAdsInProfile() { const myListings ...seats, ...offers.filter(l > l.postedBy currentUser.email); const now new Date(); const activeListings myListings.filter(l > { if (!l.expiryDate) return true; const day, month, year l.expiryDate.split(/); const expiry new Date(year, month - 1, day); return expiry > now; }); const profileAdsGrid document.getElementById(profileAdsGrid); profileAdsGrid.innerHTML activeListings.length ? activeListings.map(l > ` div stylebackground: #000; border: 2px solid #222; border-radius: 8px; padding: 1rem;> div stylefont-weight: 700; font-size: 1.1rem; margin-bottom: 0.5rem;>${l.shopName}/div> div stylecolor: #999; margin-bottom: 0.5rem;>π ${l.region} | Β£${l.price}/${l.priceUnit || week}/div> div stylecolor: #ccc; margin-bottom: 0.5rem; font-size: 0.9rem;>${l.description.substring(0, 100)}.../div> ${l.featured ? div stylecolor: #FFD700; font-size: 0.85rem; margin-bottom: 0.5rem;>β FEATURED/div> : } div styledisplay: flex; gap: 0.5rem; margin-top: 0.5rem;> button classbtn-search onclickeditListing(${l.id}) styleflex: 1; padding: 0.5rem; font-size: 0.9rem;>Edit/button> button classbtn-search onclickdeleteListing(${l.id}) styleflex: 1; padding: 0.5rem; font-size: 0.9rem; background: #BF0A30;>Delete/button> /div> ${l.expiryDate ? `div stylecolor: #666; font-size: 0.8rem; margin-top: 0.5rem;>Expires: ${l.expiryDate}/div>` : } /div> `).join() : p stylecolor: #666; text-align: center;>You have no active listings./p>; } function showProfile() { document.getElementById(profileName).value currentUser.name; document.getElementById(profileShopName).value currentUser.shopName || ; document.getElementById(profileLocation).value currentUser.location || ; document.getElementById(profileContact).value currentUser.contact || ; document.getElementById(profileServicesCategory).value ; document.getElementById(profileServicesCheckboxes).style.display none; if (currentUser.services) { const firstService currentUser.services.split(, )0; for (const cat, services of Object.entries(servicesByCategory)) { if (services.includes(firstService)) { document.getElementById(profileServicesCategory).value cat; updateProfileServices(); const selectedServices currentUser.services.split(, ); document.querySelectorAll(#profileServicesCheckboxes inputtypecheckbox).forEach(cb > { if (selectedServices.includes(cb.value)) cb.checked true; }); document.getElementById(profileServicesCategory).dataset.selected currentUser.services; break; } } } document.getElementById(profileEmail).textContent currentUser.email; document.getElementById(profileEmailHidden).value currentUser.email; document.getElementById(profileType).textContent currentUser.type barber ? Spaces & Jobs : Products & Offers; if (currentUser.profilePicture) { document.getElementById(profilePictureImg).src currentUser.profilePicture; document.getElementById(profilePicturePreview).style.display block; } else { document.getElementById(profilePicturePreview).style.display none; } document.getElementById(profileModal).style.display block; showProfileTab(details); document.getElementById(mobileMenu).style.display none; } function handleProfilePictureUpload(event) { const file event.target.files0; if (!file) return; if (file.size > 5 * 1024 * 1024) { alert(File size must be less than 5MB); event.target.value ; return; } if (!image/jpeg, image/jpg, image/png.includes(file.type)) { alert(Only JPG and PNG files are allowed); event.target.value ; return; } const reader new FileReader(); reader.onload function(e) { const img new Image(); img.onload function() { const canvas document.createElement(canvas); const size 400; canvas.width size; canvas.height size; const ctx canvas.getContext(2d); const scale Math.max(size / img.width, size / img.height); const x (size - img.width * scale) / 2; const y (size - img.height * scale) / 2; ctx.drawImage(img, x, y, img.width * scale, img.height * scale); profilePicture canvas.toDataURL(image/jpeg, 0.85); document.getElementById(profilePictureImg).src profilePicture; document.getElementById(profilePicturePreview).style.display block; }; img.src e.target.result; }; reader.readAsDataURL(file); } async function updateProfile() { const name document.getElementById(profileName).value.trim(); const shopName document.getElementById(profileShopName).value.trim(); const location document.getElementById(profileLocation).value.trim(); const contact document.getElementById(profileContact).value.trim(); const services document.getElementById(profileServicesCategory).dataset.selected || ; if (!name) { alert(Name cannot be empty); return; } let profilePictureUrl currentUser.profilePicture; if (profilePicture && !profilePicture.startsWith(https://barber-marketplace-imgs)) { try { const presignedResponse await fetch(`${API_BASE}/upload`, { method: GET }); const { uploadUrl, publicUrl } await presignedResponse.json(); const base64Data profilePicture.split(,)1; const byteCharacters atob(base64Data); const byteNumbers new Array(byteCharacters.length); for (let i 0; i byteCharacters.length; i++) byteNumbersi byteCharacters.charCodeAt(i); const blob new Blob(new Uint8Array(byteNumbers), { type: image/jpeg }); await fetch(uploadUrl, { method: PUT, body: blob, headers: { Content-Type: image/jpeg } }); profilePictureUrl publicUrl; } catch (err) { console.error(Image upload error:, err); } } else if (profilePicture) profilePictureUrl profilePicture; currentUser.name name; currentUser.shopName shopName; currentUser.location location; currentUser.contact contact; currentUser.services services; currentUser.profilePicture profilePictureUrl; try { await fetch(`${API_BASE}/profile/${encodeURIComponent(currentUser.email)}`, { method: PUT, headers: { Content-Type: application/json }, body: JSON.stringify({ name, shopName, location, contact, services, profilePicture: profilePictureUrl, type: currentUser.type }) }); } catch (err) { console.error(Profile save error:, err); } localStorage.setItem(currentUser, JSON.stringify(currentUser)); const cognitoUser userPool.getCurrentUser(); if (cognitoUser) { cognitoUser.getSession((err, session) > { if (!err) { const attributeList new AmazonCognitoIdentity.CognitoUserAttribute({ Name: name, Value: name }); cognitoUser.updateAttributes(attributeList, (err, result) > { if (err) console.error(Cognito update error:, err); }); } }); } document.getElementById(profileBtn).textContent name; document.getElementById(mobileProfile).textContent name; alert(Profile updated successfully!); } function updateName() { updateProfile(); } function changePassword() { const current document.getElementById(currentPassword).value; const newPass document.getElementById(newPassword).value; const confirm document.getElementById(confirmPassword).value; if (!current || !newPass || !confirm) { alert(Please fill all password fields); return; } if (newPass ! confirm) { alert(New passwords do not match); return; } if (newPass.length 8) { alert(Password must be at least 8 characters); return; } // Update password in Cognito const cognitoUser userPool.getCurrentUser(); if (cognitoUser) { cognitoUser.getSession((err, session) > { if (err) { alert(Session error: + err.message); return; } cognitoUser.changePassword(current, newPass, (err, result) > { if (err) { alert(Password change error: + err.message); return; } alert(Password updated successfully!); document.getElementById(currentPassword).value ; document.getElementById(newPassword).value ; document.getElementById(confirmPassword).value ; closeModal(profileModal); }); }); } else { alert(Please login again to change password); } } async function closeAccount() { if (!confirm(β οΈ WARNING: This will permanently delete your account, profile, all listings, and uploaded images.\n\nThis action CANNOT be undone.\n\nAre you absolutely sure?)) return; if (!confirm(Final confirmation: Delete everything and close account?)) return; showSpinner(); try { const myListings ...seats, ...offers.filter(l > l.postedBy currentUser.email); for (const listing of myListings) { await fetch(`${API_BASE}/listings/${listing.id}`, { method: DELETE }); } await fetch(`${API_BASE}/profile/${encodeURIComponent(currentUser.email)}`, { method: DELETE }); await fetch(https://lb66oyprze.execute-api.eu-west-2.amazonaws.com/prod/send-email, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ type: account_deleted, data: { email: currentUser.email, name: currentUser.name } }) }); const cognitoUser userPool.getCurrentUser(); if (cognitoUser) { cognitoUser.getSession((err, session) > { if (!err) cognitoUser.deleteUser((err) > {}); }); } localStorage.removeItem(currentUser); alert(Account closed successfully. All data has been deleted.); location.reload(); } catch (err) { console.error(Account deletion error:, err); alert(Error closing account. Please contact support.); } finally { hideSpinner(); } } let isSubmitting false; async function postListing() { if (!currentUser) { alert(Please login first); return; } if (isSubmitting) return; const termsAccepted document.getElementById(listingTermsCheckbox).checked; if (!termsAccepted) { alert(Please accept the Terms & Conditions); return; } const listingType document.getElementById(listingType).value; const seatCategory document.getElementById(seatCategory).value; const expiryDays document.getElementById(expiryDays).value; const primaryCategory document.getElementById(primaryCategory).value; const postcode document.getElementById(region).value.trim(); const shopName document.getElementById(shopName).value.trim(); const price listingType offer ? document.getElementById(productPrice).value : document.getElementById(price).value; const description document.getElementById(description).value.trim(); const contact document.getElementById(contact).value.trim(); if (listingType seat) { if (!primaryCategory) { alert(Please select a category); return; } if (!seatCategory) { alert(Please select a workspace type); return; } } else if (listingType offer) { const productCategory document.getElementById(productCategory).value; const offerType document.getElementById(offerType).value; if (!productCategory) { alert(Please select a product category); return; } if (!offerType) { alert(Please select an offer type); return; } } if (!shopName) { alert(Please enter a shop/company name); return; } if (!postcode) { alert(Please enter a postcode); return; } if (!price) { alert(Please enter a price); return; } if (!description) { alert(Please enter a description); return; } if (!contact) { alert(Please enter contact details); return; } if (!expiryDays) { alert(Please select ad duration); return; } isSubmitting true; showSpinner(); const expiryDate expiryDays ? new Date(Date.now() + parseInt(expiryDays) * 24 * 60 * 60 * 1000).toLocaleDateString(en-GB) : ; const featured document.getElementById(featuredOption).value featured; // Validate postcode and get coordinates const coords await getPostcodeCoords(postcode); if (!coords) { alert(Invalid postcode. Please enter a valid UK postcode.); isSubmitting false; hideSpinner(); return; } const listing { type: listingType, category: listingType seat ? seatCategory : null, productCategory: listingType offer ? document.getElementById(productCategory).value : null, offerType: listingType offer ? document.getElementById(offerType).value : null, shopName: document.getElementById(shopName).value, region: postcode.toUpperCase(), coords: coords, price: listingType offer ? document.getElementById(productPrice).value : document.getElementById(price).value, priceUnit: listingType offer ? null : document.getElementById(priceUnit).value, image: uploadedImage, description: document.getElementById(description).value, contact: document.getElementById(contact).value, orderUrl: listingType offer ? document.getElementById(orderUrl).value : null, expiryDate: expiryDate, featured: featured, primaryCategory: listingType seat ? primaryCategory : null, postedBy: currentUser.email, tempId: Date.now().toString() }; // Check if payment required if (featured) { const amount parseInt(expiryDays); // Store listing in localStorage before redirect localStorage.setItem(pendingListing_ + listing.tempId, JSON.stringify(listing)); processStripePayment(amount, listing); } else { saveListing(listing); } isSubmitting false; hideSpinner(); } async function processStripePayment(amount, listing) { try { const response await fetch(`${API_BASE}/create-checkout-session`, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ amount: amount, listingId: listing.tempId, shopName: listing.shopName }) }); const { url } await response.json(); window.location.href url; } catch (err) { console.error(Payment error:, err); alert(Payment processing unavailable. Posting as standard listing.); listing.featured false; saveListing(listing); } } async function saveListing(listing) { try { // Upload image to S3 if exists and not a placeholder URL if (uploadedImage && !uploadedImage.startsWith(https://barber-marketplace-imgs)) { // Get presigned URL from backend const presignedResponse await fetch(`${API_BASE}/upload`, { method: GET }); const { uploadUrl, publicUrl } await presignedResponse.json(); // Convert base64 to blob const base64Data uploadedImage.split(,)1; const byteCharacters atob(base64Data); const byteNumbers new Array(byteCharacters.length); for (let i 0; i byteCharacters.length; i++) { byteNumbersi byteCharacters.charCodeAt(i); } const byteArray new Uint8Array(byteNumbers); const blob new Blob(byteArray, { type: image/jpeg }); // Upload directly to S3 using presigned URL await fetch(uploadUrl, { method: PUT, body: blob, headers: { Content-Type: image/jpeg } }); listing.image publicUrl; } else if (uploadedImage) { listing.image uploadedImage; } // Remove tempId before saving delete listing.tempId; // Create listing via API const response await fetch(`${API_BASE}/listings`, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(listing) }); if (response.ok) { const result await response.json(); await fetch(https://lb66oyprze.execute-api.eu-west-2.amazonaws.com/prod/send-email, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ type: listing.featured ? payment_confirmation : new_listing, data: { email: currentUser.email, name: currentUser.name, shopName: listing.shopName, region: listing.region, price: listing.price, expiryDate: listing.expiryDate, listingId: result.id, amount: listing.featured ? parseInt(document.getElementById(expiryDays).value) : 0 } }) }); closeModal(postModal); uploadedImage null; document.getElementById(imagePreview).style.display none; await loadListings(); alert(Listing posted successfully!); return result.id; } else { throw new Error(Failed to create listing); } } catch (err) { console.error(Error saving listing:, err); alert(Error posting listing. Please try again.); return null; } } init(); /script>/body>/html>
View on OTX
|
View on ThreatMiner
Please enable JavaScript to view the
comments powered by Disqus.
Data with thanks to
AlienVault OTX
,
VirusTotal
,
Malwr
and
others
. [
Sitemap
]