PageSourceSearch

https://app.viewrankai.com/login

html viewrankai.com collected 2026-09-24 14:56:02 UTC 24,913 bytes, 709 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3<head>
4  <meta charset="UTF-8">
5  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6  <!-- App page on app.viewrankai.com: keep it out of search. -->
7  <meta name="robots" content="noindex, follow">
8  <title>Login - ViewRank AI</title>
9  <link rel="icon" href="/favicon.svg" type="image/svg+xml">
10  <link rel="icon" href="/favicon.ico" sizes="32x32">
11  <link rel="apple-touch-icon" href="/apple-touch-icon.png">
12  <link rel="preconnect" href="https://fonts.googleapis.com">
13  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
14  <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&display=swap" rel="stylesheet">
15  <style>
16    :root {
17      --primary: #E87B35;
18      --primary-hover: #D66B28;
19      --bg: #FAFAFA;
20      --surface: #FFFFFF;
21      --surface-muted: #F5F5F5;
22      --border: #E8E8E8;
23      --text: #1A1A1A;
24      --text-secondary: #6B6B6B;
25      --text-muted: #9A9A9A;
26      --radius: 12px;
27      --radius-sm: 8px;
28      --shadow-lg: 0 20px 40px rgba(15, 23, 42, 0.12);
29    }
30    * { margin: 0; padding: 0; box-sizing: border-box; }
31    body {
32      min-height: 100vh;
33      display: flex;
34      align-items: center;
35      justify-content: center;
36      background: radial-gradient(circle at top, #FFF4ED 0, #FAFAFA 45%, #F3F4F6 100%);
37      font-family: 'DM Sans', -apple-system, system-ui, sans-serif;
38      color: var(--text);
39    }
40    .auth-card {
41      background: var(--surface);
42      border-radius: var(--radius);
43      box-shadow: var(--shadow-lg);
44      padding: 32px 32px 28px;
45      width: 100%;
46      max-width: 420px;
47      border: 1px solid var(--border);
48    }
49    .logo-row {
50      display: flex;
51      align-items: center;
52      gap: 10px;
53      justify-content: center;
54      margin-bottom: 16px;
55    }
56    .logo-icon {
57      width: 32px;
58      height: 32px;
59      border-radius: 8px;
60      flex-shrink: 0;
61      display: block;
62      object-fit: cover;
63    }
64    .logo-text {
65      font-weight: 700;
66      font-size: 18px;
67      color: var(--text);
68    }
69    .logo-badge {
70      font-size: 10px;
71      font-weight: 500;
72      padding: 2px 6px;
73      border-radius: 999px;
74      background: var(--surface-muted);
75      color: var(--text-muted);
76    }
77    .auth-title {
78      text-align: center;
79      font-size: 20px;
80      font-weight: 600;
81      margin-bottom: 4px;
82    }
83    .auth-subtitle {
84      text-align: center;
85      font-size: 13px;
86      color: var(--text-secondary);
87      margin-bottom: 20px;
88    }
89    .alert {
90      display: none;
91      padding: 10px 12px;
92      border-radius: var(--radius-sm);
93      font-size: 12px;
94      margin-bottom: 14px;
95    }
96    .alert-error {
97      background: #FEF2F2;
98      border: 1px solid #FCA5A5;
99      color: #B91C1C;
100    }
101    .alert-success {
102      background: #F0FDF4;
103      border: 1px solid #86EFAC;
104      color: #166534;
105    }
106    .form-group {
107      margin-bottom: 16px;
108    }
109    .form-label {
110      display: block;
111      font-size: 13px;
112      font-weight: 500;
113      color: var(--text-secondary);
114      margin-bottom: 6px;
115    }
116    .form-input {
117      width: 100%;
118      padding: 10px 12px;
119      border-radius: var(--radius-sm);
120      border: 1px solid var(--border);
121      font-size: 14px;
122      font-family: inherit;
123      color: var(--text);
124      background: #F9FAFB;
125      transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
126    }
127    .form-input::placeholder {
128      color: var(--text-muted);
129    }
130    .form-input:focus {
131      outline: none;
132      border-color: var(--primary);
133      background: #fff;
134      box-shadow: 0 0 0 3px rgba(232, 123, 53, 0.15);
135    }
136    .auth-button {
137      width: 100%;
138      margin-top: 4px;
139      padding: 10px 14px;
140      border-radius: var(--radius-sm);
141      border: none;
142      background: var(--primary);
143      color: #fff;
144      font-weight: 600;
145      font-size: 14px;
146      cursor: pointer;
147      transition: background 0.15s ease, transform 0.05s ease, box-shadow 0.15s ease;
148      box-shadow: 0 8px 20px rgba(232, 123, 53, 0.25);
149    }
150    .auth-button:hover {
151      background: var(--primary-hover);
152      transform: translateY(-1px);
153    }
154    .auth-button:disabled {
155      opacity: 0.6;
156      cursor: default;
157      box-shadow: none;
158    }
159    .auth-button.secondary {
160      margin-top: 10px;
161      background: #fff;
162      color: var(--primary);
163      border: 1px solid rgba(232, 123, 53, 0.3);
164      box-shadow: none;
165    }
166    .auth-button.secondary:hover {
167      background: #FFF4ED;
168    }
169    .form-hint {
170      margin-top: 6px;
171      font-size: 12px;
172      color: var(--text-muted);
173      line-height: 1.4;
174    }
175    /* Google sign in. Hidden until Clerk confirms the connection is live, so
176       the page never carries a button that can only fail. */
177    .social-auth { display: none; }
178    .social-button {
179      width: 100%;
180      display: flex;
181      align-items: center;
182      justify-content: center;
183      gap: 10px;
184      padding: 10px 14px;
185      border-radius: var(--radius-sm);
186      border: 1px solid var(--border);
187      background: #fff;
188      color: var(--text);
189      font-family: inherit;
190      font-weight: 600;
191      font-size: 14px;
192      cursor: pointer;
193      transition: background 0.15s ease, border-color 0.15s ease;
194    }
195    .social-button:hover {
196      background: var(--surface-muted);
197      border-color: #D8D8D8;
198    }
199    .social-button:disabled { opacity: 0.6; cursor: default; }
200    .social-button svg { width: 18px; height: 18px; flex-shrink: 0; }
201    .auth-divider {
202      display: flex;
203      align-items: center;
204      gap: 12px;
205      margin: 16px 0;
206      font-size: 12px;
207      color: var(--text-muted);
208    }
209    .auth-divider::before,
210    .auth-divider::after {
211      content: '';
212      flex: 1;
213      height: 1px;
214      background: var(--border);
215    }
216    .auth-footer {
217      margin-top: 18px;
218      text-align: center;
219      font-size: 13px;
220      color: var(--text-secondary);
221    }
222    .auth-footer a {
223      color: var(--primary);
224      font-weight: 500;
225      text-decoration: none;
226    }
227    .auth-footer a:hover {
228      text-decoration: underline;
229    }
230    .form-link-row {
231      display: flex;
232      justify-content: flex-end;
233      margin-top: -6px;
234      margin-bottom: 14px;
235    }
236    .text-button {
237      border: none;
238      background: none;
239      color: var(--primary);
240      font: inherit;
241      font-size: 13px;
242      font-weight: 500;
243      cursor: pointer;
244      padding: 0;
245    }
246    .text-button:hover {
247      text-decoration: underline;
248    }
249    .reset-panel {
250      display: none;
251      margin-top: 18px;
252      padding-top: 18px;
253      border-top: 1px solid var(--border);
254    }
255    .reset-copy {
256      font-size: 12px;
257      line-height: 1.5;
258      color: var(--text-secondary);
259      margin-bottom: 14px;
260    }
261  </style>
262</head>
263<body>
264  <div class="auth-card">
265    <div class="logo-row">
266      <img class="logo-icon" src="/ViewRank_logo.png" alt="" width="32" height="32">
267      <span class="logo-text">ViewRank AI</span>
268    </div>
269    <h1 class="auth-title">Welcome back</h1>
270    <p class="auth-subtitle">Sign in to view and analyze your Reels.</p>
271
272    <div id="error-message" class="alert alert-error">
273      <span id="error-text"></span>
274    </div>
275    <div id="success-message" class="alert alert-success">
276      <span id="success-text"></span>
277    </div>
278
279    <div class="social-auth" id="social-auth">
280      <button type="button" id="google-button" class="social-button">
281        <svg viewBox="0 0 48 48" aria-hidden="true" focusable="false">
282          <path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/>
283          <path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/>
284          <path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/>
285          <path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/>
286        </svg>
287        <span id="google-button-text">Continue with Google</span>
288      </button>
289      <div class="auth-divider">or</div>
290    </div>
291
292    <form id="login-form">
293      <div class="form-group">
294        <label for="email" class="form-label">Email</label>
295        <input
296          type="email"
297          id="email"
298          name="email"
299          required
300          class="form-input"
301          placeholder="[email protected]"
302        >
303      </div>
304
305      <div class="form-group">
306        <label for="password" class="form-label">Password</label>
307        <input
308          type="password"
309          id="password"
310          name="password"
311          required
312          class="form-input"
313          placeholder="••••••••"
314        >
315      </div>
316      <div class="form-link-row">
317        <button type="button" id="show-reset-btn" class="text-button">Forgot password?</button>
318      </div>
319
320      <button type="submit" id="login-button" class="auth-button">
321        Sign In
322      </button>
323    </form>
324
325    <form id="verify-form" style="display:none;">
326      <div class="form-group">
327        <label for="verification-code" class="form-label">Verification code</label>
328        <input
329          type="text"
330          id="verification-code"
331          name="verification-code"
332          required
333          inputmode="numeric"
334          autocomplete="one-time-code"
335          class="form-input"
336          placeholder="Enter the code from your email"
337        >
338        <p id="verify-hint" class="form-hint">Check your email and paste the code ViewRank sent you.</p>
339      </div>
340
341      <button type="submit" id="verify-button" class="auth-button">
342        Verify and Sign In
343      </button>
344      <button type="button" id="resend-code-button" class="auth-button secondary">
345        Send a new code
346      </button>
347      <button type="button" id="back-to-login-button" class="auth-button secondary">
348        Back to sign in
349      </button>
350    </form>
351
352    <div id="reset-panel" class="reset-panel">
353      <p class="reset-copy">Enter your email to receive a ViewRank reset code, then set a new password.</p>
354      <form id="reset-request-form">
355        <div class="form-group">
356          <label for="reset-email" class="form-label">Email</label>
357          <input type="email" id="reset-email" required class="form-input" placeholder="[email protected]">
358        </div>
359        <button type="submit" id="reset-request-button" class="auth-button">Send Reset Code</button>
360      </form>
361      <form id="reset-complete-form" style="display:none;">
362        <div class="form-group">
363          <label for="reset-code" class="form-label">Reset code</label>
364          <input type="text" id="reset-code" required class="form-input" placeholder="Enter emailed code">
365        </div>
366        <div class="form-group">
367          <label for="new-password" class="form-label">New password</label>
368          <input type="password" id="new-password" required minlength="8" class="form-input" placeholder="New password">
369        </div>
370        <button type="submit" id="reset-complete-button" class="auth-button">Reset Password</button>
371      </form>
372    </div>
373
374    <div class="auth-footer">
375      Don’t have an account?
376      <a href="https://viewrankai.com/#pricing">Choose a plan</a>
377    </div>
378  </div>
379
380  
380<script src="js/config.js?v=20260609-checkout"></script>
380
381  
381<script src="js/analytics.js?v=20260808-app"></script>
381
382  
382<script src="js/auth.js?v=20260918-subscribe"></script>
382
383  
383<script>
384    document.addEventListener('DOMContentLoaded', async () => {
385      let session = null;
386      try {
387        session = await window.METLY_AUTH.getSession();
388      } catch (error) {
389        console.warn('Auth not ready:', error);
390      }
391      if (session) {
392        await window.METLY_AUTH.redirectAfterAuth();
393        return;
394      }
395
396      const loginForm = document.getElementById('login-form');
397      const verifyForm = document.getElementById('verify-form');
398      const loginButton = document.getElementById('login-button');
399      const verifyButton = document.getElementById('verify-button');
400      const resendCodeButton = document.getElementById('resend-code-button');
401      const backToLoginButton = document.getElementById('back-to-login-button');
402      const verificationCodeInput = document.getElementById('verification-code');
403      const verifyHint = document.getElementById('verify-hint');
404      const errorMessage = document.getElementById('error-message');
405      const errorText = document.getElementById('error-text');
406      const successMessage = document.getElementById('success-message');
407      const successText = document.getElementById('success-text');
408      const socialAuth = document.getElementById('social-auth');
409      const googleButton = document.getElementById('google-button');
410      const googleButtonText = document.getElementById('google-button-text');
411      let googleAvailable = false;
412
413      if (new URLSearchParams(window.location.search).get('expired') === '1') {
414        errorText.textContent = 'Your session expired. Please log in again.';
415        errorMessage.style.display = 'block';
416      }
417      const resetPanel = document.getElementById('reset-panel');
418      const resetRequestForm = document.getElementById('reset-request-form');
419      const resetCompleteForm = document.getElementById('reset-complete-form');
420      const resetRequestButton = document.getElementById('reset-request-button');
421      const resetCompleteButton = document.getElementById('reset-complete-button');
422
423      function showError(message) {
424        successMessage.style.display = 'none';
425        errorText.textContent = message;
426        errorMessage.style.display = 'block';
427      }
428
429      function showSuccess(message) {
430        errorMessage.style.display = 'none';
431        successText.textContent = message;
432        successMessage.style.display = 'block';
433      }
434
435      function showVerificationStep(result) {
436        loginForm.style.display = 'none';
437        // Google is not an option once an email sign in is part way through.
438        socialAuth.style.display = 'none';
439        verifyForm.style.display = 'block';
440        errorMessage.style.display = 'none';
441
442        const email = result.email || document.getElementById('email').value.trim();
443        const isDeviceCheck = result.kind === 'device';
444        const channel = result.channel || 'email';
445
446        if (channel === 'authenticator') {
447          successText.textContent = 'Enter the 6-digit code from your authenticator app.';
448          verifyHint.textContent = 'Open your authenticator app and enter the current code.';
449          resendCodeButton.style.display = 'none';
450        } else if (channel === 'phone') {
451          successText.textContent = isDeviceCheck
452            ? `We sent a verification code to your phone to confirm this device.`
453            : `Enter the verification code sent to your phone.`;
454          verifyHint.textContent = 'Check your phone and paste the code ViewRank sent you.';
455          resendCodeButton.style.display = 'block';
456        } else {
457          successText.textContent = isDeviceCheck
458            ? `We sent a verification code to ${email} to confirm this device.`
459            : `We sent a verification code to ${email}.`;
460          verifyHint.textContent = 'Check your email and paste the code ViewRank sent you.';
461          resendCodeButton.style.display = 'block';
462        }
463
464        successMessage.style.display = 'block';
465        verificationCodeInput.value = '';
466        verificationCodeInput.focus();
467      }
468
469      function showLoginStep() {
470        verifyForm.style.display = 'none';
471        loginForm.style.display = 'block';
472        if (googleAvailable) socialAuth.style.display = 'block';
473        successMessage.style.display = 'none';
474        errorMessage.style.display = 'none';
475        verificationCodeInput.value = '';
476        loginGuard.reset();
477        verifyGuard.reset();
478      }
479
480      // Holding the Enter key repeats a form's submit event every few
481      // milliseconds, and disabling the button does not stop it. Every form
482      // on this page runs one attempt at a time. The auth calls have no
483      // timeout of their own, so a call that never answers releases the form
484      // after STALL_MS and invites a retry instead of leaving a dead button.
485      const STALL_MS = 20000;
486
487      const track = (name, props) => {
488        if (window.METLY_ANALYTICS) window.METLY_ANALYTICS.track(name, props);
489      };
490
491      const failureReason = (error) =>
492        String((error && error.message) || 'unknown').replace(/\s+/g, ' ').trim().slice(0, 100);
493
494      // Ask Clerk whether Google is connected before offering it.
495      window.METLY_AUTH.googleEnabled().then((enabled) => {
496        googleAvailable = enabled;
497        if (enabled && verifyForm.style.display !== 'block') {
498          socialAuth.style.display = 'block';
499        }
500      });
501
502      const releaseGoogleButton = () => {
503        googleButton.disabled = false;
504        googleButtonText.textContent = 'Continue with Google';
505      };
506
507      googleButton.addEventListener('click', async () => {
508        if (googleButton.disabled) return;
509        errorMessage.style.display = 'none';
510        googleButton.disabled = true;
511        googleButtonText.textContent = 'Taking you to Google...';
512        track('google_auth_started', { mode: 'login' });
513
514        // The browser leaves this page when this works, so the timer below
515        // only ever fires when Clerk has not answered. Without it the button
516        // stays disabled for good and the person has nothing left to press.
517        const startedAt = Date.now();
518        let stalled = false;
519        const stallTimer = setTimeout(() => {
520          stalled = true;
521          releaseGoogleButton();
522          showError('Google is taking longer than usual. Please try again, or use your email and password below.');
523          track('google_auth_stalled', { mode: 'login', waited_ms: STALL_MS });
524        }, STALL_MS);
525
526        try {
527          await window.METLY_AUTH.continueWithGoogle('login');
528          clearTimeout(stallTimer);
529        } catch (error) {
530          clearTimeout(stallTimer);
531          track('google_auth_failed', {
532            mode: 'login',
533            reason: failureReason(error),
534            duration_ms: Date.now() - startedAt
535          });
536          if (!stalled) {
537            showError(error.message || 'Could not continue with Google. Please try again.');
538            releaseGoogleButton();
539          }
540        }
541      });
542
543      // Wires one form (or button) to run one attempt at a time.
544      //   step:     label carried on the login_* events
545      //   run:      does the work; return true to keep the button locked
546      //             because a redirect is about to happen
547      //   fallback: error shown when the call fails without a message
548      //   cooldownMs: after a success, keep the button locked this long
549      //             (stops a mashed "send a new code" sending several)
550      function guard({ element, event = 'submit', button, busyText, idleText, step, run, fallback, countFirst = false, cooldownMs = 0 }) {
551        let inFlight = false;
552        let attempts = 0;
553
554        const unlock = () => {
555          inFlight = false;
556          button.disabled = false;
557          button.textContent = idleText;
558        };
559
560        element.addEventListener(event, async (e) => {
561          if (e && e.preventDefault) e.preventDefault();
562          if (inFlight) return;
563          inFlight = true;
564          button.disabled = true;
565          button.textContent = busyText;
566          errorMessage.style.display = 'none';
567
568          attempts += 1;
569          if (countFirst && attempts === 1) track('login_submitted', { step });
570
571          const startedAt = Date.now();
572          let stalled = false;
573          const stallTimer = setTimeout(() => {
574            stalled = true;
575            unlock();
576            showError('This is taking longer than usual. Please try again.');
577            track('login_stalled', { step, attempt: attempts, waited_ms: STALL_MS });
578          }, STALL_MS);
579
580          let keepLocked = false;
581          let failed = false;
582          try {
583            keepLocked = (await run()) === true;
584          } catch (error) {
585            failed = true;
586            track('login_failed', {
587              step,
588              attempt: attempts,
589              reason: failureReason(error),
590              duration_ms: Date.now() - startedAt
591            });
592            showError(error.message || fallback);
593          } finally {
594            clearTimeout(stallTimer);
595            // After a stall the form is already open again and a fresh
596            // attempt may be running: leave its state alone.
597            if (stalled || keepLocked) return;
598            if (cooldownMs && !failed) {
599              button.textContent = idleText;
600              setTimeout(unlock, cooldownMs);
601            } else {
602              unlock();
603            }
604          }
605        });
606
607        return { reset: unlock };
608      }
609
610      const loginGuard = guard({
611        element: loginForm,
612        button: loginButton,
613        busyText: 'Signing in...',
614        idleText: 'Sign In',
615        step: 'password',
616        countFirst: true,
617        fallback: 'Failed to sign in. Please check your credentials.',
618        run: async () => {
619          successMessage.style.display = 'none';
620          const email = document.getElementById('email').value;
621          const password = document.getElementById('password').value;
622          const result = await window.METLY_AUTH.login(email, password);
623          if (result?.status === 'needs_verification') {
624            showVerificationStep(result);
625          }
626          // Stay locked: either the redirect follows, or the code step is
627          // showing and the back button reopens this form.
628          return true;
629        }
630      });
631
632      const verifyGuard = guard({
633        element: verifyForm,
634        button: verifyButton,
635        busyText: 'Verifying...',
636        idleText: 'Verify and Sign In',
637        step: 'code',
638        fallback: 'Failed to verify code.',
639        run: async () => {
640          await window.METLY_AUTH.verifyLoginCode(verificationCodeInput.value.trim());
641          // Stay locked: the redirect to the workspace follows.
642          return true;
643        }
644      });
645
646      guard({
647        element: resendCodeButton,
648        event: 'click',
649        button: resendCodeButton,
650        busyText: 'Sending...',
651        idleText: 'Send a new code',
652        step: 'resend',
653        cooldownMs: 10000,
654        fallback: 'Failed to resend verification code.',
655        run: async () => {
656          await window.METLY_AUTH.resendLoginVerificationCode();
657          showSuccess('A new verification code has been sent.');
658        }
659      });
660
661      backToLoginButton.addEventListener('click', () => {
662        showLoginStep();
663      });
664
665      document.getElementById('show-reset-btn')?.addEventListener('click', () => {
666        resetPanel.style.display = resetPanel.style.display === 'block' ? 'none' : 'block';
667        const email = document.getElementById('email').value.trim();
668        if (email) document.getElementById('reset-email').value = email;
669      });
670
671      guard({
672        element: resetRequestForm,
673        button: resetRequestButton,
674        busyText: 'Sending...',
675        idleText: 'Send Reset Code',
676        step: 'reset_request',
677        fallback: 'Failed to send reset code.',
678        run: async () => {
679          successMessage.style.display = 'none';
680          await window.METLY_AUTH.startPasswordReset(document.getElementById('reset-email').value.trim());
681          resetRequestForm.style.display = 'none';
682          resetCompleteForm.style.display = 'block';
683          showSuccess('Reset code sent. Check your email, then enter the code and a new password.');
684          // Stay locked: this form is hidden from here on.
685          return true;
686        }
687      });
688
689      guard({
690        element: resetCompleteForm,
691        button: resetCompleteButton,
692        busyText: 'Resetting...',
693        idleText: 'Reset Password',
694        step: 'reset_complete',
695        fallback: 'Failed to reset password.',
696        run: async () => {
697          successMessage.style.display = 'none';
698          await window.METLY_AUTH.completePasswordReset(
699            document.getElementById('reset-code').value.trim(),
700            document.getElementById('new-password').value
701          );
702          // Stay locked: the redirect to the workspace follows.
703          return true;
704        }
705      });
706    });
707  </script>
707
708</body>
709</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.