1<!doctype html> 2<html lang="en" class="scroll-smooth"> 3 4<head> 5 <meta charset="utf-8"> 6 <meta name="viewport" content="width=device-width, initial-scale=1"> 7 <title>InkArtists.com - Updates, notes, and studio moments</title> 8 <meta name="description" content="Discover tattoo artists, studios, and events worldwide on InkArtists."> 9 <meta name="robots" content="index,follow"> 10 <meta property="og:type" content="website"> 11 <meta property="og:site_name" content="InkArtists"> 12 <meta property="og:title" content="InkArtists.com - Updates, notes, and studio moments"> 13 <meta property="og:description" content="Discover tattoo artists, studios, and events worldwide on InkArtists."> 14 <meta property="og:url" content="https://www.inkartists.com/blog"> 15 <meta name="twitter:card" content="summary_large_image"> 16 <meta name="twitter:title" content="InkArtists.com - Updates, notes, and studio moments"> 17 <meta name="twitter:description" content="Discover tattoo artists, studios, and events worldwide on InkArtists."> 18 <link rel="alternate" hreflang="en" 19 href="https://www.inkartists.com/blog"> 20 <link rel="alternate" hreflang="es" 21 href="https://www.inkartists.com/es/blog"> 22 <link rel="alternate" hreflang="fr" 23 href="https://www.inkartists.com/fr/blog"> 24 <link rel="alternate" hreflang="pt" 25 href="https://www.inkartists.com/pt/blog"> 26 <link rel="alternate" hreflang="ja" 27 href="https://www.inkartists.com/ja/blog"> 28 <link rel="alternate" hreflang="ko" 29 href="https://www.inkartists.com/ko/blog"> 30 <link rel="alternate" hreflang="de" 31 href="https://www.inkartists.com/de/blog"> 32 <link rel="alternate" hreflang="it" 33 href="https://www.inkartists.com/it/blog"> 34 <link rel="alternate" hreflang="nl" 35 href="https://www.inkartists.com/nl/blog"> 36 <link rel="alternate" hreflang="ru" 37 href="https://www.inkartists.com/ru/blog"> 38 <link rel="alternate" hreflang="tr" 39 href="https://www.inkartists.com/tr/blog"> 40 <link rel="alternate" hreflang="pl" 41 href="https://www.inkartists.com/pl/blog"> 42 <link rel="alternate" hreflang="x-default" 43 href="https://www.inkartists.com/blog"> 44 45
45<script> 46 (function () { 47 const forceDark = false; 48 const stored = window.localStorage ? localStorage.getItem('ink-theme') : null; 49 const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; 50 const theme = forceDark ? 'dark' : (stored || (prefersDark ? 'dark' : 'light')); 51 document.documentElement.setAttribute('data-theme', theme); 52 })(); 53 </script>
53 54 55 56
56<script> 57 (function () { 58 try { 59 var secure = location.protocol === 'https:' ? '; Secure' : ''; 60 var year = 60 * 60 * 24 * 365; 61 var set = function (name, value, maxAge) { 62 document.cookie = name + '=' + encodeURIComponent(value) 63 + '; path=/; max-age=' + maxAge + '; SameSite=Lax' + secure; 64 }; 65 var read = function (name) { 66 var match = document.cookie.match('(?:^|; )' + name + '=([^;]*)'); 67 return match ? decodeURIComponent(match[1]) : null; 68 }; 69 70 var deviceId = read('ia_did'); 71 if (!deviceId || !/^[A-Za-z0-9_-]{16,64}$/.test(deviceId)) { 72 var bytes = new Uint8Array(16); 73 (window.crypto || window.msCrypto).getRandomValues(bytes); 74 deviceId = Array.prototype.map.call(bytes, function (b) { 75 return ('0' + b.toString(16)).slice(-2); 76 }).join(''); 77 } 78 // Re-set every load so the year rolls forward on active users. 79 set('ia_did', deviceId, year); 80 81 var zone = null; 82 if (window.Intl && Intl.DateTimeFormat) { 83 zone = Intl.DateTimeFormat().resolvedOptions().timeZone || null; 84 } 85 if (zone) { 86 set('ia_tz', zone, year); 87 } 88 // getTimezoneOffset() counts minutes *behind* UTC; the backend 89 // stores minutes east of it. 90 set('ia_tzo', String(-new Date().getTimezoneOffset()), year); 91 } catch (e) { 92 // A browser with cookies or crypto locked down still gets the 93 // site; it just arrives without a device fingerprint. 94 } 95 })(); 96 </script>
96 97 98 99 <link rel="preload" as="style" href="https://inkartists.com/build/assets/app-gynZxSks.css" /><link rel="preload" as="style" href="https://inkartists.com/build/assets/app-jWmyelEn.css" /><link rel="modulepreload" as="script" href="https://inkartists.com/build/assets/app-D90LKBrw.js" /><link rel="stylesheet" href="https://inkartists.com/build/assets/app-gynZxSks.css" /><link rel="stylesheet" href="https://inkartists.com/build/assets/app-jWmyelEn.css" />
99<script type="module" src="https://inkartists.com/build/assets/app-D90LKBrw.js"></script>
99
99<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
99 100 101 <meta name="csrf-token" content="5mNMtSeAK8ErMh3KLEwAOEKBdSzlYZoUWli8a9y1"> 102 103 <!-- Favicon + cool stuff --> 104 <link rel="icon" type="image/png" href="https://inkartists.com/storage/images/favicon/favicon-96x96.png" sizes="96x96" /> 105 <link rel="icon" type="image/svg+xml" href="https://inkartists.com/storage/images/favicon/favicon.svg" /> 106 <link rel="shortcut icon" href="https://inkartists.com/storage/images/favicon/favicon.ico" /> 107 <link rel="apple-touch-icon" sizes="180x180" href="https://inkartists.com/storage/images/favicon/apple-touch-icon.png" /> 108 <meta name="apple-mobile-web-app-title" content="InkArtists" /> 109 <link rel="manifest" href="https://inkartists.com/storage/images/favicon/site.webmanifest" /> 110 111 <!-- Google tag (gtag.js) -->
vendor: 68 bytes, lines 111-112
111 112 <script async src="https://www.googletagmanager.com/gtag/js?id=
112G-H8P3EF5QTG
vendor: 16 bytes, lines 112-113
112"></script> 113
113<script> 114
vendor: 154 bytes, lines 114-117
114window.dataLayer = window.dataLayer || []; 115 function gtag() { dataLayer.push(arguments); } 116 gtag('js', new Date()); 117 gtag('config', '
117G-H8P3EF5QTG
vendor: 8 bytes, lines 117-118
117'); 118
118</script>
118 119 <style> 120 html[data-theme="light"] .home-light-bg { 121 background-image: url('https://inkartists.com/storage/images/background/light_bg.jpg'); 122 background-size: cover; 123 background-position: center; 124 background-attachment: fixed; 125 background-color: rgba(248, 250, 252, 0.78); 126 background-blend-mode: lighten; 127 } 128 129 html[data-theme="light"] .home-hero { 130 background-color: #f8fafc; 131 background-image: url('https://inkartists.com/storage/images/background/light_bg.jpg'); 132 background-size: cover; 133 background-position: center; 134 } 135 136 html[data-theme="light"] .home-hero .home-hero-media { 137 background-image: url('https://inkartists.com/storage/images/background/light_bg.jpg'); 138 background-size: cover; 139 background-position: center; 140 } 141 142 html[data-theme="light"] .home-hero .home-hero-media video { 143 opacity: 0; 144 } 145 146 html[data-theme="light"] .home-hero .home-hero-media video, 147 html[data-theme="light"] .home-hero .home-hero-media img { 148 filter: saturate(0.9) brightness(1.08); 149 opacity: 0; 150 } 151 152 html[data-theme="light"] .home-hero .home-hero-overlay { 153 background: linear-gradient(to bottom, rgba(248, 250, 252, 0.35), rgba(248, 250, 252, 0.2), rgba(248, 250, 252, 0.12)) !important; 154 } 155 156 html[data-theme="light"] .home-hero .home-hero-noise { 157 opacity: 0.08 !important; 158 mix-blend-mode: normal !important; 159 } 160 161 html[data-theme="light"] .home-hero .home-hero-studio-overlay { 162 background-color: rgba(248, 250, 252, 0.85) !important; 163 } 164 165 html[data-theme="light"] .home-hero .home-hero-title-gradient { 166 background-image: linear-gradient(90deg, #0f172a, #155e75, #1d4ed8) !important; 167 color: transparent !important; 168 -webkit-text-fill-color: transparent !important; 169 } 170 171 html[data-theme="light"] .home-hero .text-white, 172 html[data-theme="light"] .home-hero .text-white\/90, 173 html[data-theme="light"] .home-hero .text-white\/80, 174 html[data-theme="light"] .home-hero .text-white\/75, 175 html[data-theme="light"] .home-hero .text-white\/70, 176 html[data-theme="light"] .home-hero .text-white\/60, 177 html[data-theme="light"] .home-hero .text-gray-300, 178 html[data-theme="light"] .home-hero .text-gray-200, 179 html[data-theme="light"] .home-hero .text-emerald-200, 180 html[data-theme="light"] .home-hero .text-cyan-300, 181 html[data-theme="light"] .home-hero .text-purple-300 { 182 color: #0f172a !important; 183 } 184 185 html[data-theme="light"] .home-hero .home-cta-button { 186 color: #ffffff !important; 187 } 188 189 html[data-theme="light"] .home-hero .bg-white\/10 { 190 background-color: rgba(15, 23, 42, 0.06) !important; 191 } 192 193 html[data-theme="light"] .home-hero .bg-white\/20 { 194 background-color: rgba(15, 23, 42, 0.1) !important; 195 } 196 197 html[data-theme="light"] .home-hero .border-white\/20, 198 html[data-theme="light"] .home-hero .border-white\/30, 199 html[data-theme="light"] .home-hero .border-white\/60 { 200 border-color: rgba(15, 23, 42, 0.16) !important; 201 } 202 203 html[data-theme="light"] .featured-cta-primary { 204 color: #ffffff !important; 205 background-color: #0f172a !important; 206 } 207 208 html[data-theme="light"] .featured-cta-primary:hover { 209 background-color: #111827 !important; 210 } 211 </style> 212</head> 213 214 <body class="min-h-screen bg-gray-50 text-gray-900 flex flex-col "> 215 216 <header x-data="{ 217 scrolled: false, 218 userMenu: false, 219 guestMenu: false, 220 scrollToWaitlist() { 221 const target = document.getElementById('waitlist'); 222 const email = document.getElementById('waitlist-email'); 223 if (target) { 224 target.scrollIntoView({ behavior: 'smooth' }); 225 } 226 if (email) { 227 setTimeout(() => email.focus({ preventScroll: true }), 400); 228 } 229 }, 230 openLogin() { 231 window.__inkAccountType = ''; 232 window.dispatchEvent(new CustomEvent('ink-login-open', { 233 detail: { accountType: '' } 234 })); 235 } 236}" x-init="scrolled = window.scrollY > 50" 237 @scroll.window="scrolled = window.scrollY > 50" 238 class="bg-gradient-to-br border-b/10" 239 :class="scrolled ? 'bg-white/80 border-white/10' : ''"> 240 241 <div class="max-w-7xl mx-auto px-6 py-5 flex items-center justify-between"> 242 243 <div class="flex items-center gap-4"> 244 <!-- Logo --> 245 <a href="https://inkartists.com" class="relative flex items-center gap-3 group"> 246 <img src="https://inkartists.com/storage/images/logo/logo_long_black.svg" 247 alt="Ink Artists" 248 class="h-10 w-auto object-contain transition group-hover:scale-105 "> 249 </a> 250 251 <a href="https://inkartists.com/artists/search" 252 class="hidden lg:flex items-center gap-3 rounded-full border border-white/60 bg-white/80 px-5 py-2 text-sm font-semibold uppercase tracking-[0.3em] text-gray-900 transition hover:-translate-y-0.5 hover:bg-white"> 253 Tattoo Artists 254 </a> 255 <a href="https://inkartists.com/tattoos" 256 class="hidden lg:flex items-center gap-3 rounded-full border border-white/60 bg-white/80 px-5 py-2 text-sm font-semibold uppercase tracking-[0.3em] text-gray-900 transition hover:-translate-y-0.5 hover:bg-white"> 257 Tattoos 258 </a> 259 <a href="https://inkartists.com/jobs" 260 class="hidden lg:flex items-center gap-3 rounded-full border border-white/60 bg-white/80 px-5 py-2 text-sm font-semibold uppercase tracking-[0.3em] text-gray-900 transition hover:-translate-y-0.5 hover:bg-white"> 261 Jobs 262 </a> 263 <a href="https://inkartists.com/tattoo-events" 264 class="hidden lg:flex items-center gap-3 rounded-full border border-white/60 bg-white/80 px-5 py-2 text-sm font-semibold uppercase tracking-[0.3em] text-gray-900 transition hover:-translate-y-0.5 hover:bg-white"> 265 Tattoo Events 266 </a> 267 </div> 268 269 <!-- CTA Button (glows harder on scroll) --> 270 <div class="flex items-center gap-3 relative" @click.outside="guestMenu = false"> 271 <button type="button" @click="guestMenu = !guestMenu" 272 class="lg:hidden inline-flex h-11 w-11 items-center justify-center rounded-xl border border-white/60 bg-white/80 text-gray-900 hover:bg-white shadow-sm transition"> 273 <i class="fa-solid fa-bars"></i> 274 </button> 275 <a href="https://inkartists.com/login" 276 class="hidden lg:inline-flex items-center gap-2 rounded-xl border border-white/60 bg-white/80 px-5 py-3 text-sm font-semibold text-gray-900 shadow-sm hover:-translate-y-0.5 hover:bg-white transition"> 277 Login 278 </a> 279 <button type="button" @click="openLogin()" 280 class="hidden lg:inline-flex relative items-center gap-3 px-6 py-3 rounded-xl font-bold shadow-xl transition-all hover:scale-105 text-black bg-gradient-to-r from-cyan-400 to-purple-400 hover:from-cyan-300 hover:to-purple-300">
281 <span class="relative z-10">Sign up</span> 282 <span class="absolute inset-0 rounded-full bg-white/20 blur-xl animate-pulse"></span> 283 </button> 284 <div x-show="guestMenu" x-transition 285 class="lg:hidden absolute right-0 top-full mt-3 w-56 rounded-2xl border shadow-2xl overflow-hidden z-50 border-white/60 bg-white/95 backdrop-blur-xl"> 286 <div class="px-4 py-3 border-b border-slate-100"> 287 <p class="text-xs font-semibold uppercase tracking-[0.2em] text-slate-500"> 288 Explore 289 </p> 290 </div> 291 <div class="py-2"> 292 <a href="https://inkartists.com/artists/search" 293 class="flex items-center gap-2 px-4 py-2 text-sm font-semibold text-slate-800 hover:bg-slate-100"> 294 <i class="fa-solid fa-users text-slate-500"></i> 295 Tattoo Artists 296 </a> 297 <a href="https://inkartists.com/tattoos" 298 class="flex items-center gap-2 px-4 py-2 text-sm font-semibold text-slate-800 hover:bg-slate-100"> 299 <i class="fa-solid fa-image text-slate-500"></i> 300 Tattoos 301 </a> 302 <a href="https://inkartists.com/jobs" 303 class="flex items-center gap-2 px-4 py-2 text-sm font-semibold text-slate-800 hover:bg-slate-100"> 304 <i class="fa-solid fa-briefcase text-slate-500"></i> 305 Jobs 306 </a> 307 <a href="https://inkartists.com/tattoo-events" 308 class="flex items-center gap-2 px-4 py-2 text-sm font-semibold text-slate-800 hover:bg-slate-100"> 309 <i class="fa-solid fa-calendar-days text-slate-500"></i> 310 Tattoo Events 311 </a> 312 <a href="https://inkartists.com/login" 313 class="flex items-center gap-2 px-4 py-2 text-sm font-semibold text-slate-800 hover:bg-slate-100"> 314 <i class="fa-solid fa-right-to-bracket text-slate-500"></i> 315 Login 316 </a> 317 </div> 318 <div class="px-4 py-3 border-t border-slate-100"> 319 <button type="button" @click="openLogin(); guestMenu = false" 320 class="w-full inline-flex items-center justify-center rounded-xl px-4 py-2 text-sm font-semibold text-white bg-slate-900 hover:bg-slate-800"> 321 Sign up 322 </button> 323 </div> 324 </div> 325 </div> 326 327 </div> 328 329</header> 330 331 332 333 <main class="flex-1 "> 334 <div class="min-h-screen bg-gradient-to-b from-amber-50 via-white to-white py-12"> 335 <div class="max-w-6xl mx-auto px-4 space-y-10"> 336 <div class="flex flex-col gap-4"> 337 <div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"> 338 <div class="space-y-3"> 339 <p class="text-xs uppercase tracking-[0.35em] text-amber-700">InkArtists Log</p> 340 <h1 class="text-4xl md:text-5xl font-black text-gray-900">Updates, notes, and studio moments</h1> 341 </div> 342 </div> 343 <p class="text-base md:text-lg text-gray-600 max-w-2xl"> 344 Short updates from the team. New features, artist highlights, and little wins worth sharing. 345 </p> 346 </div> 347 348 <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> 349 <a href="https://inkartists.com/blog/2026-02-20/inkartists-studios-search-tattoo-shops-artists-styles-and-tags-in-one-place/6" 350 class="group rounded-3xl border border-gray-100 bg-white shadow-xl overflow-hidden hover:-translate-y-1 transition"> 351 <div class="h-44 bg-gray-100 overflow-hidden"> 352 <img src="https://ink-storage.fra1.cdn.digitaloceanspaces.com/blog/covers/r5D0x2IWdKmzJRb14QZGznHYwRnlgkA3C6zVOAXy.png" alt="InkArtists Studios: Search Tattoo Shops, Artists, Styles, and Tags in One Place" 353 class="w-full h-full object-cover group-hover:scale-105 transition duration-300"> 354 </div> 355 <div class="p-6 space-y-3"> 356 <div class="flex items-center gap-2 text-xs text-gray-500">
357 <span class="uppercase tracking-[0.2em]">Post</span> 358 <span>â¢</span> 359 <span>Feb 20, 2026</span> 360 </div> 361 <h2 class="text-xl font-bold text-gray-900 group-hover:text-amber-700 transition"> 362 InkArtists Studios: Search Tattoo Shops, Artists, Styles, and Tags in One Place 363 </h2> 364 <p class="text-sm text-gray-600"> 365 The new /studios directory is live, and it is built to make tattoo shop discovery feel effortless. If you are searching for the best tattoo... 366 </p> 367 <div class="text-sm font-semibold text-amber-700">Read â</div> 368 </div> 369 </a> 370 <a href="https://inkartists.com/blog/2026-01-21/tattoo-events-are-live-on-inkartists/5" 371 class="group rounded-3xl border border-gray-100 bg-white shadow-xl overflow-hidden hover:-translate-y-1 transition"> 372 <div class="h-44 bg-gray-100 overflow-hidden"> 373 <img src="https://ink-storage.fra1.cdn.digitaloceanspaces.com/blog/covers/PTmLtUNYFH9npzjrEt6eKQrolm43oqBoremMKt7I.jpg" alt="Tattoo Events Are Live on InkArtists" 374 class="w-full h-full object-cover group-hover:scale-105 transition duration-300"> 375 </div> 376 <div class="p-6 space-y-3"> 377 <div class="flex items-center gap-2 text-xs text-gray-500"> 378 <span class="uppercase tracking-[0.2em]">Post</span> 379 <span>â¢</span> 380 <span>Jan 21, 2026</span> 381 </div> 382 <h2 class="text-xl font-bold text-gray-900 group-hover:text-amber-700 transition"> 383 Tattoo Events Are Live on InkArtists 384 </h2> 385 <p class="text-sm text-gray-600"> 386 The Tattoo Events section is now live on InkArtists. It is the fastest way to find conventions, guest shows, and recurring editions without... 387 </p> 388 <div class="text-sm font-semibold text-amber-700">Read â</div> 389 </div> 390 </a> 391 <a href="https://inkartists.com/blog/2026-01-08/guest-spots-zero-friction/4" 392 class="group rounded-3xl border border-gray-100 bg-white shadow-xl overflow-hidden hover:-translate-y-1 transition"> 393 <div class="h-44 bg-gray-100 overflow-hidden"> 394 <img src="https://ink-storage.fra1.cdn.digitaloceanspaces.com/blog/covers/ag1V8BE0nlH6xPmCz2nDAkECkmAjRkfgxuquin21.png" alt="Guest Spots, Zero Friction" 395 class="w-full h-full object-cover group-hover:scale-105 transition duration-300"> 396 </div> 397 <div class="p-6 space-y-3"> 398 <div class="flex items-center gap-2 text-xs text-gray-500"> 399 <span class="uppercase tracking-[0.2em]">Post</span> 400 <span>â¢</span> 401 <span>Jan 08, 2026</span> 402 </div> 403 <h2 class="text-xl font-bold text-gray-900 group-hover:text-amber-700 transition"> 404 Guest Spots, Zero Friction 405 </h2> 406 <p class="text-sm text-gray-600"> 407 Weâve got a fresh update for artists who move around: guest spots are now easier to manage and way easier to share. 408 409You can update your g... 410 </p> 411 <div class="text-sm font-semibold text-amber-700">Read â</div> 412 </div> 413 </a> 414 <a href="https://inkartists.com/blog/2026-01-03/introducing-inkartists-jobs-guest-spots-residencies-and-studio-openings/3" 415 class="group rounded-3xl border border-gray-100 bg-white shadow-xl overflow-hidden hover:-translate-y-1 transition"> 416 <div class="h-44 bg-gray-100 overflow-hidden"> 417 <img src="https://ink-storage.fra1.cdn.digitaloceanspaces.com/blog/covers/FecWPhNeFtfeW5iZl31skz72tx9ICLNGtzTpYLQT.jpg" alt="Introducing InkArtists Jobs: guest spots, residencies, and studio openings" 418 class="w-full h-full object-cover group-hover:scale-105 transition duration-300"> 419 </div> 420 <div class="p-6 space-y-3"> 421 <div class="flex items-center gap-2 text-xs text-gray-500">
422 <span class="uppercase tracking-[0.2em]">Post</span> 423 <span>â¢</span> 424 <span>Jan 03, 2026</span> 425 </div> 426 <h2 class="text-xl font-bold text-gray-900 group-hover:text-amber-700 transition"> 427 Introducing InkArtists Jobs: guest spots, residencies, and studio openings 428 </h2> 429 <p class="text-sm text-gray-600"> 430 We just launched the InkArtists Jobs board, a dedicated space to match tattoo artists and studios for guest spots, residencies, and openings... 431 </p> 432 <div class="text-sm font-semibold text-amber-700">Read â</div> 433 </div> 434 </a> 435 <a href="https://inkartists.com/blog/2026-01-02/the-studio-era-is-here-claim-organize-grow/2" 436 class="group rounded-3xl border border-gray-100 bg-white shadow-xl overflow-hidden hover:-translate-y-1 transition"> 437 <div class="h-44 bg-gray-100 overflow-hidden"> 438 <img src="https://ink-storage.fra1.cdn.digitaloceanspaces.com/blog/covers/CTt9ooKxZSF9aeTCBO8vwH0BSsnLJ60vL7fk8LiE.jpg" alt="The Studio Era Is Here: Claim, Organize, Grow" 439 class="w-full h-full object-cover group-hover:scale-105 transition duration-300"> 440 </div> 441 <div class="p-6 space-y-3"> 442 <div class="flex items-center gap-2 text-xs text-gray-500"> 443 <span class="uppercase tracking-[0.2em]">Post</span> 444 <span>â¢</span> 445 <span>Jan 02, 2026</span> 446 </div> 447 <h2 class="text-xl font-bold text-gray-900 group-hover:text-amber-700 transition"> 448 The Studio Era Is Here: Claim, Organize, Grow 449 </h2> 450 <p class="text-sm text-gray-600"> 451 Studios are no longer just a pin on a map. They're the heartbeat of the scene, where culture gets built and talent levels up. Clients want c... 452 </p> 453 <div class="text-sm font-semibold text-amber-700">Read â</div> 454 </div> 455 </a> 456 <a href="https://inkartists.com/blog/2025-12-29/what-is-inkartists/1" 457 class="group rounded-3xl border border-gray-100 bg-white shadow-xl overflow-hidden hover:-translate-y-1 transition"> 458 <div class="h-44 bg-gray-100 overflow-hidden"> 459 <img src="https://ink-storage.fra1.cdn.digitaloceanspaces.com/blog/covers/KLbjbKzaLjzJdpripmYAIXwD0eJqpFaL8VY99nHv.jpg" alt="What is InkArtists?" 460 class="w-full h-full object-cover group-hover:scale-105 transition duration-300"> 461 </div> 462 <div class="p-6 space-y-3"> 463 <div class="flex items-center gap-2 text-xs text-gray-500"> 464 <span class="uppercase tracking-[0.2em]">Post</span> 465 <span>â¢</span> 466 <span>Dec 29, 2025</span> 467 </div> 468 <h2 class="text-xl font-bold text-gray-900 group-hover:text-amber-700 transition"> 469 What is InkArtists? 470 </h2> 471 <p class="text-sm text-gray-600"> 472 InkArtists is a booking platform built for tattoo artists. It's not a marketplace that takes a cut of your work. It's a clean way to show yo... 473 </p> 474 <div class="text-sm font-semibold text-amber-700">Read â</div> 475 </div> 476 </a> 477 </div> 478 479 <div class="pt-4"> 480 481 </div> 482 </div> 483 </div> 484 </main> 485 486 <footer class="relative overflow-hidden backdrop-blur-2xl border-t bg-black/95 border-white/10"> 487 488 <div aria-hidden="true" class="absolute inset-0 opacity-20"> 489 <div class="absolute -top-40 -left-40 w-96 h-96 bg-purple-600 rounded-full blur-3xl animate-blob"></div> 490 <div 491 class="absolute top-20 -right-40 w-80 h-80 bg-cyan-500 rounded-full blur-3xl animate-blob animation-delay-2000"> 492 </div> 493 </div> 494 495 <div class="relative max-w-7xl mx-auto px-6 py-16"> 496 <div class="grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-5 ga
496p-8 sm:gap-10"> 497 498 <!-- Brand + Tagline --> 499 <div 500 class="col-span-2 order-1 lg:order-none sm:col-span-1 md:col-span-1 flex flex-col items-center md:items-start lg:items-center gap-3"> 501 <a href="https://inkartists.com" class="inline-flex"> 502 <img src="https://inkartists.com/storage/images/logo/logo_white_text.png" 503 alt="Ink Artists" class="h-35 w-auto object-contain"> 504 </a> 505 <p class="text-sm text-gray-400 max-w-xs text-center lg:text-center md:text-left"> 506 The global home for the tattoo industry. No DM chaosâwork smart. 507 </p> 508 509 <div class="mt-6 flex gap-4 justify-center md:justify-start"> 510 <a href="https://www.instagram.com/inkartistscom" target="_blank" rel="noopener" 511 class="w-10 h-10 rounded-full bg-white/10 backdrop-blur hover:bg-white/20 flex items-center justify-center transition"> 512 <span class="sr-only">Instagram</span> 513 <i class="fa-brands fa-instagram text-white"></i> 514 </a> 515 </div> 516 <div class="mt-2 flex flex-wrap gap-2 justify-center md:justify-start"> 517 <a href="https://play.google.com/store/apps/details?id=com.inkartists.app" target="_blank" rel="noopener noreferrer" 518 class="inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-xs font-semibold text-gray-200 transition hover:border-white/25 hover:bg-white/10 hover:text-white"> 519 <i class="fa-brands fa-google-play text-sm text-emerald-300"></i> 520 Get it on Google Play 521 </a> 522 <a href="https://apps.apple.com/app/inkartists-tattoo-booking-app/id6793135266" target="_blank" rel="noopener noreferrer" 523 class="inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-xs font-semibold text-gray-200 transition hover:border-white/25 hover:bg-white/10 hover:text-white"> 524 <i class="fa-brands fa-apple text-sm text-cyan-300"></i> 525 Download on the App Store 526 </a> 527 </div> 528 </div> 529 530 <!-- Quick Links --> 531 <div class="col-span-1 order-2 lg:order-none text-center sm:text-left"> 532 <h3 class="font-bold text-white mb-3">Discover</h3> 533 <ul class="space-y-2 text-sm text-gray-300"> 534 <li><a href="https://inkartists.com/partners" 535 class="block px-1 py-1 hover:text-white transition">For Artists</a> 536 </li> 537 <li><a href="https://inkartists.com/clients" 538 class="block px-1 py-1 hover:text-white transition">For Clients</a> 539 </li> 540 <li><a href="https://inkartists.com/for-studios" 541 class="block px-1 py-1 hover:text-white transition">For Studios</a> 542 </li> 543 <li><a href="https://inkartists.com/pricing" 544 class="block px-1 py-1 hover:text-white transition">Pricing</a> 545 </li> 546 <li><a href="https://inkartists.com/support" 547 class="block px-1 py-1 hover:text-white transition">Support</a> 548 </li> 549 </ul> 550 </div> 551 552 <!-- Discover --> 553 <div class="col-span-1 order-3 lg:order-none text-center sm:text-left"> 554 <h3 class="font-bold text-white mb-3">Platform</h3> 555 <ul class="space-y-2 text-sm text-gray-300"> 556 <li><a href="https://inkartists.com/studios" 557 class="block px-1 py-1 hover:text-white transition">Tattoo Studios</a> 558 </li> 559 <li><a href="https://inkartists.com/jobs" 560 class="block px-1 py-1 hover:text-white transition">Jobs</a> 561 </li> 562 <li><a href="https://inkartists.com/tattoo-apprenticeships" 563 class="block px-1 py-1 hover:text-white transition">Apprenticeships</a> 564 </li> 565 <li><a href="https://inkartists.com/tattoo-guest-spots" 566 class="block px-1 py-1 hover:text-white transition">Guest spots</a> 567 </li> 568 <li><a href="https://inkartists.com/marketplace" 569 class="block px-1 py-1 hover:text-white transition">Equipment marketplace</a> 570 </li> 571 <li><a href="https://inkartists.com/guides" 572 class="block px-1 py-1 hover:text-white transition">Licensing guides</a> 573 </li> 574 <li><a href="https://inkartists.com/artists" 575 class="block px-1 py-1 hover:text-white transition">Tattoo artists</a> 576 </li> 577 <li><a href="https://inkartists.com/tattoos" 578 class="block px-1 py-1 hover:text-white transition">Tattoo Designs</a> 579 </li> 580 </ul> 581 </div> 582 583 <!-- Company --> 584 <div class="col-span-1 order-4 lg:order-none text-center sm:text-left"> 585 <h3 class="font-bold text-white mb-3">Company</h3> 586 <ul class="space-y-2 text-sm text-gray-300"> 587 <li><a href="https://inkartists.com/company" 588 class="block px-1 py-1 hover:text-white transition">Company</a> 589 </li> 590 <li><a href="https://inkartists.com/blog" 591 class="block px-1 py-1 hover:text-white transition">Blog</a> 592 </li> 593 <li><a href="https://inkartists.com/instagram" 594 class="block px-1 py-1 hover:text-white transition">Our Instagram</a> 595 </li> 596 <li><a href="https://inkartists.com/support" 597 class="block px-1 py-1 hover:text-white transition">Contact</a> 598 </li> 599 </ul> 600 </div> 601 602 <!-- Legal + Final CTA --> 603 <div class="col-span-2 order-5 lg:order-none lg:col-span-1 text-center sm:text-left lg:text-center"> 604 <div 605 class="flex flex-wrap items-start justify-between gap-6 lg:block lg:space-y-6 lg:text-center"> 606 <div class="min-w-[140px]"> 607 <h3 class="font-bold text-white mb-3 lg:mb-3">Legal</h3> 608 <ul class="flex flex-col gap-2 text-sm text-gray-300"> 609 <li><a href="https://inkartists.com/privacy" 610 class="px-1 py-1 hover:text-white transition">Privacy Policy</a> 611 </li> 612 <li><a href="https://inkartists.com/terms" 613 class="px-1 py-1 hover:text-white transition">Terms of Service</a> 614 </li> 615 <li><a href="https://inkartists.com/cookies" 616 class="px-1 py-1 hover:text-white transition">Cookie Policy</a> 617 </li> 618 </ul> 619 </div> 620 <div class="min-w-[160px] lg:mt-6"> 621 <label class="block text-xs uppercase tracking-[0.2em] text-gray-400 mb-2" 622 for="footer-locale">Language</label> 623 <select id="footer-locale" 624 class="w-full rounded-xl bg-white/10 border border-white/10 text-white text-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-cyan-400/60" 625 onchange="window.location = this.value"> 626 <option value="https://inkartists.com/locale/de?to=%2Fblog" > 627 Deutsch 628 </option> 629 <option value="https://inkartists.com/locale/en?to=%2Fblog" selected> 630 English 631 </option> 632 <option value="https://inkartists.com/locale/es?to=%2Fblog" > 633 Español 634 </option> 635 <option value="https://inkartists.com/locale/fr?to=%2Fblog" > 636 Français 637 </option> 638 <option value="https://inkartists.com/locale/it?to=%2Fblog" > 639 Italiano 640 </option> 641 <option value="https://inkartists.com/locale/nl?to=%2Fblog" > 642 Nederlands 643 </option> 644 <option value="https://inkartists.com/locale/pl?to=%2Fblog" > 645 Polski 646 </option> 647 <option value="https://inkartists.com/locale/pt?to=%2Fblog" > 648 Português 649 </option> 650 <option value="https://inkartists.com/locale/tr?to=%2Fblog" > 651 Türkçe 652 </option> 653 <option value="https://inkartists.com/locale/ru?to=%2Fblog" > 654 Ð ÑÑÑкий 655 </option> 656 <option value="https://inkartists.com/locale/ko?to=%2Fblog" > 657 íêµì´ 658 </option> 659 <option value="https://inkartists.com/locale/ja?to=%2Fblog" > 660 æ¥æ¬èª 661 </option> 662 </select> 663 <noscript> 664 <ul class="mt-2 space-y-1 text-sm text-gray-300"> 665 <li> 666 <a href="https://inkartists.com/locale/de?to=%2Fblog" class="hover:text-white">
666Deutsch</a> 667 </li> 668 <li> 669 <a href="https://inkartists.com/locale/en?to=%2Fblog" class="hover:text-white">English</a> 670 </li> 671 <li> 672 <a href="https://inkartists.com/locale/es?to=%2Fblog" class="hover:text-white">Español</a> 673 </li> 674 <li> 675 <a href="https://inkartists.com/locale/fr?to=%2Fblog" class="hover:text-white">Français</a> 676 </li> 677 <li> 678 <a href="https://inkartists.com/locale/it?to=%2Fblog" class="hover:text-white">Italiano</a> 679 </li> 680 <li> 681 <a href="https://inkartists.com/locale/nl?to=%2Fblog" class="hover:text-white">Nederlands</a> 682 </li> 683 <li> 684 <a href="https://inkartists.com/locale/pl?to=%2Fblog" class="hover:text-white">Polski</a> 685 </li> 686 <li> 687 <a href="https://inkartists.com/locale/pt?to=%2Fblog" class="hover:text-white">Português</a> 688 </li> 689 <li> 690 <a href="https://inkartists.com/locale/tr?to=%2Fblog" class="hover:text-white">Türkçe</a> 691 </li> 692 <li> 693 <a href="https://inkartists.com/locale/ru?to=%2Fblog" class="hover:text-white">Ð ÑÑÑкий</a> 694 </li> 695 <li> 696 <a href="https://inkartists.com/locale/ko?to=%2Fblog" class="hover:text-white">íêµì´</a> 697 </li> 698 <li> 699 <a href="https://inkartists.com/locale/ja?to=%2Fblog" class="hover:text-white">æ¥æ¬èª</a> 700 </li> 701 </ul> 702 </noscript> 703 </div> 704 <div 705 class="min-w-[200px] lg:mt-10 w-full lg:w-auto p-5 rounded-2xl border bg-gradient-to-r from-cyan-500/10 to-purple-500/10 border-white/10"> 706 <p class="text-xs text-gray-300"> 707 © 2026 InkArtists. All rights reserved. 708 </p> 709 <p class="mt-2 text-xs font-medium text-cyan-300"> 710 Made for the culture ð¤ 711 </p> 712 </div> 713 </div> 714 </div> 715 </div> 716 717 </div> 718</footer> 719 720 <div x-data="{ 721 open: false, 722 lastClosedAt: 0, 723 close() { 724 this.open = false; 725 this.lastClosedAt = Date.now(); 726 window.dispatchEvent(new Event('ink-login-close')); 727 }, 728 openIfAllowed() { 729 if (Date.now() - this.lastClosedAt < 250) { 730 return; 731 } 732 this.open = true; 733 } 734 }" x-show="open" x-cloak x-transition.opacity 735 x-on:ink-login-open.window="openIfAllowed()" 736 x-on:keydown.escape.window="close()" 737 @click.self="close()" 738 @mousedown.self.prevent="close()" 739 class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm"> 740 <div class="relative z-10 w-full max-w-md rounded-3xl border border-white/70 bg-white/95 shadow-2xl ring-1 ring-black/5" 741 data-ink-login-card @click.stop 742 x-data="inkAccountStepper(null, false)" 743 x-on:ink-login-open.window="applyType($event.detail && $event.detail.accountType)" 744 x-on:ink-login-close.window="onClose()"> 745 <div class="p-6"> 746 <div x-show="step === 1" x-cloak> 747 <div class="mb-2"> 748 <h4 class="text-xl font-black text-gray-900">Choose your account type</h4>
749 <span class="mt-2 block h-1 w-16 rounded-full bg-gradient-to-r from-indigo-500 via-pink-500 to-amber-400"></span> 750 </div> 751 <p class="text-sm text-gray-600 mb-5">Who are you signing up as?</p> 752 <div class="mb-6"> 753 <p class="text-[11px] uppercase tracking-[0.2em] text-gray-400">I am a</p> 754 <div class="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-3"> 755 <button type="button" @click="select('artist')" 756 :class="selected === 'artist' ? 'bg-gray-900 text-white border-gray-900' : 'border-gray-200 text-gray-700 hover:border-gray-300 hover:text-gray-900'" 757 class="rounded-2xl border px-4 py-3 text-sm font-semibold transition"> 758 Artist 759 </button> 760 <button type="button" @click="select('client')" 761 :class="selected === 'client' ? 'bg-gray-900 text-white border-gray-900' : 'border-gray-200 text-gray-700 hover:border-gray-300 hover:text-gray-900'" 762 class="rounded-2xl border px-4 py-3 text-sm font-semibold transition"> 763 Client 764 </button> 765 <button type="button" @click="select('studio')" 766 :class="selected === 'studio' ? 'bg-gray-900 text-white border-gray-900' : 'border-gray-200 text-gray-700 hover:border-gray-300 hover:text-gray-900'" 767 class="rounded-2xl border px-4 py-3 text-sm font-semibold transition"> 768 Studio 769 </button> 770 </div> 771 </div> 772 <button type="button" @click="step = 2" :disabled="!selected" 773 class="w-full rounded-2xl bg-gray-900 px-4 py-3 text-sm font-semibold text-white transition hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed"> 774 Continue 775 </button> 776 </div> 777 778 <input type="hidden" data-ink-login-redirect value="https://inkartists.com/blog"> 779 780 <div x-show="step === 2" x-cloak class="space-y-3"> 781 <div class="mb-2"> 782 <h4 class="text-xl font-black text-gray-900">Sign in to continue</h4> 783 <span class="mt-2 block h-1 w-16 rounded-full bg-gradient-to-r from-indigo-500 via-pink-500 to-amber-400"></span> 784 </div> 785 <p class="text-sm text-gray-600 mb-5">Choose a sign-in method to continue.</p> 786 <div class="flex flex-wrap items-center justify-between gap-3"> 787 <button type="button" @click="step = 1" x-show="!skipType" x-cloak 788 class="inline-flex items-center gap-2 text-xs font-semibold text-gray-500 hover:text-gray-800"> 789 <i class="fa-solid fa-arrow-left"></i> 790 Back 791 </button> 792 <span class="text-xs font-semibold text-gray-500" x-text="selectedLabel()"></span> 793 </div> 794 <button 795 type="button" 796 class="group w-full rounded-2xl border border-gray-200 bg-white px-4 py-3 font-semibold text-gray-900 shadow-sm transition hover:-translate-y-0.5 hover:border-gray-300 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-indigo-500/40" 797 data-ink-login="google" 798 data-ink-login-idle="Continue with Google" 799 data-ink-login-loading="Signing inâ¦" 800 > 801 <span class="flex items-center justify-center gap-2"> 802 <span class="inline-flex h-8 w-8 items-center justify-center rounded-full bg-gray-100 text-gray-700 group-hover:bg-gray-200"> 803 <i class="fa-brands fa-google"></i> 804 </span>
805 <span>Continue with Google</span> 806 </span> 807 </button> 808 <button 809 type="button" 810 class="group w-full rounded-2xl border border-gray-200 bg-white px-4 py-3 font-semibold text-gray-900 shadow-sm transition hover:-translate-y-0.5 hover:border-gray-300 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-indigo-500/40" 811 data-ink-login="facebook" 812 data-ink-login-idle="Continue with Facebook" 813 data-ink-login-loading="Signing inâ¦" 814 > 815 <span class="flex items-center justify-center gap-2"> 816 <span class="inline-flex h-8 w-8 items-center justify-center rounded-full bg-blue-50 text-blue-600 group-hover:bg-blue-100"> 817 <i class="fa-brands fa-facebook-f"></i> 818 </span> 819 <span>Continue with Facebook</span> 820 </span> 821 </button> 822 <div class="flex items-center gap-3 text-xs text-gray-400"> 823 <span class="h-px flex-1 bg-gray-200"></span> 824 <span>or use email</span> 825 <span class="h-px flex-1 bg-gray-200"></span> 826 </div> 827 <form data-ink-login-email-form class="space-y-3"> 828 <input type="hidden" data-ink-email-mode value="signup"> 829 <div class="grid gap-3"> 830 <input type="email" name="email" autocomplete="email" required 831 class="w-full rounded-2xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-400 focus:border-indigo-400 focus:ring-indigo-400" 832 placeholder="Email address"> 833 <input type="password" name="password" autocomplete="current-password" required 834 class="w-full rounded-2xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-900 placeholder:text-gray-400 focus:border-indigo-400 focus:ring-indigo-400" 835 placeholder="Password"> 836 </div> 837 <button type="submit" data-ink-email-action="signin" 838 class="w-full rounded-2xl bg-gray-900 px-4 py-3 text-sm font-semibold text-white transition hover:bg-gray-800"> 839 <span class="inline-flex items-center justify-center gap-2"> 840 <i class="fa-solid fa-envelope" data-ink-email-action-icon></i> 841 <span data-ink-email-action-label>Sign in with email</span> 842 </span> 843 </button> 844 <button type="button" data-ink-email-toggle 845 class="w-full text-sm font-semibold text-gray-600 hover:text-gray-900"> 846 <span class="inline-flex items-center justify-center gap-2"> 847 <i class="fa-solid fa-user-plus" data-ink-email-toggle-icon></i> 848 <span data-ink-email-toggle-label>Donât have an account? Create one</span> 849 </span> 850 </button> 851 <p data-ink-login-email-success class="text-emerald-600 text-sm text-center hidden"></p> 852 <p data-ink-login-email-error class="text-red-600 text-sm text-center hidden"></p> 853 </form> 854 855 </div> 856 857 <p data-ink-login-error class="text-red-600 text-sm text-center hidden mt-4"></p> 858 </div> 859 </div> 860 </div> 861 862
862<script src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js" defer></script>
862 863
864<script type="module"> 865 import { 866 initializeApp 867 } from "https://www.gstatic.com/firebasejs/10.11.1/firebase-app.js"; 868 import { 869 initializeAuth, 870 indexedDBLocalPersistence, 871 browserLocalPersistence, 872 inMemoryPersistence, 873 browserPopupRedirectResolver, 874 signInWithCredential, 875 linkWithCredential, 876 linkWithPopup, 877 linkWithRedirect, 878 OAuthProvider, 879 GoogleAuthProvider, 880 FacebookAuthProvider, 881 fetchSignInMethodsForEmail, 882 signInWithPopup, 883 signInWithRedirect, 884 getRedirectResult, 885 createUserWithEmailAndPassword, 886 signInWithEmailAndPassword, 887 sendEmailVerification, 888 signOut, 889 } from "https://www.gstatic.com/firebasejs/10.11.1/firebase-auth.js"; 890 891 function gtagSafe() { 892 try { 893 window.gtag && window.gtag(...arguments); 894 } catch {} 895 } 896 897 function isAccountExistsError(err) { 898 const code = err && err.code ? String(err.code) : ''; 899 return code === 'auth/account-exists-with-different-credential'; 900 } 901 902 function formatAuthError(err) { 903 const code = err && err.code ? String(err.code) : ''; 904 if (code === 'auth/account-exists-with-different-credential') { 905 return 'This email is already registered with another sign-in method. Please sign in with that method first.'; 906 } 907 if (code === 'auth/popup-closed-by-user') { 908 return 'Sign-in popup was closed.'; 909 } 910 return (err && err.message) || 'Something went wrong.'; 911 } 912 913 function labelsFromMethods(methods = []) { 914 const map = { 915 'google.com': 'Google', 916 'facebook.com': 'Facebook', 917 'apple.com': 'Apple', 918 'password': 'Email', 919 }; 920 const labels = methods.map((m) => map[m] || m).filter(Boolean); 921 return Array.from(new Set(labels)); 922 } 923 924 function fallbackProviderList() { 925 const labels = []; 926 if (document.querySelector('[data-ink-login="google"]')) labels.push('Google'); 927 if (document.querySelector('[data-ink-login-email-form]')) labels.push('Email'); 928 return labels; 929 } 930 931 function labelFromProviderId(providerId) { 932 if (providerId === 'google.com') return 'Google'; 933 if (providerId === 'facebook.com') return 'Facebook'; 934 if (providerId === 'apple.com') return 'Apple'; 935 if (providerId === 'password') return 'Email'; 936 return ''; 937 } 938 939 // --- helpers --- 940 async function sha256Hex(str) { 941 const enc = new TextEncoder().encode(str); 942 const hash = await crypto.subtle.digest("SHA-256", enc); 943 return [...new Uint8Array(hash)].map(b => b.toString(16).padStart(2, "0")).join(""); 944 } 945 946 function randomNonce(len = 32) { 947 const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 948 const arr = new Uint8Array(len); 949 crypto.getRandomValues(arr); 950 return [...arr].map(b => chars[b % chars.length]).join(""); 951 } 952 async function waitForApple() { 953 if (window.AppleID?.auth) return; 954 await new Promise(resolve => { 955 const t = setInterval(() => { 956 if (window.AppleID?.auth) { 957 clearInterval(t); 958 resolve(); 959 } 960 }, 50); 961 setTimeout(() => { 962 clearInterval(t); 963 resolve(); 964 }, 3000); 965 }); 966 } 967 968 // --- one-time init --- 969 if (!window.__inkAuthInit) { 970 window.__inkAuthInit = true; 971 972 const firebaseConfig = { 973 apiKey: "AIzaSyBnuY0IrO5vbzBiALrFvOoifF-dGCNDRc4", 974 authDomain: "www.inkartists.com", 975 projectId: "inkartists-1d4b3", 976 appId: "1:156698075123:web:548d07ee6c6fc9cec07c86", 977 messagingSenderId: "156698075123", 978 storageBucket: "inkartists-1d4b3.firebasestorage.app", 979 }; 980 981 const app = initializeApp(firebaseConfig); 982 const auth = initializeAuth(app, { 983 persistence: [indexedDBLocalPersistence, browserLocalPersistence, inMemoryPersistence], 984 popupRedirectResolver: browserPopupRedirectResolver, 985 }); 986 window.__inkFirebaseAuth = auth; 987 window.__inkLastProviderId = ''; 988 window.__inkLastProviderLabel = ''; 989 window.__inkAuthInFlight = false;
990 991 // redirect param persistence 992 const getRedirectParam = () => { 993 try { 994 return new URL(window.location.href).searchParams.get("redirect") || ""; 995 } catch { 996 return ""; 997 } 998 }; 999 const persistRedirect = v => v && sessionStorage.setItem("ink_redirect", v); 1000 const readPersistedRedirect = () => sessionStorage.getItem("ink_redirect") || ""; 1001 const clearPersistedRedirect = () => sessionStorage.removeItem("ink_redirect"); 1002 const applyLoginRedirect = (value) => { 1003 if (!value) return; 1004 persistRedirect(value); 1005 document.querySelectorAll('[data-ink-login-redirect]').forEach((el) => { 1006 el.value = value; 1007 }); 1008 }; 1009 window.addEventListener('ink-login-open', (event) => { 1010 const redirectValue = event?.detail?.redirect || ''; 1011 if (!redirectValue) return; 1012 applyLoginRedirect(redirectValue); 1013 }); 1014 1015 // env detection 1016 const ua = navigator.userAgent || ""; 1017 const isIOS = /iP(hone|od|ad)/.test(ua) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1); 1018 1019 // Apple popup (desktop) 1020 async function signInWithAppleDesktopPopup() { 1021 await waitForApple(); 1022 const rawNonce = randomNonce(); 1023 const hashed = await sha256Hex(rawNonce); 1024 window.AppleID.auth.init({ 1025 clientId: "login.inkartists.com", // <-- Apple Service ID for InkArtists 1026 scope: "name email", 1027 redirectURI: "https://www.inkartists.com/__/auth/handler", // or https://auth.inkartists.com/__/auth/handler 1028 usePopup: true, 1029 nonce: hashed, 1030 }); 1031 const res = await window.AppleID.auth.signIn(); 1032 const idToken = res?.authorization?.id_token; 1033 if (!idToken) throw new Error("apple_missing_id_token"); 1034 const provider = new OAuthProvider("apple.com"); 1035 const cred = provider.credential({ 1036 idToken, 1037 rawNonce 1038 }); 1039 const userCred = await signInWithCredential(auth, cred); 1040 return userCred.user; 1041 } 1042 1043 function markAuthProgress() { 1044 try { 1045 sessionStorage.setItem("ink_auth_progress", "1"); 1046 } catch {} 1047 window.dispatchEvent(new Event("ink-auth-progress")); 1048 window.__inkAuthTimeout && clearTimeout(window.__inkAuthTimeout); 1049 window.__inkAuthTimeout = setTimeout(() => { 1050 try { 1051 sessionStorage.removeItem("ink_auth_progress"); 1052 } catch {} 1053 window.dispatchEvent(new Event("ink-auth-complete")); 1054 }, 15000); 1055 } 1056 1057 function clearAuthProgress() { 1058 try { 1059 sessionStorage.removeItem("ink_auth_progress"); 1060 } catch {} 1061 window.__inkAuthInFlight = false; 1062 window.dispatchEvent(new Event("ink-auth-complete")); 1063 } 1064 1065 function showGlobalLoginError(message) { 1066 if (window.__inkLastAuthError === message) return; 1067 window.__inkLastAuthError = message; 1068 const showOnce = (selector) => { 1069 const nodes = Array.from(document.querySelectorAll(selector)); 1070 if (!nodes.length) return; 1071 nodes.forEach((el) => el.classList.add('hidden')); 1072 const target = nodes.find((el) => !el.closest('[hidden]')) || nodes[0]; 1073 target.textContent = message; 1074 target.classList.remove('hidden'); 1075 }; 1076 const hasLoginError = document.querySelector('[data-ink-login-error]'); 1077 showOnce(hasLoginError ? '[data-ink-login-error]' : '[data-ink-login-email-error]'); 1078 } 1079 1080 function persistPendingLink(pending) { 1081 try { 1082 sessionStorage.setItem('ink_pending_link', JSON.stringify(pending)); 1083 } catch {} 1084 } 1085 1086 function restorePendingLink() { 1087 try { 1088 const raw = sessionStorage.getItem('ink_pending_link'); 1089 if (!raw) return null; 1090 const data = JSON.parse(raw); 1091 if (!data || !data.providerId) return null; 1092 let credential = null; 1093 if (data.providerId === 'facebook.com' && data.accessToken) { 1094 credential = FacebookAuthProvider.credential(data.accessToken); 1095 } else if (data.providerId === 'google.com' && (data.idToken || data.accessToken)) { 1096 credential = GoogleAuthProvider.credential(data.idToken || null, data.accessToken || null); 1097 } else if (data.providerId === 'apple.com' && data.idToken) { 1098 const provider = new OAuthProvider('apple.com'); 1099 credential = provider.credential({ 1100 idToken: data.idToken, 1101 rawNonce: data.rawNonce || undefined 1102 }); 1103 } 1104 if (!credential) return null; 1105 return { 1106 credential, 1107 email: data.email || '', 1108 attempted: data.attempted || labelFromProviderId(data.providerId), 1109 providerId: data.providerId 1110 }; 1111 } catch { 1112 return null; 1113 } 1114 } 1115 1116 function capturePendingCredential(err, attemptedProviderLabel = '', providerIdOverride = '') { 1117 const email = err?.customData?.email || err?.email || ''; 1118 const providerId = providerIdOverride || err?.customData?.providerId || window.__inkLastProviderId || ''; 1119 let credential = null; 1120 if (providerId === 'facebook.com') { 1121 credential = FacebookAuthProvider.credentialFromError(err); 1122 } else if (providerId === 'google.com') { 1123 credential = GoogleAuthProvider.credentialFromError(err); 1124 } else if (providerId === 'apple.com') { 1125 credential = OAuthProvider.credentialFromError(err); 1126 } 1127 if (!credential) { 1128 try { 1129 } catch {} 1130 } 1131 if (credential) { 1132 window.__inkPendingLink = { 1133 credential, 1134 email, 1135 attempted: attemptedProviderLabel || labelFromProviderId(providerId), 1136 providerId 1137 }; 1138 const payload = { 1139 providerId, 1140 email, 1141 attempted: attemptedProviderLabel || labelFromProviderId(providerId), 1142 accessToken: credential.accessToken || null, 1143 idToken: credential.idToken || null, 1144 rawNonce: credential.rawNonce || null 1145 }; 1146 persistPendingLink(payload); 1147 try { 1148 } catch {} 1149 } 1150 if (!credential && providerId) { 1151 const payload = { 1152 providerId, 1153 email, 1154 attempted: attemptedProviderLabel || labelFromProviderId(providerId), 1155 accessToken: null, 1156 idToken: null, 1157 rawNonce: null 1158 }; 1159 persistPendingLink(payload); 1160 window.__inkPendingLink = { 1161 credential: null, 1162 email, 1163 attempted: attemptedProviderLabel || labelFromProviderId(providerId), 1164 providerId 1165 }; 1166 } 1167 } 1168 1169 async function linkPendingCredential(user, providerLabel = '') { 1170 const pending = window.__inkPendingLink || restorePendingLink(); 1171 if (!pending || !pending.credential) return false;
1172 const pendingEmail = (pending.email || '').toLowerCase(); 1173 const userEmail = (user?.email || '').toLowerCase(); 1174 if (pendingEmail && userEmail && pendingEmail !== userEmail) { 1175 return false; 1176 } 1177 try { 1178 await linkWithCredential(user, pending.credential); 1179 window.__inkPendingLink = null; 1180 try { 1181 sessionStorage.removeItem('ink_pending_link'); 1182 } catch {} 1183 try { 1184 } catch {} 1185 return true; 1186 } catch (err) { 1187 const code = err && err.code ? String(err.code) : ''; 1188 if (code === 'auth/provider-already-linked' || code === 'auth/credential-already-in-use') { 1189 window.__inkPendingLink = null; 1190 try { 1191 sessionStorage.removeItem('ink_pending_link'); 1192 } catch {} 1193 return false; 1194 } 1195 try { 1196 } catch {} 1197 return false; 1198 } 1199 } 1200 1201 async function linkPendingProvider(user) { 1202 const pending = window.__inkPendingLink || restorePendingLink(); 1203 if (!pending || pending.credential || !pending.providerId) return { linked: false, redirecting: false }; 1204 const providerId = pending.providerId; 1205 let provider = null; 1206 if (providerId === 'facebook.com') { 1207 provider = new FacebookAuthProvider(); 1208 provider.setCustomParameters({ scope: 'public_profile,email' }); 1209 } else if (providerId === 'google.com') { 1210 provider = new GoogleAuthProvider(); 1211 } else if (providerId === 'apple.com') { 1212 provider = new OAuthProvider('apple.com'); 1213 } else { 1214 return { linked: false, redirecting: false }; 1215 } 1216 try { 1217 await linkWithPopup(user, provider); 1218 window.__inkPendingLink = null; 1219 try { 1220 sessionStorage.removeItem('ink_pending_link'); 1221 } catch {} 1222 return { linked: true, redirecting: false }; 1223 } catch (err) { 1224 try { 1225 } catch {} 1226 try { 1227 await linkWithRedirect(user, provider); 1228 return { linked: false, redirecting: true }; 1229 } catch {} 1230 return { linked: false, redirecting: false }; 1231 } 1232 } 1233 1234 // Expose helpers for handlers that may run outside this module's scope. 1235 window.linkPendingCredential = linkPendingCredential; 1236 window.linkPendingProvider = linkPendingProvider; 1237 window.clearAuthProgress = clearAuthProgress; 1238 window.showGlobalLoginError = showGlobalLoginError; 1239 1240 async function handleAccountExistsError(err) { 1241 const email = err?.customData?.email || err?.email || ''; 1242 const providerId = err?.customData?.providerId || window.__inkLastProviderId || ''; 1243 const attemptedProvider = labelFromProviderId(providerId) || window.__inkLastProviderLabel || ''; 1244 capturePendingCredential(err, attemptedProvider, providerId); 1245 if (!email) { 1246 showGlobalLoginError(formatAuthError(err)); 1247 return; 1248 } 1249 try { 1250 const methods = await fetchSignInMethodsForEmail(auth, email); 1251 try { 1252 } catch {} 1253 const labels = labelsFromMethods(methods); 1254 if (methods.length) { 1255 const providerList = (labels.length ? labels : methods).join(', '); 1256 showGlobalLoginError( 1257 'This email is already registered with another sign-in method.' + 1258 ' ' + 1259 'Please sign in with' + 1260 ' ' + 1261 providerList + 1262 (attemptedProvider ? ' ' + 'to link' + ' ' + attemptedProvider + '.' : '.') 1263 ); 1264 return; 1265 } 1266 const fallback = fallbackProviderList(); 1267 if (fallback.length) { 1268 showGlobalLoginError( 1269 'This email is already registered with another sign-in method.' + 1270 ' ' + 1271 'Please sign in with' + 1272 ' ' + 1273 fallback.join(', ') + 1274 (attemptedProvider ? ' ' + 'to link' + ' ' + attemptedProvider + '.' : '.') 1275 ); 1276 return; 1277 } 1278 } catch {} 1279 showGlobalLoginError(formatAuthError(err)); 1280 } 1281 1282 async function finishLogin(user, providerLabel = 'Unknown') { 1283 markAuthProgress(); 1284 const meta = user?.metadata || {}; 1285 const isNew = meta.creationTime && meta.lastSignInTime && (meta.creationTime === meta.lastSignInTime); 1286 try { 1287 } catch {} 1288 1289 // GA4 events 1290 gtagSafe('event', isNew ? 'sign_up' : 'login', { 1291 method: providerLabel, 1292 transport_type: 'beacon' 1293 }); 1294 gtagSafe('set', 'user_properties', { 1295 auth_provider: providerLabel, 1296 signup_flow: isNew ? 'new' : 'returning' 1297 }); 1298 1299 // server session on InkArtists 1300 const idToken = await user.getIdToken(); 1301 const clientRedirect = getRedirectParam() || readPersistedRedirect(); 1302 const linked = typeof linkPendingCredential === 'function' 1303 ? await linkPendingCredential(user, providerLabel) 1304 : false;
1305 if (!linked) { 1306 const linkProviderResult = typeof linkPendingProvider === 'function' 1307 ? await linkPendingProvider(user) 1308 : { linked: false, redirecting: false }; 1309 if (linkProviderResult.redirecting) { 1310 return; 1311 } 1312 } 1313 const xhr = new XMLHttpRequest(); 1314 xhr.open("POST", "/login/callback", 1315 true); // <-- InkArtists endpoint (same origin) 1316 xhr.withCredentials = true; 1317 xhr.setRequestHeader("Content-Type", "application/json"); 1318 xhr.setRequestHeader("Authorization", "Bearer " + idToken); 1319 xhr.setRequestHeader("X-CSRF-TOKEN", "5mNMtSeAK8ErMh3KLEwAOEKBdSzlYZoUWli8a9y1"); 1320 xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); 1321 xhr.onreadystatechange = function() { 1322 if (xhr.readyState !== 4) return; 1323 clearPersistedRedirect(); 1324 try { 1325 const body = JSON.parse(xhr.responseText || "{}"); 1326 try { 1327 } catch {} 1328 if (xhr.status === 200) { 1329 window.__inkAccountType = ''; 1330 const target = body.redirect || clientRedirect || window.location.href; 1331 if (target === window.location.href) window.location.reload(); 1332 else window.location.assign(target); 1333 } else { 1334 clearAuthProgress(); 1335 showGlobalLoginError(body.message || 'Something went wrong.'); 1336 gtagSafe('event', 'login_error', { 1337 method: providerLabel, 1338 flow: 'callback', 1339 message: 'callback_failed', 1340 transport_type: 'beacon' 1341 }); 1342 } 1343 } catch { 1344 try { 1345 } catch {} 1346 clearAuthProgress(); 1347 showGlobalLoginError('Something went wrong.'); 1348 gtagSafe('event', 'login_error', { 1349 method: providerLabel, 1350 flow: 'callback', 1351 message: 'callback_parse_error', 1352 transport_type: 'beacon' 1353 }); 1354 } 1355 }; 1356 let accountType = ""; 1357 try { 1358 accountType = window.__inkAccountType || ""; 1359 } catch {} 1360 xhr.send(JSON.stringify({ 1361 _token: "5mNMtSeAK8ErMh3KLEwAOEKBdSzlYZoUWli8a9y1", 1362 redirect: clientRedirect, 1363 idToken: idToken, 1364 account_type: accountType, 1365 provider: providerLabel 1366 })); 1367 } 1368 1369 window.__inkFinishLogin = finishLogin; 1370 1371 // finalize redirect flows 1372 getRedirectResult(auth).then(async (res) => { 1373 if (res && res.user) { 1374 markAuthProgress(); 1375 const providerId = res._tokenResponse?.providerId || res.providerId || res.operationType; 1376 const label = providerId?.includes('apple') ? 'Apple' : (providerId?.includes('google') ? 1377 'Google' : (providerId?.includes('facebook') ? 'Facebook' : 'Unknown')); 1378 try { 1379 } catch {} 1380 await finishLogin(res.user, label); 1381 } 1382 }).catch(async (err) => { 1383 clearAuthProgress(); 1384 if (isAccountExistsError(err)) { 1385 await handleAccountExistsError(err); 1386 return; 1387 } 1388 showGlobalLoginError(formatAuthError(err)); 1389 try { 1390 } catch {} 1391 }); 1392 1393 // public entry 1394 window.signInWithFirebase = async function(gateway = 1, redirectOverride = '') { 1395 if (window.__inkAuthInFlight) { 1396 return; 1397 } 1398 const hiddenRedirect = redirectOverride || document.querySelector('[data-ink-login-redirect]')?.value || ''; 1399 const redirectValue = getRedirectParam() || readPersistedRedirect() || hiddenRedirect; 1400 if (redirectValue) { 1401 persistRedirect(redirectValue); 1402 } 1403 window.__inkAuthInFlight = true; 1404 markAuthProgress(); 1405 1406 const isApple = gateway === 2; 1407 const isFacebook = gateway === 3; 1408 const isLoginPage = /\/login\/?$/.test(window.location.pathname || ''); 1409 const provider = isApple 1410 ? new OAuthProvider("apple.com") 1411 : (isFacebook ? new FacebookAuthProvider() : new GoogleAuthProvider()); 1412 const providerLabel = isApple ? 'Apple' : (isFacebook ? 'Facebook' : 'Google'); 1413 window.__inkLastProviderId = isApple ? 'apple.com' : (isFacebook ? 'facebook.com' : 'google.com'); 1414 window.__inkLastProviderLabel = providerLabel; 1415 if (isFacebook) { 1416 provider.setCustomParameters({ 1417 scope: 'public_profile,email' 1418 }); 1419 } 1420 1421 // Prefer redirect for Facebook except on the dedicated login page. 1422 if (isFacebook && !isLoginPage) { 1423 await signInWithRedirect(auth, provider); 1424 return; 1425 } 1426 1427 // iOS + Apple => redirect (Apple rules) 1428 if (isApple && isIOS) { 1429 await signInWithRedirect(auth, provider); 1430 return; 1431 } 1432 1433 try { 1434 try { 1435 } catch {} 1436 const result = await (isApple ? signInWithAppleDesktopPopup() : signInWithPopup(auth, provider) 1437 .then(r => r.user)); 1438 await finishLogin(result, providerLabel); 1439 } catch (e) { 1440 if (isAccountExistsError(e)) { 1441 clearAuthProgress(); 1442 await handleAccountExistsError(e); 1443 try { 1444 } catch {} 1445 return; 1446 } 1447 const errorCode = e && e.code ? String(e.code) : ''; 1448 if (errorCode === 'auth/cancelled-popup-request') { 1449 return; 1450 } 1451 if (errorCode === 'auth/popup-blocked') { 1452 await signInWithRedirect(auth, provider); 1453 return; 1454 } 1455 try { 1456 } catch {} 1457 gtagSafe('event', 'login_error', { 1458 method: providerLabel, 1459 flow: 'popup', 1460 message: String(e && e.code || e), 1461 transport_type: 'beacon' 1462 }); 1463 showGlobalLoginError(formatAuthError(e)); 1464 clearAuthProgress(); 1465 } 1466 }; 1467 } 1468 1469 // clear progress on page show 1470 (function() { 1471 const clear = () => { 1472 try { 1473 if (sessionStorage.getItem('ink_auth_progress')) { 1474 sessionStorage.removeItem('ink_auth_progress'); 1475 window.dispatchEvent(new Event('ink-auth-complete')); 1476 } 1477 } catch {} 1478 }; 1479 window.addEventListener('pageshow', clear); 1480 document.addEventListener('DOMContentLoaded', () => setTimeout(clear, 0)); 1481 })(); 1482 1483 window.addEventListener('DOMContentLoaded', () => { 1484 const googleBtns = document.querySelectorAll('[data-ink-login="google"]'); 1485 const facebookBtns = document.querySelectorAll('[data-ink-login="facebook"]'); 1486 const emailForms = document.querySelectorAll('[data-ink-login-email-form]'); 1487 1488 function setLoading(el, on = true) { 1489 if (!el) return; 1490 const idleText = el.getAttribute('data-ink-login-idle') || 'Continue with Google'; 1491 const loadingText = el.getAttribute('data-ink-login-loading') || 'Signing inâ¦'; 1492 el.disabled = on; 1493 el.classList.toggle('opacity-60', on); 1494 el.classList.toggle('pointer-events-none', on); 1495 el.textContent = on ? loadingText : idleText; 1496 } 1497 1498 googleBtns.forEach((btn) => { 1499 const card = btn.closest('[data-ink-login-card]'); 1500 const errorEl = card ? card.querySelector('[data-ink-login-error]') : null; 1501 const redirectInput = card ? card.querySelector('[data-ink-login-redirect]') : null; 1502 const redirectValue = redirectInput ? redirectInput.value : ''; 1503 1504 btn.addEventListener('click', async (e) => { 1505 e.preventDefault(); 1506 errorEl?.classList.add('hidden');
1507 setLoading(btn, true); 1508 try { 1509 await window.signInWithFirebase(1, redirectValue); // 1 = Google (your code) 1510 // If popup fails, your code falls back to redirect, so no more UI work needed here. 1511 } catch (err) { 1512 // Only hits if something throws before redirect. 1513 errorEl.textContent = (err && err.message) || 'Something went wrong.'; 1514 errorEl.classList.remove('hidden'); 1515 setLoading(btn, false); 1516 } 1517 }); 1518 }); 1519 1520 facebookBtns.forEach((btn) => { 1521 const card = btn.closest('[data-ink-login-card]'); 1522 const errorEl = card ? card.querySelector('[data-ink-login-error]') : null; 1523 const redirectInput = card ? card.querySelector('[data-ink-login-redirect]') : null; 1524 const redirectValue = redirectInput ? redirectInput.value : ''; 1525 1526 btn.addEventListener('click', async (e) => { 1527 e.preventDefault(); 1528 errorEl?.classList.add('hidden'); 1529 setLoading(btn, true); 1530 try { 1531 await window.signInWithFirebase(3, redirectValue); // 3 = Facebook 1532 } catch (err) { 1533 errorEl.textContent = (err && err.message) || 'Something went wrong.'; 1534 errorEl.classList.remove('hidden'); 1535 setLoading(btn, false); 1536 } 1537 }); 1538 }); 1539 1540 function setEmailLoading(form, on = true) { 1541 const buttons = form.querySelectorAll('button'); 1542 buttons.forEach((btn) => { 1543 btn.disabled = on; 1544 btn.classList.toggle('opacity-60', on); 1545 btn.classList.toggle('pointer-events-none', on); 1546 }); 1547 } 1548 1549 function showEmailMessage(form, type, message) { 1550 const errorEl = form.querySelector('[data-ink-login-email-error]'); 1551 const successEl = form.querySelector('[data-ink-login-email-success]'); 1552 if (errorEl) errorEl.classList.add('hidden'); 1553 if (successEl) successEl.classList.add('hidden'); 1554 1555 const target = type === 'success' ? successEl : errorEl; 1556 if (target) { 1557 target.textContent = message; 1558 target.classList.remove('hidden'); 1559 } 1560 } 1561 1562 emailForms.forEach((form) => { 1563 const modeInput = form.querySelector('[data-ink-email-mode]'); 1564 const toggleBtn = form.querySelector('[data-ink-email-toggle]'); 1565 const submitBtn = form.querySelector('[data-ink-email-action="signin"]'); 1566 const submitLabel = form.querySelector('[data-ink-email-action-label]'); 1567 const toggleLabel = form.querySelector('[data-ink-email-toggle-label]'); 1568 const submitIcon = form.querySelector('[data-ink-email-action-icon]'); 1569 const toggleIcon = form.querySelector('[data-ink-email-toggle-icon]'); 1570 1571 function syncEmailMode(mode) { 1572 if (modeInput) modeInput.value = mode; 1573 if (submitLabel) { 1574 submitLabel.textContent = mode === 'signup' 1575 ? 'Create account' 1576 : 'Sign in with email'; 1577 } 1578 if (toggleLabel) { 1579 toggleLabel.textContent = mode === 'signup' 1580 ? 'Already have an account? Sign in' 1581 : 'Donât have an account? Create one'; 1582 } 1583 if (submitIcon) { 1584 submitIcon.classList.remove('fa-envelope', 'fa-user-plus'); 1585 submitIcon.classList.add(mode === 'signup' ? 'fa-user-plus' : 'fa-envelope'); 1586 } 1587 if (toggleIcon) { 1588 toggleIcon.classList.remove('fa-user-plus', 'fa-right-to-bracket'); 1589 toggleIcon.classList.add(mode === 'signup' ? 'fa-right-to-bracket' : 'fa-user-plus'); 1590 } 1591 } 1592 1593 syncEmailMode(modeInput?.value || 'signin'); 1594 1595 toggleBtn?.addEventListener('click', () => {
1596 const current = modeInput?.value || 'signin'; 1597 syncEmailMode(current === 'signin' ? 'signup' : 'signin'); 1598 }); 1599 1600 form.addEventListener('submit', async (e) => { 1601 e.preventDefault(); 1602 const auth = window.__inkFirebaseAuth; 1603 if (!auth) return; 1604 1605 const email = (form.querySelector('input[name="email"]')?.value || '').trim(); 1606 const password = form.querySelector('input[name="password"]')?.value || ''; 1607 const mode = modeInput?.value || 'signin'; 1608 1609 showEmailMessage(form, 'error', ''); 1610 showEmailMessage(form, 'success', ''); 1611 setEmailLoading(form, true); 1612 1613 try { 1614 let userCred; 1615 if (mode === 'signup') { 1616 userCred = await createUserWithEmailAndPassword(auth, email, password); 1617 } else { 1618 userCred = await signInWithEmailAndPassword(auth, email, password); 1619 } 1620 1621 const user = userCred.user; 1622 await user.reload(); 1623 if (!user.emailVerified) { 1624 try { 1625 await sendEmailVerification(user); 1626 } catch {} 1627 await signOut(auth); 1628 syncEmailMode('signin'); 1629 showEmailMessage(form, 'success', 1630 'Check your email to verify your account before signing in.'); 1631 return; 1632 } 1633 1634 const finisher = window.__inkFinishLogin; 1635 if (!finisher) { 1636 throw new Error('login_finish_unavailable'); 1637 } 1638 await linkPendingCredential(user, 'Email'); 1639 await finisher(user, 'Email'); 1640 } catch (err) { 1641 const code = err && err.code ? String(err.code) : ''; 1642 const rawMessage = err?.message || err?.error?.message || ''; 1643 const normalized = `${code} ${rawMessage}`.toLowerCase(); 1644 const isEmailExists = normalized.includes('email_exists') 1645 || normalized.includes('email-already-in-use') 1646 || normalized.includes('email already in use'); 1647 let message = rawMessage || 'Something went wrong.'; 1648 if (isEmailExists) { 1649 let existingCred = null; 1650 try { 1651 existingCred = await signInWithEmailAndPassword(auth, email, password); 1652 } catch (signinErr) { 1653 const signinCode = signinErr && signinErr.code ? String(signinErr.code) : ''; 1654 const signinMessage = signinErr?.message || ''; 1655 const signinNormalized = `${signinCode} ${signinMessage}`.toLowerCase(); 1656 const isBadPassword = signinNormalized.includes('wrong-password') 1657 || signinNormalized.includes('invalid-credential') 1658 || signinNormalized.includes('invalid-login-credentials') 1659 || signinNormalized.includes('user-mismatch'); 1660 if (isBadPassword) { 1661 message = 'Incorrect email or password.'; 1662 } else { 1663 message = 'Email already in use. Try signing in instead.'; 1664 } 1665 showEmailMessage(form, 'error', message); 1666 return; 1667 } 1668 1669 const existingUser = existingCred.user; 1670 await existingUser.reload(); 1671 if (!existingUser.emailVerified) { 1672 try { 1673 await sendEmailVerification(existingUser); 1674 } catch {} 1675 await signOut(auth); 1676 syncEmailMode('signin'); 1677 showEmailMessage(form, 'success', 1678 'Check your email to verify your account before signing in.'); 1679 return; 1680 } 1681 1682 try { 1683 const finisher = window.__inkFinishLogin; 1684 if (!finisher) { 1685 throw new Error('login_finish_unavailable'); 1686 } 1687 if (typeof linkPendingCredential === 'function') { 1688 await linkPendingCredential(existingUser, 'Email'); 1689 } 1690 await finisher(existingUser, 'Email'); 1691 return; 1692 } catch (finishErr) { 1693 if (typeof clearAuthProgress === 'function') { 1694 clearAuthProgress(); 1695 } else { 1696 window.__inkAuthInFlight = false;
1697 window.dispatchEvent(new Event("ink-auth-complete")); 1698 } 1699 if (typeof showGlobalLoginError === 'function') { 1700 showGlobalLoginError('Something went wrong.'); 1701 } else { 1702 showEmailMessage(form, 'error', 'Something went wrong.'); 1703 } 1704 return; 1705 } 1706 } else if (code.includes('auth/user-not-found')) { 1707 message = 'No account found for that email.'; 1708 } else if (code.includes('auth/wrong-password') || code.includes('auth/invalid-credential')) { 1709 message = 'Incorrect email or password.'; 1710 } else if (code.includes('auth/weak-password')) { 1711 message = 'Password is too weak.'; 1712 } else if (code.includes('auth/invalid-email')) { 1713 message = 'Enter a valid email address.'; 1714 } 1715 showEmailMessage(form, 'error', message); 1716 } finally { 1717 setEmailLoading(form, false); 1718 } 1719 }); 1720 }); 1721 }); 1722</script>
1722 1723 1724
1724<script> 1725 function inkAccountStepper(initialType, skipType) { 1726 return { 1727 step: skipType || initialType ? 2 : 1, 1728 selected: initialType || '', 1729 skipType: !!skipType, 1730 init() { 1731 if (initialType) { 1732 this.selected = initialType; 1733 this.step = 2; 1734 window.__inkAccountType = initialType; 1735 return; 1736 } 1737 if (skipType) { 1738 this.step = 2; 1739 this.selected = ''; 1740 window.__inkAccountType = ''; 1741 } 1742 }, 1743 applyType(value) { 1744 if (!value) { 1745 if (!initialType) { 1746 this.selected = ''; 1747 this.step = skipType ? 2 : 1; 1748 window.__inkAccountType = ''; 1749 } 1750 return; 1751 } 1752 this.selected = value; 1753 this.step = 2; 1754 window.__inkAccountType = value; 1755 }, 1756 select(value) { 1757 this.selected = value; 1758 window.__inkAccountType = value; 1759 }, 1760 onClose() { 1761 this.step = (skipType || initialType) ? 2 : 1; 1762 this.selected = initialType || ''; 1763 if (initialType) { 1764 window.__inkAccountType = initialType; 1765 } else { 1766 window.__inkAccountType = ''; 1767 } 1768 }, 1769 selectedLabel() { 1770 if (!this.selected) { 1771 return ''; 1772 } 1773 const labels = { 1774 artist: 'Signing up as Artist', 1775 client: 'Signing up as Client', 1776 studio: 'Signing up as Studio', 1777 }; 1778 return labels[this.selected] || ''; 1779 }, 1780 }; 1781 } 1782 </script>
1782 1783 1784 1785 1786 </body> 1787 1788</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.