Help
RSS
API
Feed
Maltego
Contact
Domain > laughder.com
×
More information on this domain is in
AlienVault OTX
Is this malicious?
Yes
No
DNS Resolutions
Date
IP Address
2019-08-13
172.64.111.24
(
ClassC
)
2025-08-11
77.37.76.51
(
ClassC
)
Port 80
HTTP/1.1 301 Moved PermanentlyDate: Mon, 11 Aug 2025 13:46:46 GMTContent-Type: text/htmlContent-Length: 795Connection: keep-aliveLocation: https://laughder.com/platform: hostingerpanel: hpanelContent-Security-Policy: upgrade-insecure-requestsServer: hcdnalt-svc: h3:443; ma86400x-hcdn-request-id: 2266fd287af7224b5ebb17c8357a05f4-phx-edge5x-hcdn-cache-status: EXPIREDx-hcdn-upstream-rt: 0.000 !DOCTYPE html>html styleheight:100%>head>meta nameviewport contentwidthdevice-width, initial-scale1, shrink-to-fitno />title> 301 Moved Permanently/title>style>@media (prefers-color-scheme:dark){body{background-color:#000!important}}/style>/head>body stylecolor: #444; margin:0;font: normal 14px/20px Arial, Helvetica, sans-serif; height:100%; background-color: #fff;>div styleheight:auto; min-height:100%; > div styletext-align: center; width:800px; margin-left: -400px; position:absolute; top: 30%; left:50%;> h1 stylemargin:0; font-size:150px; line-height:150px; font-weight:bold;>301/h1>h2 stylemargin-top:20px;font-size: 30px;>Moved Permanently/h2>p>The document has been permanently moved./p>/div>/div>/body>/html>
Port 443
HTTP/1.1 200 OKDate: Mon, 11 Aug 2025 13:46:47 GMTContent-Type: text/htmlTransfer-Encoding: chunkedConnection: keep-aliveVary: Accept-EncodingLast-Modified: Sun, 10 Aug 2025 18:37:32 GMTEtag: W/13e3c-6898e6ec-8a968214b6e68690;gzplatform: hostingerpanel: hpanelContent-Security-Policy: upgrade-insecure-requestsServer: hcdnalt-svc: h3:443; ma86400x-hcdn-request-id: 1b092e342bfaad7fec475e7d3edcc049-phx-edge6x-hcdn-cache-status: DYNAMICx-hcdn-upstream-rt: 0.013 !DOCTYPE html>html langen>head> meta charsetUTF-8> meta nameviewport contentwidthdevice-width, initial-scale1.0> title>Laughder.com - All Comedy. One Place./title> script srchttps://cdn.tailwindcss.com>/script> link relpreconnect hrefhttps://fonts.googleapis.com> link relpreconnect hrefhttps://fonts.gstatic.com crossorigin> link hrefhttps://fonts.googleapis.com/css2?familyInter:wght@400;700;900&familyPoppins:wght@500;700&displayswap relstylesheet> style> :root { --dark-bg: #111827; --card-bg: #1f2937; --border-color: #374151; --primary-text: #f9fafb; --secondary-text: #9ca3af; } html { scroll-behavior: smooth; } body { font-family: Inter, sans-serif; background-color: var(--dark-bg); color: var(--primary-text); } .font-nav { font-family: Poppins, sans-serif; } .bg-gradient-primary { background: linear-gradient(135deg, #ec4899, #22d3ee); } .text-gradient-primary { background: linear-gradient(135deg, #ec4899, #22d3ee); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .nav-link { @apply px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white transition-colors; } .nav-link-active { @apply bg-gray-900 text-white; } .card { @apply bg-card-bg rounded-xl shadow-lg overflow-hidden transform hover:-translate-y-2 transition-transform duration-300 border border-transparent hover:border-cyan-400; } .btn { @apply px-6 py-3 rounded-full font-bold text-white transition-transform duration-300 shadow-lg; } .btn-primary { @apply bg-gradient-primary hover:scale-105; } .btn-secondary { @apply bg-gray-600 hover:bg-gray-700; } .section-title { @apply text-4xl font-extrabold mb-8 text-center; } .footer { @apply bg-gray-900 text-gray-400 pt-16 pb-8; } /* Glassmorphism Input/Button Style */ .form-input-glass, .btn-glass { border: 1px solid transparent; background-image: linear-gradient(rgba(31, 41, 55, 0.7), rgba(31, 41, 55, 0.7)), linear-gradient(135deg, #ec4899, #22d3ee); background-origin: border-box; background-clip: padding-box, border-box; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); border-radius: 8px; /* Slightly more rounded */ color: white; transition: all 0.2s; @apply w-full p-3; } .form-input-glass:focus, .btn-glass:focus { outline: none; box-shadow: 0 0 0 2px var(--dark-bg), 0 0 0 4px #22d3ee; } .form-input-glass::placeholder { color: var(--secondary-text); } .form-label { @apply block mb-2 text-sm font-bold text-gray-300; } @keyframes fade-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .animate-fade-in { animation: fade-in 0.5s ease-out forwards; } .video-thumbnail-container { position: relative; cursor: pointer; background-color: #000; } .play-button-overlay { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 64px; height: 64px; background-color: rgba(0, 0, 0, 0.5); border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: background-color 0.2s; } .video-thumbnail-container:hover .play-button-overlay { background-color: rgba(236, 72, 153, 0.8); } .play-button-overlay svg { width: 32px; height: 32px; color: white; } /* Modal styles */ .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.8); display: flex; align-items: center; justify-content: center; z-index: 50; opacity: 0; transition: opacity 0.3s ease; pointer-events: none; } .modal-content { background-color: var(--card-bg); border-radius: 8px; padding: 24px; width: 90%; max-width: 800px; transform: scale(0.95); transition: transform 0.3s ease; position: relative; } .modal-overlay.active { opacity: 1; pointer-events: auto; } .modal-overlay.active .modal-content { transform: scale(1); } .modal-close-btn { position: absolute; top: 10px; right: 10px; background: none; border: none; color: var(--secondary-text); font-size: 24px; cursor: pointer; line-height: 1; } .modal-close-btn:hover { color: var(--primary-text); } .video-responsive { overflow: hidden; padding-top: 56.25%; position: relative; height: 0; } .video-responsive iframe { left: 0; top: 0; height: 100%; width: 100%; position: absolute; border: none; } /* Toast Notification Styles */ #notification-toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 100; } .toast { background-color: #22c55e; /* green-500 */ color: white; padding: 12px 20px; border-radius: 8px; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); opacity: 0; transform: translateY(20px); transition: opacity 0.3s, transform 0.3s; margin-top: 10px; } .toast.error { background-color: #ef4444; /* red-500 */ } .toast.show { opacity: 1; transform: translateY(0); } /style>/head>body classantialiased> !-- App Container --> div idapp classmin-h-screen flex flex-col> !-- Header --> header classbg-gray-800/80 backdrop-blur-sm sticky top-0 z-40 shadow-lg> nav classcontainer mx-auto px-4 sm:px-6 lg:px-8> div classflex items-center justify-between h-16> div classflex items-center> a href#home data-pagehome> img classh-12 w-auto srchttps://firebasestorage.googleapis.com/v0/b/laugher-website.appspot.com/o/images%2FIMG_2191.png?altmedia&token040ee7b2-e900-485f-bb03-1cf8e0ae46e0 altLaughder Logo onerrorthis.onerrornull;this.srchttps://placehold.co/100x50/111827/ffffff?textLaughder;> /a> /div> div classhidden md:flex items-center> div classflex items-baseline space-x-2 font-nav> a href#home classnav-link data-pagehome>Home/a> a href#news classnav-link data-pagenews>News/a> a href#videos classnav-link data-pagevideos>Videos/a> a href#tour-dates classnav-link data-pagetour-dates>Tour Dates/a> a href#shop classnav-link data-pageshop>Shop/a> a href#admin idadmin-link classnav-link hidden data-pageadmin>Admin/a> /div> !-- Auth Links --> div idauth-links classml-4> a href#login classbtn-glass text-sm py-1 px-3 !w-auto data-pagelogin>Login / Sign Up/a> /div> div iduser-links classhidden items-center space-x-4 ml-4> a href#profile classnav-link data-pageprofile>Profile/a> button idlogout-btn classbtn-glass text-sm py-1 px-3 !w-auto>Logout/button> /div> /div> div classmd:hidden flex items-center> button idmobile-menu-button classinline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none> svg classh-6 w-6 idmenu-open-icon xmlnshttp://www.w3.org/2000/svg fillnone viewBox0 0 24 24 strokecurrentColor>path stroke-linecapround stroke-linejoinround stroke-width2 dM4 6h16M4 12h16M4 18h16 />/svg> svg classh-6 w-6 hidden idmenu-close-icon xmlnshttp://www.w3.org/2000/svg fillnone viewBox0 0 24 24 strokecurrentColor>path stroke-linecapround stroke-linejoinround stroke-width2 dM6 18L18 6M6 6l12 12 />/svg> /button> /div> /div> /nav> div classmd:hidden hidden idmobile-menu> div classpx-2 pt-2 pb-3 space-y-1 sm:px-3 font-nav> a href#home classblock nav-link data-pagehome>Home/a> a href#news classblock nav-link data-pagenews>News/a> a href#videos classblock nav-link data-pagevideos>Videos/a> a href#tour-dates classblock nav-link data-pagetour-dates>Tour Dates/a> a href#shop classblock nav-link data-pageshop>Shop/a> !-- Mobile Auth/User links will be injected here by JS --> /div> /div> /header> !-- Main Content --> main idmain-content classflex-grow> !-- Content will be injected here --> /main> !-- Footer --> footer classfooter mt-auto> div classcontainer mx-auto px-4 sm:px-6 lg:px-8> div classgrid grid-cols-1 md:grid-cols-4 gap-8> !-- About Section --> div classcol-span-1 md:col-span-2> h3 classtext-lg font-bold text-white mb-4 font-nav>About Laughder/h3> p classtext-sm>The only place where the headlines are as ridiculous as the news itself. We deliver the daily news with a satirical spin, a comedic edge, and a healthy dose of reality checks./p> div classmt-6 flex space-x-4> a href# classtext-gray-400 hover:text-white>span classsr-only>X/span>svg classh-6 w-6 fillcurrentColor viewBox0 0 24 24>path dM18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 7.184L18.901 1.153zm-1.61 19.931h2.5l-10.8-12.436H7.134l10.157 12.436z/>/svg>/a> a href# classtext-gray-400 hover:text-white>span classsr-only>Facebook/span>svg classh-6 w-6 fillcurrentColor viewBox0 0 24 24>path dM18 2h-3a5 5 0 00-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 011-1h3z/>/svg>/a> a href# classtext-gray-400 hover:text-white>span classsr-only>Instagram/span>svg classh-6 w-6 fillcurrentColor viewBox0 0 24 24>path fill-ruleevenodd dM12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.024.06 1.378.06 3.808s-.012 2.784-.06 3.808c-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.024.048-1.378.06-3.808.06s-2.784-.013-3.808-.06c-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.048-1.024-.06-1.378-.06-3.808s.012-2.784.06-3.808c.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153 1.772A4.902 4.902 0 016.08 2.525c.636-.247 1.363-.416 2.427-.465C9.531 2.013 9.884 2 12.315 2zM12 0C9.58 0 9.22.01 8.134.059 7.032.107 6.09.274 5.218.598a6.902 6.902 0 00-2.583 1.62A6.902 6.902 0 001.01 4.799c-.324.87-.49 1.812-.538 2.914C.01 8.78 0 9.14 0 11.56s.01 2.78.059 3.866c.048 1.102.214 2.044.538 2.914a6.902 6.902 0 001.62 2.583 6.902 6.902 0 002.583 1.62c.87.324 1.812.49 2.914.538 1.087.048 1.447.058 3.866.058s2.78-.01 3.866-.058c1.102-.048 2.044-.214 2.914-.538a6.902 6.902 0 002.583-1.62 6.902 6.902 0 001.62-2.583c.324-.87.49-1.812.538-2.914.048-1.087.058-1.447.058-3.866s-.01-2.78-.058-3.866c-.048-1.102-.214-2.044-.538-2.914a6.902 6.902 0 00-1.62-2.583A6.902 6.902 0 0019.2 1.01c-.87-.324-1.812-.49-2.914-.538C15.22.01 14.86 0 12.315 0h.001zM12 6.848c-2.835 0-5.152 2.316-5.152 5.152s2.317 5.152 5.152 5.152 5.152-2.316 5.152-5.152S14.835 6.848 12 6.848zm0 8.304c-1.742 0-3.152-1.41-3.152-3.152s1.41-3.152 3.152-3.152 3.152 1.41 3.152 3.152-1.41 3.152-3.152 3.152zm6.353-8.224a1.202 1.202 0 110-2.404 1.202 1.202 0 010 2.404z clip-ruleevenodd />/svg>/a> a href# classtext-gray-400 hover:text-white>span classsr-only>YouTube/span>svg classh-6 w-6 fillcurrentColor viewBox0 0 24 24>path fill-ruleevenodd dM19.812 5.418c.861.23 1.538.907 1.768 1.768C21.998 8.78 22 12 22 12s0 3.22-.42 4.814a2.506 2.506 0 0 1-1.768 1.768c-1.594.42-7.812.42-7.812.42s-6.218 0-7.812-.42a2.506 2.506 0 0 1-1.768-1.768C2 15.22 2 12 2 12s0-3.22.42-4.814a2.506 2.506 0 0 1 1.768-1.768C5.782 5 12 5 12 5s6.218 0 7.812.418zM9.5 15.5V8.5l6 3.5-6 3.5z clip-ruleevenodd />/svg>/a> a href# classtext-gray-400 hover:text-white>span classsr-only>TikTok/span>svg classh-6 w-6 fillcurrentColor viewBox0 0 24 24>path dM12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.22.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z/>/svg>/a> /div> /div> !-- Navigation Links --> div> h3 classtext-lg font-bold text-white mb-4 font-nav>Explore/h3> ul classspace-y-2 text-sm> li>a href#home data-pagehome classhover:text-white>Home/a>/li> li>a href#news data-pagenews classhover:text-white>News/a>/li> li>a href#videos data-pagevideos classhover:text-white>Videos/a>/li> li>a href#tour-dates data-pagetour-dates classhover:text-white>Tour Dates/a>/li> li>a href#shop data-pageshop classhover:text-white>Shop/a>/li> /ul> /div> !-- Newsletter --> div> h3 classtext-lg font-bold text-white mb-4 font-nav>Stay Updated/h3> p classtext-sm mb-4>Get the best of Laughder delivered to your inbox./p> form idfooter-newsletter-form classflex flex-col gap-2> input typeemail idfooter-newsletter-email placeholderYour Email required classform-input-glass bg-gray-700/70 p-2 rounded-md border-gray-600> button typesubmit classbtn btn-primary text-sm py-2>Subscribe/button> /form> /div> /div> div classmt-12 border-t border-gray-800 pt-8 text-center text-sm> p>© 2024 Laughder.com. All Rights Reserved./p> /div> /div> /footer> /div> !-- Global Components --> div idnotification-toast-container>/div> div idmodal-container>/div> !-- Page Templates --> template idtemplate-home> div classanimate-fade-in> !-- Hero Section --> section classcontainer mx-auto px-4 sm:px-6 lg:px-8 py-20 text-center> h1 classtext-5xl md:text-7xl font-black tracking-tighter mb-4 text-white> All Comedy. span classtext-gradient-primary>One Place./span> /h1> p classmax-w-2xl mx-auto text-lg md:text-xl text-gray-300 mb-8> Your curated daily dose of comedy journalism, reviews, and viral hits. /p> /section> !-- Latest News Section --> section classcontainer mx-auto px-4 sm:px-6 lg:px-8 py-16> h2 classtext-3xl font-bold mb-6 text-left text-gradient-primary>Latest News/h2> div idlatest-news-grid classgrid md:grid-cols-2 lg:grid-cols-4 gap-8> !-- News items will be loaded here by Firebase --> /div> /section> !-- Featured Videos Section --> section classcontainer mx-auto px-4 sm:px-6 lg:px-8 py-8> h2 classtext-3xl font-bold mb-6 text-left text-gradient-primary>Featured Videos/h2> div idfeatured-videos-grid classgrid md:grid-cols-2 lg:grid-cols-3 gap-8> !-- Featured videos will be loaded here by Firebase --> /div> /section> !-- Featured Articles Section --> section classcontainer mx-auto px-4 sm:px-6 lg:px-8 py-8> h2 classtext-3xl font-bold mb-6 text-left text-gradient-primary>Featured Articles/h2> div idfeatured-articles-grid classgrid md:grid-cols-2 lg:grid-cols-3 gap-8> !-- Featured articles will be loaded here by Firebase --> /div> /section> /div> /template> template idtemplate-news> div classcontainer mx-auto px-4 sm:px-6 lg:px-8 py-16 animate-fade-in> h1 classsection-title text-gradient-primary>News/h1> div idnews-page-grid classgrid md:grid-cols-2 lg:grid-cols-3 gap-8> !-- Full news list will be loaded here --> /div> /div> /template> template idtemplate-videos> div classcontainer mx-auto px-4 sm:px-6 lg:px-8 py-16 animate-fade-in> h1 classsection-title text-gradient-primary>Videos/h1> div idvideos-page-grid classgrid md:grid-cols-2 lg:grid-cols-3 gap-8> !-- Full video list will be loaded here --> /div> /div> /template> template idtemplate-tour-dates> div classcontainer mx-auto px-4 sm:px-6 lg:px-8 py-16 animate-fade-in> h1 classsection-title text-gradient-primary>Tour Dates/h1> !-- Search Inputs --> div classmax-w-2xl mx-auto mb-12 grid grid-cols-1 sm:grid-cols-2 gap-4> input typetext idcomedian-search classform-input-glass placeholderSearch by Comedian...> input typetext idlocation-search classform-input-glass placeholderSearch by Location...> /div> div idtour-dates-list classspace-y-4 max-w-3xl mx-auto> !-- Tour dates will be loaded here by Firebase --> /div> /div> /template> template idtemplate-shop> div classcontainer mx-auto px-4 sm:px-6 lg:px-8 py-16 animate-fade-in> h1 classsection-title text-gradient-primary>Shop/h1> div idshop-grid classgrid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8> !-- Products will be loaded here --> /div> /div> /template> template idtemplate-login> div classcontainer mx-auto max-w-md px-4 py-16 animate-fade-in> h1 classsection-title text-gradient-primary>Welcome/h1> div classbg-card-bg p-8 rounded-xl shadow-2xl> div classflex border-b border-border-color mb-6> button idlogin-tab-btn classflex-1 py-2 font-bold text-white border-b-2 border-cyan-400 transition-colors>Login/button> button idsignup-tab-btn classflex-1 py-2 font-bold text-gray-500 border-b-2 border-transparent transition-colors>Sign Up/button> /div> !-- Login Form --> form idlogin-form> div classspace-y-6> div classgrid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-2 items-center> label forlogin-email classform-label sm:col-span-1 sm:text-right !mb-0>Email/label> div classsm:col-span-2> input typeemail idlogin-email required classform-input-glass placeholderyou@example.com> /div> /div> div classgrid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-2 items-center> label forlogin-password classform-label sm:col-span-1 sm:text-right !mb-0>Password/label> div classsm:col-span-2> input typepassword idlogin-password required classform-input-glass placeholder••••••••> /div> /div> /div> div classmt-8> button typesubmit classbtn btn-primary w-full>Login/button> /div> /form> !-- Signup Form --> form idsignup-form classhidden> div classspace-y-6> div classgrid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-2 items-center> label forsignup-email classform-label sm:col-span-1 sm:text-right !mb-0>Email/label> div classsm:col-span-2> input typeemail idsignup-email required classform-input-glass placeholderyou@example.com> /div> /div> div classgrid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-2 items-center> label forsignup-password classform-label sm:col-span-1 sm:text-right !mb-0>Password/label> div classsm:col-span-2> input typepassword idsignup-password required classform-input-glass placeholderMust be at least 6 characters> /div> /div> /div> div classmt-8> button typesubmit classbtn btn-primary w-full>Create Account/button> /div> /form> !-- Dev Login Button --> div classtext-center mt-6 pt-4 border-t border-border-color> button iddev-login-btn classtext-xs text-gray-500 hover:text-white transition-colors>Dev Admin Login/button> /div> /div> /div> /template> template idtemplate-profile> div classcontainer mx-auto px-4 py-16 animate-fade-in> h1 classsection-title text-gradient-primary>Your Profile/h1> div idprofile-content classbg-card-bg p-8 rounded-xl shadow-2xl max-w-lg mx-auto text-center> p classtext-gray-400>Loading profile.../p> /div> /div> /template> template idtemplate-admin> div classcontainer mx-auto max-w-2xl px-4 py-16 animate-fade-in> h1 classsection-title text-gradient-primary>Admin Panel/h1> div classbg-card-bg p-8 rounded-xl shadow-2xl> !-- Admin Buttons --> div idadmin-controls classflex flex-wrap items-center justify-center gap-4 mb-8> button data-formadd-news-form classadmin-form-btn btn-glass !w-auto px-5 py-2>Add News/button> button data-formadd-video-form classadmin-form-btn btn-glass !w-auto px-5 py-2>Add Video/button> button data-formadd-tour-form classadmin-form-btn btn-glass !w-auto px-5 py-2>Add Tour Date/button> /div> !-- Add News Form --> form idadd-news-form> h2 classtext-2xl font-bold text-white mb-6 text-center>Add News Article/h2> div classspace-y-6> div classgrid grid-cols-1 sm:grid-cols-4 gap-x-4 gap-y-2 items-center> label fornews-title classform-label sm:col-span-1 sm:text-right !mb-0>Title/label> div classsm:col-span-3>input typetext idnews-title required classform-input-glass>/div> /div> div classgrid grid-cols-1 sm:grid-cols-4 gap-x-4 gap-y-2 items-start> label fornews-content classform-label sm:col-span-1 sm:text-right !mb-0 pt-3>Content/label> div classsm:col-span-3>textarea idnews-content required classform-input-glass rows4>/textarea>/div> /div> div classgrid grid-cols-1 sm:grid-cols-4 gap-x-4 gap-y-2 items-center> label fornews-image-url classform-label sm:col-span-1 sm:text-right !mb-0>Image URL/label> div classsm:col-span-3>input typeurl idnews-image-url classform-input-glass placeholderhttps://...>/div> /div> div classgrid grid-cols-1 sm:grid-cols-4 gap-x-4 gap-y-2 items-center> div classsm:col-start-2 sm:col-span-3 flex items-center> input typecheckbox idnews-featured classh-4 w-4 rounded border-gray-300 text-cyan-500 focus:ring-cyan-500> label fornews-featured classml-2 block text-sm text-gray-300>Featured Article?/label> /div> /div> /div> div classmt-8>button typesubmit classbtn btn-primary w-full>Submit News Article/button>/div> /form> !-- Add Video Form --> form idadd-video-form classhidden> h2 classtext-2xl font-bold text-white mb-6 text-center>Add Video/h2> div classspace-y-6> div classgrid grid-cols-1 sm:grid-cols-4 gap-x-4 gap-y-2 items-center> label forvideo-title classform-label sm:col-span-1 sm:text-right !mb-0>Title/label> div classsm:col-span-3>input typetext idvideo-title required classform-input-glass>/div> /div> div classgrid grid-cols-1 sm:grid-cols-4 gap-x-4 gap-y-2 items-start> label forvideo-content classform-label sm:col-span-1 sm:text-right !mb-0 pt-3>Description/label> div classsm:col-span-3>textarea idvideo-content required classform-input-glass rows3>/textarea>/div> /div> div classgrid grid-cols-1 sm:grid-cols-4 gap-x-4 gap-y-2 items-center> label forvideo-url classform-label sm:col-span-1 sm:text-right !mb-0>YouTube URL/label> div classsm:col-span-3>input typeurl idvideo-url required classform-input-glass placeholderhttps://youtube.com/watch?v...>/div> /div> div classgrid grid-cols-1 sm:grid-cols-4 gap-x-4 gap-y-2 items-center> label forvideo-image-url classform-label sm:col-span-1 sm:text-right !mb-0>Thumbnail URL/label> div classsm:col-span-3>input typeurl idvideo-image-url classform-input-glass placeholderhttps://...>/div> /div> div classgrid grid-cols-1 sm:grid-cols-4 gap-x-4 gap-y-2 items-center> div classsm:col-start-2 sm:col-span-3 flex items-center> input typecheckbox idvideo-featured classh-4 w-4 rounded border-gray-300 text-cyan-500 focus:ring-cyan-500> label forvideo-featured classml-2 block text-sm text-gray-300>Featured Video?/label> /div> /div> /div> div classmt-8>button typesubmit classbtn btn-primary w-full>Submit Video/button>/div> /form> !-- Add Tour Date Form --> form idadd-tour-form classhidden> h2 classtext-2xl font-bold text-white mb-6 text-center>Add Tour Dates/h2> div classmb-8 p-4 border border-border-color rounded-lg> h3 classfont-bold text-lg mb-3 text-center>Fetch from Ticketmaster/h3> div classgrid grid-cols-1 sm:grid-cols-4 gap-x-4 gap-y-2 items-center> label forfetch-comedian-name classform-label sm:col-span-1 sm:text-right !mb-0>Comedian/label> div classsm:col-span-3> input typetext idfetch-comedian-name required classform-input-glass placeholdere.g., Jerry Seinfeld> /div> /div> div classmt-4> button typebutton idfetch-tour-dates-btn classbtn-secondary !w-full !rounded-lg py-2>Fetch Dates/button> /div> div idfetched-dates-container classmt-4 max-h-60 overflow-y-auto>/div> /div> hr classborder-border-color my-8> h3 classfont-bold text-lg mb-4 text-center>Add Manually/h3> div classspace-y-6> div classgrid grid-cols-1 sm:grid-cols-4 gap-x-4 gap-y-2 items-center> label fortour-comedian classform-label sm:col-span-1 sm:text-right !mb-0>Comedian/label> div classsm:col-span-3>input typetext idtour-comedian required classform-input-glass>/div> /div> div classgrid grid-cols-1 sm:grid-cols-4 gap-x-4 gap-y-2 items-center> label fortour-city classform-label sm:col-span-1 sm:text-right !mb-0>City/label> div classsm:col-span-3>input typetext idtour-city required classform-input-glass>/div> /div> div classgrid grid-cols-1 sm:grid-cols-4 gap-x-4 gap-y-2 items-center> label fortour-venue classform-label sm:col-span-1 sm:text-right !mb-0>Venue/label> div classsm:col-span-3>input typetext idtour-venue required classform-input-glass>/div> /div> div classgrid grid-cols-1 sm:grid-cols-4 gap-x-4 gap-y-2 items-center> label fortour-date classform-label sm:col-span-1 sm:text-right !mb-0>Date/label> div classsm:col-span-3>input typedate idtour-date required classform-input-glass stylecolor-scheme: dark;>/div> /div> /div> div classmt-8>button typesubmit classbtn btn-primary w-full>Submit Manual Entry/button>/div> /form> /div> /div> /template> script typemodule> // Firebase Imports import { initializeApp } from https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js; import { getFirestore, collection, onSnapshot, query, where, addDoc, serverTimestamp, Timestamp, writeBatch, doc, getDoc, setLogLevel } from https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js; import { getAuth, onAuthStateChanged, createUserWithEmailAndPassword, signInWithEmailAndPassword, signOut, signInAnonymously, signInWithCustomToken } from https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js; import { getFunctions, httpsCallable } from https://www.gstatic.com/firebasejs/11.6.1/firebase-functions.js; // --- App State --- let appInitialized false; let db, auth, functions; let devBypassActive false; // Flag for temporary dev login let allTourDates ; // Cache for tour date filtering let fetchedDatesCache ; // Cache for fetched tour dates before saving const ADMIN_UID 7bajPilt3MhiOFWgCU0ok5TQJTd2; // Replace with your actual Firebase Admin UID const appId typeof __app_id ! undefined ? __app_id : laughder-website-dev; // --- Firestore Path Helpers --- const getPublicCollection (name) > { const path `artifacts/${appId}/public/data/${name}`; return collection(db, path); }; const getPrivateCollection (name) > { const user getEffectiveUser(); if (!user || !user.uid) { console.error(Cannot get private collection: user not logged in or UID is missing.); return null; } const userId user.uid; const path `artifacts/${appId}/users/${userId}/${name}`; return collection(db, path); }; // --- Auth Helper --- const getEffectiveUser () > { if (devBypassActive) { return { uid: ADMIN_UID, email: dev-admin@laughder.com }; } return auth.currentUser; }; // --- Pricing Helper --- const calculateRetailPrice (cost) > { const costFloat parseFloat(cost); if (isNaN(costFloat)) return N/A; // 20% markup const markupPrice costFloat * 1.2; // Round up to the nearest 5 const roundedUp Math.ceil(markupPrice / 5) * 5; // Subtract a penny for .99 pricing const finalPrice roundedUp - 0.01; return finalPrice.toFixed(2); }; // --- DOM Elements --- const mainContent document.getElementById(main-content); // --- Page Config --- const pages { home: template-home, tour-dates: template-tour-dates, news: template-news, videos: template-videos, shop: template-shop, login: template-login, profile: template-profile, admin: template-admin }; // --- UI Components --- const openModal (title, contentHTML) > { const modalContainer document.getElementById(modal-container); modalContainer.innerHTML ` div classmodal-overlay idmodal-overlay> div classmodal-content> button classmodal-close-btn idmodal-close-btn>×/button> ${contentHTML} /div> /div> `; const overlay document.getElementById(modal-overlay); setTimeout(() > overlay.classList.add(active), 10); const closeModal () > { overlay.classList.remove(active); setTimeout(() > modalContainer.innerHTML , 300); }; document.getElementById(modal-close-btn).addEventListener(click, closeModal); overlay.addEventListener(click, (e) > { if (e.target overlay) closeModal(); }); }; const showToast (message, type success) > { const container document.getElementById(notification-toast-container); const toast document.createElement(div); toast.className `toast ${type}`; toast.innerHTML message; container.appendChild(toast); setTimeout(() > toast.classList.add(show), 10); setTimeout(() > { toast.classList.remove(show); toast.addEventListener(transitionend, () > toast.remove()); }, 8000); }; // --- Navigation & Page Loading --- const navigate (page) > { if (!pagespage) page home; const user getEffectiveUser(); if (page admin && user?.uid ! ADMIN_UID) { showToast(You dont have permission to view this page., error); navigate(home); return; } if (page profile && !user) { showToast(You must be logged in to view your profile., error); navigate(login); return; } mainContent.innerHTML ; setTimeout(() > { const template document.getElementById(pagespage); if (template) { mainContent.innerHTML template.innerHTML; } else { mainContent.innerHTML `p classtext-center text-red-400 p-8>Error: Page template not found for ${page}/p>`; } window.history.pushState({ page }, `${page}`, `#${page}`); document.querySelectorAll(.nav-link).forEach(link > { link.classList.toggle(nav-link-active, link.dataset.page page); }); loadPageContent(page); }, 50); }; const loadPageContent (page) > { if (page home) loadHomePage(); else if (page tour-dates) loadTourDatesPage(); else if (page news) loadNewsPage(); else if (page videos) loadVideosPage(); else if (page shop) loadShopPage(); else if (page login) setupAuthForms(); else if (page profile) loadProfilePage(); else if (page admin) setupAdminForms(); }; // --- Page Specific Loaders & Handlers --- const loadHomePage () > { const newsGrid document.getElementById(latest-news-grid); const videosGrid document.getElementById(featured-videos-grid); const articlesGrid document.getElementById(featured-articles-grid); if (newsGrid) { const qNews query(getPublicCollection(posts), where(category, , articles)); onSnapshot(qNews, snapshot > { const docs snapshot.docs.sort((a, b) > b.data().createdAt.seconds - a.data().createdAt.seconds); const limitedSnapshot { docs: docs.slice(0, 4), empty: docs.length 0 }; renderHomeNewsGrid(newsGrid, limitedSnapshot, No news yet.); }, (error) > console.error(Error fetching news:, error)); } if (videosGrid) { const qVideos query(getPublicCollection(posts), where(featured, , true), where(category, in, videos, shorts, standup)); onSnapshot(qVideos, snapshot > { const docs snapshot.docs.sort((a, b) > b.data().createdAt.seconds - a.data().createdAt.seconds); const limitedSnapshot { docs: docs.slice(0, 3), empty: docs.length 0 }; renderPostGrid(videosGrid, limitedSnapshot, No featured videos yet.); }, (error) > console.error(Error fetching videos:, error)); } if (articlesGrid) { const qArticles query(getPublicCollection(posts), where(featured, , true), where(category, , articles)); onSnapshot(qArticles, snapshot > { const docs snapshot.docs.sort((a, b) > b.data().createdAt.seconds - a.data().createdAt.seconds); const limitedSnapshot { docs: docs.slice(0, 3), empty: docs.length 0 }; renderPostGrid(articlesGrid, limitedSnapshot, No featured articles yet.); }, (error) > console.error(Error fetching articles:, error)); } }; const loadNewsPage () > { const container document.getElementById(news-page-grid); if (container) { const q query(getPublicCollection(posts), where(category, , articles)); onSnapshot(q, snapshot > { const docs snapshot.docs.sort((a, b) > b.data().createdAt.seconds - a.data().createdAt.seconds); const sortedSnapshot { docs: docs, empty: docs.length 0 }; renderPostGrid(container, sortedSnapshot, No news articles found.); }, (error) > console.error(Error fetching news page:, error)); } }; const loadVideosPage () > { const container document.getElementById(videos-page-grid); if (container) { const q query(getPublicCollection(posts), where(category, in, videos, shorts, standup)); onSnapshot(q, snapshot > { const docs snapshot.docs.sort((a, b) > b.data().createdAt.seconds - a.data().createdAt.seconds); const sortedSnapshot { docs: docs, empty: docs.length 0 }; renderPostGrid(container, sortedSnapshot, No videos found.); }, (error) > console.error(Error fetching videos page:, error)); } }; const loadShopPage async () > { const container document.getElementById(shop-grid); if (!container) return; container.innerHTML `p classtext-gray-400 col-span-full text-center>Loading products from Printful.../p>`; try { const getPrintfulProducts httpsCallable(functions, getPrintfulProducts); const result await getPrintfulProducts(); renderProductGrid(container, result.data, The shop is currently empty. Check back soon!); } catch (error) { console.error(Error fetching Printful products:, error); container.innerHTML `p classtext-red-400 col-span-full text-center>Could not load products. Please ensure the Printful integration is configured correctly./p>`; } }; const loadTourDatesPage () > { const q query(getPublicCollection(tourDates)); onSnapshot(q, snapshot > { const dates snapshot.docs.map(doc > doc.data()); dates.sort((a, b) > a.date.seconds - b.date.seconds); allTourDates dates; renderTourDates(); }, (error) > console.error(Error fetching tour dates:, error)); setTimeout(() > { const comedianSearch document.getElementById(comedian-search); const locationSearch document.getElementById(location-search); if (comedianSearch) comedianSearch.addEventListener(input, renderTourDates); if (locationSearch) locationSearch.addEventListener(input, renderTourDates); }, 100); }; const loadProfilePage () > { const user getEffectiveUser(); const profileContent document.getElementById(profile-content); if (user && profileContent) { profileContent.innerHTML ` p classtext-lg>strong>Email:/strong> ${user.email}/p> p classtext-sm text-gray-400 mt-2>strong>User ID:/strong> ${user.uid}/p> button idprofile-logout-btn classbtn btn-secondary mt-6>Logout/button> `; document.getElementById(profile-logout-btn).addEventListener(click, handleLogout); } }; // --- Rendering Functions --- const renderTourDates () > { const container document.getElementById(tour-dates-list); if (!container) return; const comedianSearchTerm document.getElementById(comedian-search)?.value.toLowerCase() || ; const locationSearchTerm document.getElementById(location-search)?.value.toLowerCase() || ; const filteredDates allTourDates.filter(date > { const comedianMatch date.comedian.toLowerCase().includes(comedianSearchTerm); const locationMatch date.city.toLowerCase().includes(locationSearchTerm) || date.venue.toLowerCase().includes(locationSearchTerm); return comedianMatch && locationMatch; }); if (filteredDates.length 0) { container.innerHTML `p classtext-center text-gray-400>No matching tour dates found./p>`; return; } container.innerHTML filteredDates.map(date > { const eventDate new Date(date.date.seconds * 1000); const formattedDate eventDate.toLocaleDateString(undefined, { month: short, day: numeric, year: numeric }); return ` div classbg-card-bg rounded-xl shadow-lg p-4 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4> div classflex-1> p classfont-bold text-lg text-white>${date.comedian}/p> p classtext-sm text-secondary-text>${date.venue}, ${date.city}/p> /div> p classtext-lg font-bold text-cyan-400>${formattedDate}/p> /div>`; }).join(); }; const renderHomeNewsGrid (container, snapshot, emptyMessage) > { if (!container) return; let content ; if (snapshot.empty) { content `p classtext-gray-400 col-span-full text-center>${emptyMessage}/p>`; } else { content snapshot.docs.map(doc > { const data doc.data(); return ` div classbg-card-bg rounded-lg overflow-hidden transition-colors hover:bg-gray-700/50 cursor-pointer data-id${doc.id} data-categoryarticles> div classp-6> h3 classfont-bold text-lg mb-2>${data.title}/h3> p classtext-secondary-text text-sm>${data.content.substring(0, 100)}.../p> /div> /div>`; }).join(); } container.innerHTML content; }; const renderPostGrid (container, snapshot, emptyMessage) > { if (!container) return; let content ; if (snapshot.empty) { content `p classtext-gray-400 col-span-full text-center>${emptyMessage}/p>`; } else { content snapshot.docs.map(doc > getPostCardHTML(doc.id, doc.data())).join(); } container.innerHTML content; }; const getPostCardHTML (id, data) > { const image data.imageUrl || `https://placehold.co/600x400/1f2937/ffffff?textLaughder`; let contentHTML; if (data.videoUrl) { contentHTML ` div classvideo-thumbnail-container relative pt-56.25% data-video-url${data.videoUrl} data-title${data.title}> img src${image} alt${data.title} classabsolute top-0 left-0 w-full h-full object-cover onerrorthis.onerrornull;this.srchttps://placehold.co/600x400/1f2937/ffffff?textVideo;> div classplay-button-overlay> svg xmlnshttp://www.w3.org/2000/svg viewBox0 0 24 24 fillcurrentColor>path dM4.5 5.653c0-1.426 1.529-2.33 2.779-1.643l11.54 6.647c1.295.742 1.295 2.545 0 3.286L7.279 20.99c-1.25.717-2.779-.217-2.779-1.643V5.653z />/svg> /div> /div>`; } else { contentHTML `img src${image} alt${data.title} classw-full h-48 object-cover onerrorthis.onerrornull;this.srchttps://placehold.co/600x400/1f2937/ffffff?textArticle;>`; } return `div classcard flex flex-col data-id${id} data-category${data.category}> ${contentHTML} div classp-6 flex-grow flex flex-col> h3 classtext-xl font-bold mb-2>${data.title}/h3> p classtext-gray-400 text-sm flex-grow>${data.content.substring(0,100)}.../p> /div> /div>`; }; const renderProductGrid (container, products, emptyMessage) > { if (!container) return; if (!products || products.length 0) { container.innerHTML `p classtext-gray-400 col-span-full text-center>${emptyMessage}/p>`; return; } container.innerHTML products.map(product > { const image product.imageUrl || `https://placehold.co/400x400/1f2937/ffffff?textItem`; const firstVariant product.variants0; if (!firstVariant) return ; // Skip product if it has no variants const retailPrice calculateRetailPrice(firstVariant.price); return ` div classcard flex flex-col text-center data-product-id${product.id}> div classrelative pt-100% cursor-pointer product-image-container> img src${image} alt${product.name} classabsolute top-0 left-0 w-full h-full object-cover onerrorthis.onerrornull;this.srchttps://placehold.co/400x400/1f2937/ffffff?textItem;> /div> div classp-4 flex-grow flex flex-col> h3 classtext-lg font-bold flex-grow>${product.name}/h3> div classmt-4> p classtext-2xl font-black text-white>$${retailPrice}/p> button classbtn btn-primary w-full mt-2 py-2 text-sm buy-now-btn data-product-id${product.id} data-variant-id${firstVariant.id} data-price${retailPrice} data-name${product.name} data-image${image}> Buy Now /button> /div> /div> /div> `; }).join(); }; // --- Auth & Form Handlers --- const updateAuthUI (user) > { const authLinks document.getElementById(auth-links); const userLinks document.getElementById(user-links); const adminLink document.getElementById(admin-link); const mobileMenuContainer document.querySelector(#mobile-menu > div); mobileMenuContainer.querySelectorAll(.mobile-auth-link).forEach(link > link.remove()); if (user) { authLinks.classList.add(hidden); userLinks.classList.remove(hidden); adminLink.classList.toggle(hidden, user.uid ! ADMIN_UID); if (user.uid ADMIN_UID) { const adminMobileLink document.createElement(a); adminMobileLink.href #admin; adminMobileLink.className block nav-link mobile-auth-link; adminMobileLink.dataset.page admin; adminMobileLink.textContent Admin; mobileMenuContainer.appendChild(adminMobileLink); } const profileMobileLink document.createElement(a); profileMobileLink.href #profile; profileMobileLink.className block nav-link mobile-auth-link; profileMobileLink.dataset.page profile; profileMobileLink.textContent Profile; mobileMenuContainer.appendChild(profileMobileLink); const logoutMobileBtn document.createElement(button); logoutMobileBtn.className block nav-link text-left w-full mobile-auth-link; logoutMobileBtn.textContent Logout; logoutMobileBtn.addEventListener(click, handleLogout); mobileMenuContainer.appendChild(logoutMobileBtn); } else { authLinks.classList.remove(hidden); userLinks.classList.add(hidden); adminLink.classList.add(hidden); const loginMobileLink document.createElement(a); loginMobileLink.href #login; loginMobileLink.className block nav-link mobile-auth-link; loginMobileLink.dataset.page login; loginMobileLink.textContent Login / Sign Up; mobileMenuContainer.appendChild(loginMobileLink); } }; const handleSignup async (e) > { e.preventDefault(); const email document.getElementById(signup-email).value; const password document.getElementById(signup-password).value; try { await createUserWithEmailAndPassword(auth, email, password); showToast(Account created successfully! Welcome!); navigate(home); } catch (error) { console.error(Signup Error:, error); showToast(error.message, error); } }; const handleLogin async (e) > { e.preventDefault(); const email document.getElementById(login-email).value; const password document.getElementById(login-password).value; try { await signInWithEmailAndPassword(auth, email, password); showToast(Logged in successfully!); navigate(home); } catch (error) { console.error(Login Error:, error); showToast(error.message, error); } }; const handleLogout async () > { if (devBypassActive) { devBypassActive false; updateAuthUI(null); showToast(Logged out successfully.); navigate(home); return; } try { await signOut(auth); showToast(Logged out successfully.); navigate(home); } catch (error) { console.error(Logout Error:, error); showToast(error.message, error); } }; const handleDevLogin (e) > { e.preventDefault(); if (ADMIN_UID PASTE_YOUR_ADMIN_USER_ID_HERE || !ADMIN_UID) { showToast(Please set ADMIN_UID in the script for dev login., error); return; } devBypassActive true; updateAuthUI(getEffectiveUser()); navigate(admin); showToast(Dev Admin Login successful.); }; const handleNewsletterSubmit async (e) > { e.preventDefault(); const emailInput document.getElementById(footer-newsletter-email); const email emailInput.value; if (!email) return; try { await addDoc(getPublicCollection(newsletterSubscriptions), { email: email, subscribedAt: serverTimestamp() }); showToast(Thanks for subscribing!); emailInput.value ; } catch(error) { console.error(Newsletter Error:, error); showToast(Could not subscribe. Please try again., error); } }; const handleAddNews async (e) > { e.preventDefault(); const user getEffectiveUser(); if (!user) { showToast(You must be logged in., error); return; } const form e.target; const post { title: formnews-title.value, content: formnews-content.value, imageUrl: formnews-image-url.value, featured: formnews-featured.checked, category: articles, createdAt: serverTimestamp(), authorId: user.uid }; try { await addDoc(getPublicCollection(posts), post); showToast(News article added!); form.reset(); } catch (error) { console.error(Error adding news: , error); showToast(Failed to add news., error); } }; const handleAddVideo async (e) > { e.preventDefault(); const user getEffectiveUser(); if (!user) { showToast(You must be logged in., error); return; } const form e.target; const post { title: formvideo-title.value, content: formvideo-content.value, videoUrl: formvideo-url.value, imageUrl: formvideo-image-url.value, featured: formvideo-featured.checked, category: videos, createdAt: serverTimestamp(), authorId: user.uid }; try { await addDoc(getPublicCollection(posts), post); showToast(Video added!); form.reset(); } catch (error) { console.error(Error adding video: , error); showToast(Failed to add video., error); } }; const handleAddTourDate async (e) > { e.preventDefault(); const user getEffectiveUser(); if (!user) { showToast(You must be logged in., error); return; } const form e.target; const dateStr formtour-date.value; if (!dateStr) { showToast(Please select a date., error); return; } const tourDate { comedian: formtour-comedian.value, city: formtour-city.value, venue: formtour-venue.value, date: Timestamp.fromDate(new Date(dateStr)), createdAt: serverTimestamp(), authorId: user.uid }; try { await addDoc(getPublicCollection(tourDates), tourDate); showToast(Tour date added!); form.reset(); } catch (error) { console.error(Error adding tour date: , error); showToast(Failed to add tour date., error); } }; const handleFetchTourDates async () > { const comedianName document.getElementById(fetch-comedian-name).value; if (!comedianName) { showToast(Please enter a comedians name., error); return; } const container document.getElementById(fetched-dates-container); container.innerHTML `p classtext-center text-gray-400>Fetching dates.../p>`; try { const getTourDates httpsCallable(functions, getTourDates); const result await getTourDates({ comedianName }); fetchedDatesCache result.data; if (fetchedDatesCache.length 0) { container.innerHTML `p classtext-center text-gray-400>No upcoming dates found for ${comedianName}./p>`; return; } let html fetchedDatesCache.map((date, index) > { const eventDate new Date(date.date._seconds * 1000); const formattedDate eventDate.toLocaleDateString(undefined, { month: short, day: numeric, year: numeric }); return ` div classflex items-center gap-3 p-2 rounded-md hover:bg-gray-700/50> input typecheckbox iddate-${index} data-index${index} classfetched-date-checkbox h-4 w-4 rounded border-gray-300 text-cyan-500 focus:ring-cyan-500 checked> label fordate-${index} classtext-sm flex-grow> span classfont-bold>${formattedDate}/span> - ${date.venue}, ${date.city} /label> /div> `; }).join(); html + `button typebutton idsave-fetched-dates-btn classbtn btn-primary w-full mt-4 py-2 text-sm>Add Selected Dates/button>`; container.innerHTML html; document.getElementById(save-fetched-dates-btn).addEventListener(click, handleSaveFetchedDates); } catch (error) { console.error(Error calling Cloud Function:, error); const a document.createElement(a); a.href https://console.firebase.google.com/project/_/functions; a.target _blank; a.rel noopener noreferrer; a.className text-cyan-400 hover:underline; a.textContent Firebase Console; const errorMsg ` div classtext-left text-sm space-y-2> p classfont-bold text-base>Ticketmaster fetch failed with an internal error./p> p>This usually means theres an issue with the code classtext-xs bg-gray-900 p-1 rounded>getTourDates/code> Cloud Function on the server. Please check the following:/p> ul classlist-disc list-inside space-y-1 pl-2> li>The function is deployed correctly in the ${a.outerHTML}./li> li>Your Firebase project is on the strong>Blaze (pay-as-you-go)/strong> plan, which is required for making external API calls./li> li>A valid Ticketmaster API key is set as an environment variable for the function./li> li>Check the functions logs in the Firebase Console for more specific error details./li> /ul> /div> `; showToast(errorMsg, error); container.innerHTML `p classtext-center text-red-400>Error fetching dates. See notification for details./p>`; } }; const handleSaveFetchedDates async () > { const user getEffectiveUser(); if (!user) { showToast(You must be logged in., error); return; } const selectedCheckboxes document.querySelectorAll(.fetched-date-checkbox:checked); if (selectedCheckboxes.length 0) { showToast(Please select at least one date to add., error); return; } const batch writeBatch(db); let datesAdded 0; selectedCheckboxes.forEach(checkbox > { const index parseInt(checkbox.dataset.index, 10); const dateData fetchedDatesCacheindex; if (dateData) { const docRef doc(getPublicCollection(tourDates)); const dataToSave { ...dateData, date: new Timestamp(dateData.date._seconds, dateData.date._nanoseconds), createdAt: serverTimestamp(), authorId: user.uid }; batch.set(docRef, dataToSave); datesAdded++; } }); try { await batch.commit(); showToast(`${datesAdded} tour dates added successfully!`); document.getElementById(fetched-dates-container).innerHTML ; document.getElementById(fetch-comedian-name).value ; } catch (error) { console.error(Error saving fetched dates:, error); showToast(Failed to save dates., error); } }; const setupAuthForms () > { const loginForm document.getElementById(login-form); const signupForm document.getElementById(signup-form); const loginTab document.getElementById(login-tab-btn); const signupTab document.getElementById(signup-tab-btn); const devLoginBtn document.getElementById(dev-login-btn); if (!loginForm) return; loginForm.addEventListener(submit, handleLogin); signupForm.addEventListener(submit, handleSignup); if (devLoginBtn) { devLoginBtn.addEventListener(click, handleDevLogin); } loginTab.addEventListener(click, () > { loginForm.classList.remove(hidden); signupForm.classList.add(hidden); loginTab.classList.add(text-white, border-cyan-400); loginTab.classList.remove(text-gray-500, border-transparent); signupTab.classList.add(text-gray-500, border-transparent); signupTab.classList.remove(text-white, border-cyan-400); }); signupTab.addEventListener(click, () > { signupForm.classList.remove(hidden); loginForm.classList.add(hidden); signupTab.classList.add(text-white, border-cyan-400); signupTab.classList.remove(text-gray-500, border-transparent); loginTab.classList.add(text-gray-500, border-transparent); loginTab.classList.remove(text-white, border-cyan-400); }); }; const setupAdminForms () > { const newsForm document.getElementById(add-news-form); const videoForm document.getElementById(add-video-form); const tourForm document.getElementById(add-tour-form); const adminButtons document.querySelectorAll(.admin-form-btn); const fetchDatesBtn document.getElementById(fetch-tour-dates-btn); if (!newsForm) return; newsForm.addEventListener(submit, handleAddNews); videoForm.addEventListener(submit, handleAddVideo); tourForm.addEventListener(submit, handleAddTourDate); if (fetchDatesBtn) { fetchDatesBtn.addEventListener(click, handleFetchTourDates); } const forms newsForm, videoForm, tourForm; adminButtons.forEach(button > { button.addEventListener(click, () > { const formIdToShow button.dataset.form; forms.forEach(form > { if (form.id formIdToShow) { form.classList.remove(hidden); } else { form.classList.add(hidden); } }); }); }); }; const handleStripeCheckout async (productId, variantId, price, name, image) > { showToast(Redirecting to checkout...); try { const createStripeCheckoutSession httpsCallable(functions, createStripeCheckoutSession); const result await createStripeCheckoutSession({ productId, variantId, price, name, image, quantity: 1, // The success and cancel URLs are where Stripe will redirect the user after payment. successUrl: window.location.origin + window.location.pathname + #shop, cancelUrl: window.location.origin + window.location.pathname + #shop, }); const session result.data; if (session.url) { window.location.href session.url; } else { throw new Error(Failed to create a checkout session.); } } catch (error) { console.error(Stripe Checkout Error:, error); const errorMsg ` div classtext-left text-sm space-y-2> p classfont-bold text-base>Stripe Checkout Failed/p> p>Could not redirect to payment page. This usually means the code classtext-xs bg-gray-900 p-1 rounded>createStripeCheckoutSession/code> function on the server has an error./p> ul classlist-disc list-inside space-y-1 pl-2> li>Ensure the function is deployed and your project is on the strong>Blaze/strong> plan./li> li>Make sure your Stripe API keys (secret and publishable) are set as an environment variable for the function in the Firebase Console./li> li>Check the functions logs for specific errors./li> /ul> /div> `; showToast(errorMsg, error); } }; // --- App Initialization --- const initializeAppAndListeners async () > { if (appInitialized) return; appInitialized true; document.body.addEventListener(click, async (e) > { const navLink e.target.closest(data-page); if (navLink) { e.preventDefault(); navigate(navLink.dataset.page); } const videoThumb e.target.closest(.video-thumbnail-container); if (videoThumb) { const videoUrl videoThumb.dataset.videoUrl; const title videoThumb.dataset.title; const embedUrl videoUrl.replace(watch?v, embed/); const videoHTML `div classvideo-responsive>iframe src${embedUrl}?autoplay1 allowautoplay; encrypted-media allowfullscreen>/iframe>/div>`; openModal(title, videoHTML); } const productCardImage e.target.closest(.product-image-container); if (productCardImage) { const productCard productCardImage.closest(data-product-id); const productId productCard.dataset.productId; try { showToast(Loading product details...); const getPrintfulProductDetails httpsCallable(functions, getPrintfulProductDetails); const result await getPrintfulProductDetails({ productId }); const product result.data; const variantsHTML product.variants.map(variant > { const retailPrice calculateRetailPrice(variant.price); return ` div classp-2 border border-gray-700 rounded-md flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2> span classflex-grow>${variant.name}/span> div classflex items-center gap-4> span classfont-bold text-lg text-white>$${retailPrice}/span> button classbtn btn-secondary !rounded-md py-1 px-3 text-sm buy-now-btn data-product-id${product.id} data-variant-id${variant.id} data-price${retailPrice} data-name${product.name} - ${variant.name} data-image${product.imageUrl || variant.imageUrl}> Buy /button> /div> /div> `; }).join(); const modalHTML ` div classgrid grid-cols-1 md:grid-cols-2 gap-6 items-start> div> img src${product.imageUrl} alt${product.name} classw-full h-auto rounded-lg object-cover> /div> div classflex flex-col h-full> h2 classtext-3xl font-bold text-white mb-4>${product.name}/h2> h3 classtext-lg font-bold text-gray-300 mb-2>Available Options:/h3> div classspace-y-2 max-h-60 overflow-y-auto pr-2>${variantsHTML}/div> p classtext-xs text-gray-500 mt-4>Products are fulfilled by Printful./p> /div> /div> `; openModal(product.name, modalHTML); } catch (error) { console.error(Error getting product details:, error); showToast(Could not load product details., error); } } const buyButton e.target.closest(.buy-now-btn); if (buyButton) { e.stopPropagation(); // Prevent modal from opening if buy button on card is clicked const { productId, variantId, price, name, image } buyButton.dataset; handleStripeCheckout(productId, variantId, price, name, image); } }); document.getElementById(mobile-menu-button).addEventListener(click, () > { document.getElementById(mobile-menu).classList.toggle(hidden); document.getElementById(menu-open-icon).classList.toggle(hidden); document.getElementById(menu-close-icon).classList.toggle(hidden); }); document.getElementById(logout-btn).addEventListener(click, handleLogout); document.getElementById(footer-newsletter-form).addEventListener(submit, handleNewsletterSubmit); window.addEventListener(popstate, (e) > { const page (e.state && e.state.page) || window.location.hash.replace(#,) || home; navigate(page); }); onAuthStateChanged(auth, (user) > { updateAuthUI(getEffectiveUser()); }); const initialPage window.location.hash.replace(#, ) || home; navigate(initialPage); }; document.addEventListener(DOMContentLoaded, () > { const firebaseConfig typeof __firebase_config ! undefined ? JSON.parse(__firebase_config) : { // Fallback config for local development if __firebase_config is not injected apiKey: YOUR_API_KEY, authDomain: YOUR_AUTH_DOMAIN, projectId: YOUR_PROJECT_ID, storageBucket: YOUR_STORAGE_BUCKET, messagingSenderId: YOUR_MESSAGING_SENDER_ID, appId: YOUR_APP_ID }; const app initializeApp(firebaseConfig); db getFirestore(app); auth getAuth(app); functions getFunctions(app); setLogLevel(debug); // Enable Firestore debug logging if (typeof __initial_auth_token ! undefined && __initial_auth_token) { signInWithCustomToken(auth, __initial_auth_token) .then(() > initializeAppAndListeners()) .catch(err > { console.error(Custom token sign-in failed:, err); signInAnonymously(auth).then(() > initializeAppAndListeners()); }); } else { signInAnonymously(auth) .then(() > initializeAppAndListeners()) .catch(err > console.error(Anonymous sign-in failed:, err)); } }); /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
]