@getmicdrop/svelte-components 2.8.0 → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Modal/Modal.svelte +7 -1
- package/dist/components/SuperLogin/SuperLogin.svelte +1282 -0
- package/dist/components/SuperLogin/SuperLogin.svelte.d.ts +44 -0
- package/dist/components/SuperLogin/SuperLogin.svelte.d.ts.map +1 -0
- package/dist/components/SuperLogin/index.d.ts +2 -0
- package/dist/components/SuperLogin/index.d.ts.map +1 -0
- package/dist/components/SuperLogin/index.js +1 -0
- package/dist/constants/validation.d.ts +55 -0
- package/dist/constants/validation.d.ts.map +1 -0
- package/dist/constants/validation.js +91 -0
- package/dist/constants/validation.spec.d.ts +2 -0
- package/dist/constants/validation.spec.d.ts.map +1 -0
- package/dist/constants/validation.spec.js +64 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1282 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { onMount } from "svelte";
|
|
3
|
+
import { fade, slide } from "svelte/transition";
|
|
4
|
+
import { cubicOut } from "svelte/easing";
|
|
5
|
+
import WarningIcon from "../Icons/WarningIcon.svelte";
|
|
6
|
+
import DarkModeToggle from "../DarkModeToggle.svelte";
|
|
7
|
+
import PasswordStrengthIndicator from "../PasswordStrengthIndicator/PasswordStrengthIndicator.svelte";
|
|
8
|
+
import Input from "../Input/Input.svelte";
|
|
9
|
+
import Button from "../Button/Button.svelte";
|
|
10
|
+
import Checkbox from "../Checkbox/Checkbox.svelte";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} Account
|
|
14
|
+
* @property {string} type - Account type (e.g., 'owner', 'performer')
|
|
15
|
+
* @property {string} [organizationName] - Organization name for owner accounts
|
|
16
|
+
* @property {string} token - Auth token
|
|
17
|
+
* @property {string} firstName - User's first name
|
|
18
|
+
* @property {string} lastName - User's last name
|
|
19
|
+
* @property {string} email - User's email
|
|
20
|
+
* @property {string} dashboardUrl - URL to redirect after login
|
|
21
|
+
* @property {Object} [performerProfile] - Performer profile data
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} LoginResult
|
|
26
|
+
* @property {string} token - Auth token
|
|
27
|
+
* @property {string} [refreshToken] - Refresh token
|
|
28
|
+
* @property {string} [firstName] - User's first name
|
|
29
|
+
* @property {Account[]} [accounts] - Multiple accounts if user has more than one
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
let {
|
|
33
|
+
/** @type {string} Base URL for API calls (e.g., 'https://api.micdrop.com' or '' for same-origin) */
|
|
34
|
+
apiBaseUrl = "",
|
|
35
|
+
/** @type {string} Logo image source */
|
|
36
|
+
logoSrc = "",
|
|
37
|
+
/** @type {string} Logo alt text */
|
|
38
|
+
logoAlt = "Logo",
|
|
39
|
+
/** @type {'login' | 'reset' | 'login-link' | 'setup'} Initial view to show */
|
|
40
|
+
initialView = "login",
|
|
41
|
+
/** @type {boolean} Whether this is a first-time user */
|
|
42
|
+
isFirstTime = false,
|
|
43
|
+
/** @type {string} Pre-filled user email */
|
|
44
|
+
userEmail = "",
|
|
45
|
+
/** @type {string} User's first name (for welcome message) */
|
|
46
|
+
firstName = "",
|
|
47
|
+
/** @type {string} Portal type for messaging (e.g., 'performer portal', 'organization dashboard') */
|
|
48
|
+
portalType = "dashboard",
|
|
49
|
+
/** @type {string} Default redirect path after successful login */
|
|
50
|
+
defaultRedirectPath = "/",
|
|
51
|
+
/** @type {string} Default redirect path after first-time setup */
|
|
52
|
+
setupRedirectPath = "/profile",
|
|
53
|
+
/** @type {string} Terms of service URL */
|
|
54
|
+
tosUrl = "https://get-micdrop.com/tos",
|
|
55
|
+
/** @type {(result: LoginResult, rememberMe: boolean) => void} Callback when login succeeds */
|
|
56
|
+
onLoginSuccess = () => {},
|
|
57
|
+
/** @type {(account: Account, rememberMe: boolean) => void} Callback when account is selected */
|
|
58
|
+
onAccountSelect = () => {},
|
|
59
|
+
/** @type {(path: string) => void} Callback for navigation (e.g., goto) */
|
|
60
|
+
onNavigate = () => {},
|
|
61
|
+
/** @type {(url: string) => void} Callback for external navigation */
|
|
62
|
+
onExternalNavigate = (url) => { window.location.href = url; },
|
|
63
|
+
/** @type {boolean} Show dark mode toggle */
|
|
64
|
+
showDarkModeToggle = true,
|
|
65
|
+
/** @type {string} URL search params for setup detection */
|
|
66
|
+
searchParams = "",
|
|
67
|
+
} = $props();
|
|
68
|
+
|
|
69
|
+
// Parse search params
|
|
70
|
+
let parsedParams = $derived.by(() => {
|
|
71
|
+
if (typeof window === 'undefined') return { setup: null, email: null };
|
|
72
|
+
const params = new URLSearchParams(searchParams || window.location.search);
|
|
73
|
+
return {
|
|
74
|
+
setup: params.get('setup'),
|
|
75
|
+
email: params.get('email')?.replace(/ /g, '+') || null
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
let view = $state(initialView);
|
|
80
|
+
let accounts = $state([]);
|
|
81
|
+
let shake = $state(false);
|
|
82
|
+
let showErrors = $state(false);
|
|
83
|
+
let contentHeight = $state(undefined);
|
|
84
|
+
let isViewTransitioning = $state(false);
|
|
85
|
+
let successEmail = $state("");
|
|
86
|
+
let successType = $state("reset");
|
|
87
|
+
let isResendSuccess = $state(false);
|
|
88
|
+
let loginLinkMessage = $state("");
|
|
89
|
+
let cardVisible = $state(false);
|
|
90
|
+
|
|
91
|
+
// Remembered user state for "Welcome back" feature
|
|
92
|
+
let rememberedUser = $state(null);
|
|
93
|
+
|
|
94
|
+
// First name from eligibility check (for setup welcome message)
|
|
95
|
+
let setupFirstName = $state("");
|
|
96
|
+
|
|
97
|
+
// Trigger transition when view changes
|
|
98
|
+
let previousView = $state(initialView);
|
|
99
|
+
$effect(() => {
|
|
100
|
+
if (view !== previousView) {
|
|
101
|
+
previousView = view;
|
|
102
|
+
isViewTransitioning = true;
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
isViewTransitioning = false;
|
|
105
|
+
}, 300);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
let authError = $state("");
|
|
110
|
+
let email = $state(userEmail);
|
|
111
|
+
let password = $state("");
|
|
112
|
+
let errors = $state({});
|
|
113
|
+
let isLoading = $state(false);
|
|
114
|
+
let rememberMe = $state(true);
|
|
115
|
+
// Initialize synchronously to prevent flash
|
|
116
|
+
let isCheckingEligibility = $state(parsedParams.setup === "true");
|
|
117
|
+
|
|
118
|
+
let isFormValid = $derived(email.length > 0 && password.length > 0);
|
|
119
|
+
|
|
120
|
+
// Password strength state
|
|
121
|
+
let strengthText = $state("");
|
|
122
|
+
let strengthTextColor = $state("");
|
|
123
|
+
let score = $state(-1);
|
|
124
|
+
|
|
125
|
+
let isSetupValid = $derived(score >= 2);
|
|
126
|
+
|
|
127
|
+
// Track previous values to prevent unnecessary updates
|
|
128
|
+
let prevPasswordLen = $state(0);
|
|
129
|
+
let prevScore = $state(-1);
|
|
130
|
+
|
|
131
|
+
// Show/hide error based on score (debounced from component)
|
|
132
|
+
$effect(() => {
|
|
133
|
+
const currentPasswordLen = password.length;
|
|
134
|
+
const currentScore = score;
|
|
135
|
+
|
|
136
|
+
// Only process if values actually changed
|
|
137
|
+
if (currentPasswordLen === prevPasswordLen && currentScore === prevScore) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
prevPasswordLen = currentPasswordLen;
|
|
142
|
+
prevScore = currentScore;
|
|
143
|
+
|
|
144
|
+
if (currentPasswordLen === 0) {
|
|
145
|
+
// Clear error immediately if password is empty
|
|
146
|
+
if (errors.password) {
|
|
147
|
+
const { password: _, ...rest } = errors;
|
|
148
|
+
errors = rest;
|
|
149
|
+
}
|
|
150
|
+
} else if (currentScore !== -1 && currentScore < 2) {
|
|
151
|
+
errors = {
|
|
152
|
+
...errors,
|
|
153
|
+
password:
|
|
154
|
+
"Password must be at least 8 characters and include at least 2 of: uppercase, lowercase, numbers, or symbols.",
|
|
155
|
+
};
|
|
156
|
+
showErrors = true;
|
|
157
|
+
} else if (currentScore >= 2) {
|
|
158
|
+
// Clear password error if it's strong enough
|
|
159
|
+
if (errors.password) {
|
|
160
|
+
const { password: _, ...rest } = errors;
|
|
161
|
+
errors = rest;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
onMount(async () => {
|
|
167
|
+
applyTheme();
|
|
168
|
+
|
|
169
|
+
// Check for remembered user
|
|
170
|
+
try {
|
|
171
|
+
const stored = localStorage.getItem('rememberedUser');
|
|
172
|
+
if (stored) {
|
|
173
|
+
rememberedUser = JSON.parse(stored);
|
|
174
|
+
if (rememberedUser?.email) {
|
|
175
|
+
email = rememberedUser.email;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch (e) {
|
|
179
|
+
// Ignore parse errors
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Trigger fade-in animation after a brief delay
|
|
183
|
+
setTimeout(() => {
|
|
184
|
+
cardVisible = true;
|
|
185
|
+
}, 50);
|
|
186
|
+
|
|
187
|
+
const setupParam = parsedParams.setup;
|
|
188
|
+
let emailParam = parsedParams.email;
|
|
189
|
+
|
|
190
|
+
if (setupParam === "true" && emailParam) {
|
|
191
|
+
// isCheckingEligibility is already true from initialization
|
|
192
|
+
email = emailParam;
|
|
193
|
+
await checkEligibility(emailParam);
|
|
194
|
+
isCheckingEligibility = false;
|
|
195
|
+
} else if (isCheckingEligibility) {
|
|
196
|
+
// If setup was true but no email, turn off checking
|
|
197
|
+
isCheckingEligibility = false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Handle browser back/forward buttons
|
|
201
|
+
const handlePopState = () => {
|
|
202
|
+
const path = window.location.pathname;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
window.addEventListener("popstate", handlePopState);
|
|
206
|
+
return () => {
|
|
207
|
+
window.removeEventListener("popstate", handlePopState);
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
function applyTheme() {
|
|
212
|
+
const savedTheme = localStorage.getItem("theme");
|
|
213
|
+
const container = document.querySelector(".micdrop");
|
|
214
|
+
|
|
215
|
+
if (container) {
|
|
216
|
+
if (
|
|
217
|
+
savedTheme === "dark" ||
|
|
218
|
+
(!savedTheme &&
|
|
219
|
+
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
|
220
|
+
) {
|
|
221
|
+
container.classList.add("dark");
|
|
222
|
+
} else {
|
|
223
|
+
container.classList.remove("dark");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
$effect(() => {
|
|
229
|
+
if (typeof window !== "undefined") {
|
|
230
|
+
applyTheme();
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
async function checkEligibility(emailVal) {
|
|
235
|
+
isLoading = true;
|
|
236
|
+
try {
|
|
237
|
+
const response = await fetch(
|
|
238
|
+
`${apiBaseUrl}/api/public/checkIfFirstUseEligible/${encodeURIComponent(emailVal)}`,
|
|
239
|
+
);
|
|
240
|
+
if (response.ok) {
|
|
241
|
+
const data = await response.json();
|
|
242
|
+
if (data.eligible) {
|
|
243
|
+
if (data.firstName) {
|
|
244
|
+
setupFirstName = data.firstName;
|
|
245
|
+
}
|
|
246
|
+
view = "setup";
|
|
247
|
+
} else {
|
|
248
|
+
errors = {
|
|
249
|
+
email: "This link is no longer valid or has already been used.",
|
|
250
|
+
};
|
|
251
|
+
showErrors = true;
|
|
252
|
+
triggerShake();
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
errors = { email: "Invalid or expired link." };
|
|
256
|
+
showErrors = true;
|
|
257
|
+
triggerShake();
|
|
258
|
+
}
|
|
259
|
+
} catch (e) {
|
|
260
|
+
console.error(e);
|
|
261
|
+
errors = { email: "Unable to validate link. Please try again later." };
|
|
262
|
+
showErrors = true;
|
|
263
|
+
triggerShake();
|
|
264
|
+
} finally {
|
|
265
|
+
isLoading = false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function handleSubmit(event) {
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
isLoading = true;
|
|
272
|
+
authError = "";
|
|
273
|
+
const emailValue = email;
|
|
274
|
+
const passwordValue = password;
|
|
275
|
+
|
|
276
|
+
// Enforce minimum spinner duration (1s)
|
|
277
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
278
|
+
|
|
279
|
+
// Validate email format
|
|
280
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
281
|
+
if (!emailRegex.test(emailValue)) {
|
|
282
|
+
errors = { email: `Invalid email address: ${emailValue}` };
|
|
283
|
+
triggerShake();
|
|
284
|
+
isLoading = false;
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
const response = await fetch(`${apiBaseUrl}/api/public/login`, {
|
|
289
|
+
method: "POST",
|
|
290
|
+
headers: {
|
|
291
|
+
"Content-Type": "application/json",
|
|
292
|
+
},
|
|
293
|
+
body: JSON.stringify({
|
|
294
|
+
email: emailValue.toLowerCase(),
|
|
295
|
+
password: passwordValue,
|
|
296
|
+
}),
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
if (response.ok) {
|
|
300
|
+
const data = await response.json();
|
|
301
|
+
isLoading = false;
|
|
302
|
+
// Clear errors on success
|
|
303
|
+
errors = {};
|
|
304
|
+
showErrors = false;
|
|
305
|
+
|
|
306
|
+
if (data.accounts && data.accounts.length > 1) {
|
|
307
|
+
accounts = data.accounts;
|
|
308
|
+
view = "selection";
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Save remembered user for "Welcome back" feature
|
|
313
|
+
if (rememberMe && data.firstName) {
|
|
314
|
+
saveRememberedUser(emailValue, data.firstName);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Call the success callback
|
|
318
|
+
onLoginSuccess({
|
|
319
|
+
token: data.token,
|
|
320
|
+
refreshToken: data.refreshToken || data.refreshtoken,
|
|
321
|
+
firstName: data.firstName,
|
|
322
|
+
email: emailValue,
|
|
323
|
+
}, rememberMe);
|
|
324
|
+
|
|
325
|
+
onNavigate(defaultRedirectPath);
|
|
326
|
+
} else {
|
|
327
|
+
const errorData = await response.json();
|
|
328
|
+
isLoading = false;
|
|
329
|
+
|
|
330
|
+
// Check if user needs to set up password
|
|
331
|
+
if (errorData.needsSetup && errorData.email) {
|
|
332
|
+
view = "setup";
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (response.status === 401) {
|
|
337
|
+
errors = { password: "Incorrect email or password" };
|
|
338
|
+
} else {
|
|
339
|
+
errors = {
|
|
340
|
+
password: errorData.message || "Login failed. Please try again.",
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
triggerShake();
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.error("Login Error:", error);
|
|
347
|
+
if (error.message === "Network Error" || error.name === "TypeError") {
|
|
348
|
+
errors = {
|
|
349
|
+
password: "Unable to connect. Please check your internet connection.",
|
|
350
|
+
};
|
|
351
|
+
} else {
|
|
352
|
+
errors = { password: "Incorrect email or password" };
|
|
353
|
+
}
|
|
354
|
+
triggerShake();
|
|
355
|
+
isLoading = false;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function handleSetupSubmit() {
|
|
360
|
+
isLoading = true;
|
|
361
|
+
const emailValue = email;
|
|
362
|
+
const passwordValue = password;
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const response = await fetch(`${apiBaseUrl}/api/public/setFirstUsePassword`, {
|
|
366
|
+
method: "POST",
|
|
367
|
+
headers: {
|
|
368
|
+
"Content-Type": "application/json",
|
|
369
|
+
},
|
|
370
|
+
credentials: "include",
|
|
371
|
+
body: JSON.stringify({
|
|
372
|
+
email: emailValue,
|
|
373
|
+
password: passwordValue,
|
|
374
|
+
}),
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
if (response.ok) {
|
|
378
|
+
const data = await response.json();
|
|
379
|
+
|
|
380
|
+
// Call the success callback
|
|
381
|
+
onLoginSuccess({
|
|
382
|
+
token: data.token,
|
|
383
|
+
refreshToken: data.refreshToken || data.refreshtoken,
|
|
384
|
+
email: emailValue,
|
|
385
|
+
}, rememberMe);
|
|
386
|
+
|
|
387
|
+
onNavigate(setupRedirectPath);
|
|
388
|
+
} else {
|
|
389
|
+
const errorData = await response.json();
|
|
390
|
+
errors = {
|
|
391
|
+
password:
|
|
392
|
+
errorData.error ||
|
|
393
|
+
"Password must be at least 8 characters and include at least 2 of: uppercase, lowercase, numbers, or symbols.",
|
|
394
|
+
};
|
|
395
|
+
showErrors = true;
|
|
396
|
+
triggerShake();
|
|
397
|
+
}
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.error("Error setting up account:", error);
|
|
400
|
+
errors = { password: "Network error. Please try again." };
|
|
401
|
+
showErrors = true;
|
|
402
|
+
triggerShake();
|
|
403
|
+
} finally {
|
|
404
|
+
isLoading = false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function handleResetSubmit() {
|
|
409
|
+
isLoading = true;
|
|
410
|
+
// Enforce minimum spinner duration (1s)
|
|
411
|
+
const minDelay = new Promise((resolve) => setTimeout(resolve, 1000));
|
|
412
|
+
const emailValue = email;
|
|
413
|
+
|
|
414
|
+
// Validate email format
|
|
415
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
416
|
+
if (!emailRegex.test(emailValue)) {
|
|
417
|
+
errors = { email: `Invalid email address: ${emailValue}` };
|
|
418
|
+
triggerShake();
|
|
419
|
+
isLoading = false;
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const apiCall = fetch(`${apiBaseUrl}/api/public/resetPassword`, {
|
|
424
|
+
method: "POST",
|
|
425
|
+
headers: { "Content-Type": "application/json" },
|
|
426
|
+
body: JSON.stringify({ email: emailValue.toLowerCase() }),
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
const [_, response] = await Promise.all([minDelay, apiCall]);
|
|
431
|
+
isLoading = false;
|
|
432
|
+
|
|
433
|
+
// Always show success to prevent email enumeration
|
|
434
|
+
successEmail = emailValue;
|
|
435
|
+
successType = "reset";
|
|
436
|
+
isResendSuccess = false;
|
|
437
|
+
view = "success";
|
|
438
|
+
return true;
|
|
439
|
+
} catch (error) {
|
|
440
|
+
console.error("Reset Password Error:", error);
|
|
441
|
+
// Show success even on error to prevent enumeration
|
|
442
|
+
successEmail = emailValue;
|
|
443
|
+
successType = "reset";
|
|
444
|
+
isResendSuccess = false;
|
|
445
|
+
view = "success";
|
|
446
|
+
isLoading = false;
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function handleLoginLinkSubmit() {
|
|
452
|
+
loginLinkMessage = "";
|
|
453
|
+
isLoading = true;
|
|
454
|
+
// Enforce minimum spinner duration (1s)
|
|
455
|
+
const minDelay = new Promise((resolve) => setTimeout(resolve, 1000));
|
|
456
|
+
const emailValue = email;
|
|
457
|
+
|
|
458
|
+
// Validate email format
|
|
459
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
460
|
+
if (!emailRegex.test(emailValue)) {
|
|
461
|
+
errors = { email: `Invalid email address: ${emailValue}` };
|
|
462
|
+
triggerShake();
|
|
463
|
+
isLoading = false;
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const apiCall = fetch(`${apiBaseUrl}/api/public/passwordlessLogin`, {
|
|
468
|
+
method: "POST",
|
|
469
|
+
headers: { "Content-Type": "application/json" },
|
|
470
|
+
body: JSON.stringify({
|
|
471
|
+
email: emailValue.toLowerCase(),
|
|
472
|
+
destination: "dashboard",
|
|
473
|
+
}),
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
const [_, response] = await Promise.all([minDelay, apiCall]);
|
|
478
|
+
|
|
479
|
+
isLoading = false;
|
|
480
|
+
|
|
481
|
+
// Always show success to prevent email enumeration
|
|
482
|
+
successEmail = emailValue;
|
|
483
|
+
successType = "login-link";
|
|
484
|
+
isResendSuccess = false;
|
|
485
|
+
view = "success";
|
|
486
|
+
return true;
|
|
487
|
+
} catch (error) {
|
|
488
|
+
console.error("Login Link Error:", error);
|
|
489
|
+
// Show success even on error to prevent enumeration
|
|
490
|
+
successEmail = emailValue;
|
|
491
|
+
successType = "login-link";
|
|
492
|
+
isResendSuccess = false;
|
|
493
|
+
view = "success";
|
|
494
|
+
isLoading = false;
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function handleResend() {
|
|
500
|
+
let success = false;
|
|
501
|
+
if (successType === "reset") {
|
|
502
|
+
success = await handleResetSubmit();
|
|
503
|
+
} else {
|
|
504
|
+
success = await handleLoginLinkSubmit();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (success) {
|
|
508
|
+
isResendSuccess = true;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function handleTryDifferentEmail() {
|
|
513
|
+
view = successType;
|
|
514
|
+
errors = {};
|
|
515
|
+
showErrors = false;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function handleReturnToSignIn() {
|
|
519
|
+
email = "";
|
|
520
|
+
password = "";
|
|
521
|
+
errors = {};
|
|
522
|
+
showErrors = false;
|
|
523
|
+
view = "login";
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function handleNotMe() {
|
|
527
|
+
localStorage.removeItem('rememberedUser');
|
|
528
|
+
rememberedUser = null;
|
|
529
|
+
email = "";
|
|
530
|
+
password = "";
|
|
531
|
+
errors = {};
|
|
532
|
+
showErrors = false;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function saveRememberedUser(emailVal, firstNameVal) {
|
|
536
|
+
try {
|
|
537
|
+
localStorage.setItem(
|
|
538
|
+
'rememberedUser',
|
|
539
|
+
JSON.stringify({ email: emailVal, firstName: firstNameVal })
|
|
540
|
+
);
|
|
541
|
+
} catch (e) {
|
|
542
|
+
// Ignore storage errors
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function handleAccountSelectInternal(account) {
|
|
547
|
+
onAccountSelect(account, rememberMe);
|
|
548
|
+
|
|
549
|
+
// If it's an external URL, redirect there
|
|
550
|
+
if (account.dashboardUrl?.startsWith("http")) {
|
|
551
|
+
onExternalNavigate(account.dashboardUrl);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Otherwise use internal navigation
|
|
556
|
+
onNavigate(account.dashboardUrl || defaultRedirectPath);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function getAccountName(account) {
|
|
560
|
+
if (account.organizationName) {
|
|
561
|
+
return account.organizationName;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const profile = account.performerProfile || {};
|
|
565
|
+
const stageName = profile.stageName || account.stageName || "";
|
|
566
|
+
const normalizedStage = stageName?.trim() || "";
|
|
567
|
+
const useStageName = Boolean(
|
|
568
|
+
(profile.useStageName ?? account.useStageName) && normalizedStage,
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
if (useStageName) {
|
|
572
|
+
return normalizedStage;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const first = profile.firstName || account.firstName || account.name || "";
|
|
576
|
+
const lastName = profile.lastName || account.lastName || "";
|
|
577
|
+
return `${first} ${lastName}`.trim() || "Performer";
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function triggerShake() {
|
|
581
|
+
shake = true;
|
|
582
|
+
if (!showErrors) {
|
|
583
|
+
// New error: Wait for shake to finish before showing validation
|
|
584
|
+
setTimeout(() => {
|
|
585
|
+
shake = false;
|
|
586
|
+
showErrors = true;
|
|
587
|
+
}, 625);
|
|
588
|
+
} else {
|
|
589
|
+
// Switching error: Update immediately (already showing), just shake
|
|
590
|
+
setTimeout(() => {
|
|
591
|
+
shake = false;
|
|
592
|
+
}, 625);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
</script>
|
|
596
|
+
|
|
597
|
+
<div
|
|
598
|
+
class="superlogin-container min-h-screen flex items-start justify-center animated-gradient py-12 px-4 sm:px-6 lg:px-8 relative pt-32"
|
|
599
|
+
>
|
|
600
|
+
<!-- Logo positioned top-left -->
|
|
601
|
+
{#if logoSrc}
|
|
602
|
+
<div class="absolute top-8 left-8">
|
|
603
|
+
<img src={logoSrc} alt={logoAlt} class="h-8 w-auto" />
|
|
604
|
+
</div>
|
|
605
|
+
{/if}
|
|
606
|
+
|
|
607
|
+
<div class="w-full flex justify-center px-4">
|
|
608
|
+
<div
|
|
609
|
+
class="login-card bg-white dark:bg-gray-800 rounded-xl shadow-xl overflow-hidden {shake
|
|
610
|
+
? 'shake'
|
|
611
|
+
: ''} {isViewTransitioning ? 'transition-height' : ''} {cardVisible ? 'card-visible' : 'card-hidden'}"
|
|
612
|
+
style="height: {contentHeight ? contentHeight + 'px' : 'auto'}"
|
|
613
|
+
>
|
|
614
|
+
<div class="login-card-padding" bind:clientHeight={contentHeight}>
|
|
615
|
+
{#if isCheckingEligibility}
|
|
616
|
+
<div class="flex justify-center py-12">
|
|
617
|
+
<div
|
|
618
|
+
class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"
|
|
619
|
+
></div>
|
|
620
|
+
</div>
|
|
621
|
+
{:else if view === "login"}
|
|
622
|
+
<div in:fade={{ duration: 200 }} class="space-y-6">
|
|
623
|
+
<div class="space-y-2">
|
|
624
|
+
{#if isFirstTime}
|
|
625
|
+
<div>
|
|
626
|
+
<h2
|
|
627
|
+
class="text-2xl font-normal text-gray-900 dark:text-white"
|
|
628
|
+
>
|
|
629
|
+
Welcome{firstName ? `, ${firstName}` : ""}!
|
|
630
|
+
</h2>
|
|
631
|
+
<p class="text-sm text-gray-600 dark:text-gray-300 mt-2">
|
|
632
|
+
Sign in to access your {portalType}.
|
|
633
|
+
</p>
|
|
634
|
+
</div>
|
|
635
|
+
{:else if rememberedUser?.firstName}
|
|
636
|
+
<h2 class="text-2xl font-normal text-gray-900 dark:text-white">
|
|
637
|
+
Welcome back, {rememberedUser.firstName}
|
|
638
|
+
</h2>
|
|
639
|
+
<button
|
|
640
|
+
type="button"
|
|
641
|
+
class="text-sm text-gray-500 hover:text-blue-600 transition-colors"
|
|
642
|
+
onclick={handleNotMe}
|
|
643
|
+
>
|
|
644
|
+
Not {rememberedUser.firstName}?
|
|
645
|
+
</button>
|
|
646
|
+
{:else}
|
|
647
|
+
<h2 class="text-2xl font-normal text-gray-900 dark:text-white">
|
|
648
|
+
Log in
|
|
649
|
+
</h2>
|
|
650
|
+
{/if}
|
|
651
|
+
</div>
|
|
652
|
+
|
|
653
|
+
<form class="space-y-6" onsubmit={handleSubmit}>
|
|
654
|
+
<div class="form-fields-container">
|
|
655
|
+
<div>
|
|
656
|
+
<div class="mb-2">
|
|
657
|
+
<label
|
|
658
|
+
for="email"
|
|
659
|
+
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
|
660
|
+
>Email</label>
|
|
661
|
+
{#if userEmail || rememberedUser?.email}
|
|
662
|
+
<div
|
|
663
|
+
class="w-full h-10 flex items-center bg-gray-50 dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600 px-3"
|
|
664
|
+
>
|
|
665
|
+
<span class="text-sm font-medium text-gray-900 dark:text-white"
|
|
666
|
+
>{userEmail || rememberedUser?.email}</span
|
|
667
|
+
>
|
|
668
|
+
</div>
|
|
669
|
+
{:else}
|
|
670
|
+
<Input
|
|
671
|
+
id="email"
|
|
672
|
+
placeholder=""
|
|
673
|
+
bind:value={email}
|
|
674
|
+
autocomplete="username"
|
|
675
|
+
/>
|
|
676
|
+
{/if}
|
|
677
|
+
</div>
|
|
678
|
+
</div>
|
|
679
|
+
|
|
680
|
+
<!-- Slot 2: Email error OR Password field (when no email error) -->
|
|
681
|
+
<div class="error-slot">
|
|
682
|
+
{#if errors.email && showErrors}
|
|
683
|
+
<div
|
|
684
|
+
transition:slide={{ duration: 300, easing: cubicOut }}
|
|
685
|
+
class="flex items-center gap-1.5 text-red-500"
|
|
686
|
+
role="alert"
|
|
687
|
+
>
|
|
688
|
+
<WarningIcon className="shrink-0" />
|
|
689
|
+
<p class="text-sm font-medium">
|
|
690
|
+
{errors.email}
|
|
691
|
+
</p>
|
|
692
|
+
</div>
|
|
693
|
+
{/if}
|
|
694
|
+
</div>
|
|
695
|
+
|
|
696
|
+
<!-- Password field - slides between slot 2 and slot 3 -->
|
|
697
|
+
<div class="password-field-container">
|
|
698
|
+
<div class="mb-2">
|
|
699
|
+
<div class="flex justify-between items-center mb-2">
|
|
700
|
+
<label
|
|
701
|
+
for="password"
|
|
702
|
+
class="block text-sm font-medium text-gray-900 dark:text-white"
|
|
703
|
+
>Password</label>
|
|
704
|
+
<button
|
|
705
|
+
type="button"
|
|
706
|
+
onclick={() => {
|
|
707
|
+
view = "reset";
|
|
708
|
+
}}
|
|
709
|
+
class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400"
|
|
710
|
+
>
|
|
711
|
+
Forgot your password?
|
|
712
|
+
</button>
|
|
713
|
+
</div>
|
|
714
|
+
<Input
|
|
715
|
+
id="password"
|
|
716
|
+
placeholder=""
|
|
717
|
+
bind:value={password}
|
|
718
|
+
type="password"
|
|
719
|
+
autocomplete="current-password"
|
|
720
|
+
/>
|
|
721
|
+
</div>
|
|
722
|
+
</div>
|
|
723
|
+
|
|
724
|
+
<!-- Slot 3/4: Password error -->
|
|
725
|
+
<div class="error-slot">
|
|
726
|
+
{#if errors.password && showErrors}
|
|
727
|
+
<div
|
|
728
|
+
transition:slide={{ duration: 300, easing: cubicOut }}
|
|
729
|
+
class="flex items-center gap-1.5 text-red-500"
|
|
730
|
+
role="alert"
|
|
731
|
+
>
|
|
732
|
+
<WarningIcon className="shrink-0" />
|
|
733
|
+
<p class="text-xs font-medium">
|
|
734
|
+
{errors.password}
|
|
735
|
+
</p>
|
|
736
|
+
</div>
|
|
737
|
+
{/if}
|
|
738
|
+
</div>
|
|
739
|
+
</div>
|
|
740
|
+
|
|
741
|
+
<div class="flex justify-between items-center">
|
|
742
|
+
<div class="flex items-center">
|
|
743
|
+
<Checkbox
|
|
744
|
+
bind:checked={rememberMe}
|
|
745
|
+
id="remember-me"
|
|
746
|
+
>Remember me on this device</Checkbox>
|
|
747
|
+
</div>
|
|
748
|
+
</div>
|
|
749
|
+
|
|
750
|
+
<div class="space-y-4">
|
|
751
|
+
<Button
|
|
752
|
+
size="full"
|
|
753
|
+
type="submit"
|
|
754
|
+
loading={isLoading}
|
|
755
|
+
disabled={!isFormValid}
|
|
756
|
+
>
|
|
757
|
+
Log in
|
|
758
|
+
</Button>
|
|
759
|
+
|
|
760
|
+
<div class="relative">
|
|
761
|
+
<div class="absolute inset-0 flex items-center">
|
|
762
|
+
<div
|
|
763
|
+
class="w-full border-t border-gray-300 dark:border-gray-600"
|
|
764
|
+
></div>
|
|
765
|
+
</div>
|
|
766
|
+
<div class="relative flex justify-center text-sm">
|
|
767
|
+
<span class="px-2 bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400"
|
|
768
|
+
>OR</span
|
|
769
|
+
>
|
|
770
|
+
</div>
|
|
771
|
+
</div>
|
|
772
|
+
|
|
773
|
+
<Button
|
|
774
|
+
variant="gray-outline"
|
|
775
|
+
size="full"
|
|
776
|
+
type="button"
|
|
777
|
+
onclick={() => {
|
|
778
|
+
view = "login-link";
|
|
779
|
+
}}
|
|
780
|
+
>
|
|
781
|
+
Send me a login link
|
|
782
|
+
</Button>
|
|
783
|
+
</div>
|
|
784
|
+
</form>
|
|
785
|
+
</div>
|
|
786
|
+
{:else if view === "selection"}
|
|
787
|
+
<div in:fade={{ duration: 200 }} class="space-y-6">
|
|
788
|
+
<div class="space-y-2">
|
|
789
|
+
<h2 class="text-2xl font-normal text-gray-900 dark:text-white">
|
|
790
|
+
Select account
|
|
791
|
+
</h2>
|
|
792
|
+
<p class="text-sm text-gray-600 dark:text-gray-300">
|
|
793
|
+
You have multiple accounts associated with <span
|
|
794
|
+
class="font-medium text-gray-900 dark:text-white"
|
|
795
|
+
>{email}</span
|
|
796
|
+
>. Select one to continue.
|
|
797
|
+
</p>
|
|
798
|
+
</div>
|
|
799
|
+
|
|
800
|
+
<div class="space-y-3">
|
|
801
|
+
{#each accounts as account}
|
|
802
|
+
<button
|
|
803
|
+
class="w-full flex items-center p-4 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-left group"
|
|
804
|
+
onclick={() => handleAccountSelectInternal(account)}
|
|
805
|
+
>
|
|
806
|
+
<div
|
|
807
|
+
class="h-10 w-10 rounded-md bg-gray-200 flex-shrink-0 overflow-hidden mr-4"
|
|
808
|
+
>
|
|
809
|
+
{#if account.performerProfile?.profileImage}
|
|
810
|
+
<img
|
|
811
|
+
src={account.performerProfile.profileImage}
|
|
812
|
+
alt=""
|
|
813
|
+
class="h-full w-full object-cover"
|
|
814
|
+
/>
|
|
815
|
+
{:else}
|
|
816
|
+
<div
|
|
817
|
+
class="h-full w-full flex items-center justify-center bg-blue-100 text-blue-600 font-medium"
|
|
818
|
+
>
|
|
819
|
+
{account.organizationName
|
|
820
|
+
? account.organizationName[0].toUpperCase()
|
|
821
|
+
: account.firstName[0].toUpperCase()}
|
|
822
|
+
</div>
|
|
823
|
+
{/if}
|
|
824
|
+
</div>
|
|
825
|
+
<div class="flex-1 min-w-0">
|
|
826
|
+
<p
|
|
827
|
+
class="text-sm font-medium text-gray-900 dark:text-white truncate"
|
|
828
|
+
>
|
|
829
|
+
{getAccountName(account)}
|
|
830
|
+
</p>
|
|
831
|
+
<p class="text-xs text-gray-500 dark:text-gray-400 capitalize">
|
|
832
|
+
{account.type}
|
|
833
|
+
</p>
|
|
834
|
+
</div>
|
|
835
|
+
<div
|
|
836
|
+
class="text-gray-400 dark:text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-300 transition-colors"
|
|
837
|
+
>
|
|
838
|
+
<svg
|
|
839
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
840
|
+
class="h-5 w-5"
|
|
841
|
+
viewBox="0 0 20 20"
|
|
842
|
+
fill="currentColor"
|
|
843
|
+
>
|
|
844
|
+
<path
|
|
845
|
+
fill-rule="evenodd"
|
|
846
|
+
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
|
847
|
+
clip-rule="evenodd"
|
|
848
|
+
/>
|
|
849
|
+
</svg>
|
|
850
|
+
</div>
|
|
851
|
+
</button>
|
|
852
|
+
{/each}
|
|
853
|
+
</div>
|
|
854
|
+
|
|
855
|
+
<div class="pt-4 border-t border-gray-300 dark:border-gray-600">
|
|
856
|
+
<button
|
|
857
|
+
type="button"
|
|
858
|
+
class="w-full text-center text-sm text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors"
|
|
859
|
+
onclick={handleReturnToSignIn}
|
|
860
|
+
>
|
|
861
|
+
Log in with a different email
|
|
862
|
+
</button>
|
|
863
|
+
</div>
|
|
864
|
+
</div>
|
|
865
|
+
{:else if view === "setup"}
|
|
866
|
+
<div in:fade={{ duration: 200 }} class="space-y-6">
|
|
867
|
+
<div class="space-y-2">
|
|
868
|
+
<h2 class="text-2xl font-normal text-gray-900 dark:text-white">
|
|
869
|
+
{setupFirstName ? `Welcome, ${setupFirstName}` : "Set up your account"}
|
|
870
|
+
</h2>
|
|
871
|
+
<p class="text-sm text-gray-600 dark:text-gray-300">
|
|
872
|
+
Create a password to finish setting up your account.
|
|
873
|
+
</p>
|
|
874
|
+
</div>
|
|
875
|
+
|
|
876
|
+
<form
|
|
877
|
+
class="space-y-6"
|
|
878
|
+
onsubmit={(e) => { e.preventDefault(); handleSetupSubmit(); }}
|
|
879
|
+
>
|
|
880
|
+
<div class="space-y-5">
|
|
881
|
+
<div>
|
|
882
|
+
<div class="mb-6">
|
|
883
|
+
<div class="flex justify-between items-baseline mb-2">
|
|
884
|
+
<label
|
|
885
|
+
for="setup-password"
|
|
886
|
+
class="block text-sm font-medium text-gray-900 dark:text-white"
|
|
887
|
+
>Create password</label>
|
|
888
|
+
{#if strengthText}
|
|
889
|
+
<span
|
|
890
|
+
class="text-xs font-medium {strengthTextColor} transition-opacity duration-300"
|
|
891
|
+
transition:fade>{strengthText}</span
|
|
892
|
+
>
|
|
893
|
+
{/if}
|
|
894
|
+
</div>
|
|
895
|
+
<Input
|
|
896
|
+
id="setup-password"
|
|
897
|
+
placeholder=""
|
|
898
|
+
bind:value={password}
|
|
899
|
+
type="password"
|
|
900
|
+
autocomplete="new-password"
|
|
901
|
+
/>
|
|
902
|
+
<PasswordStrengthIndicator
|
|
903
|
+
{password}
|
|
904
|
+
bind:strengthText
|
|
905
|
+
bind:textColor={strengthTextColor}
|
|
906
|
+
bind:score
|
|
907
|
+
>
|
|
908
|
+
{#if errors.password && showErrors}
|
|
909
|
+
<div
|
|
910
|
+
transition:slide={{ duration: 600, easing: cubicOut }}
|
|
911
|
+
class="flex items-start gap-1.5 text-red-500"
|
|
912
|
+
role="alert"
|
|
913
|
+
>
|
|
914
|
+
<WarningIcon className="shrink-0 mt-1" />
|
|
915
|
+
<p class="text-xs font-medium">
|
|
916
|
+
{errors.password}
|
|
917
|
+
</p>
|
|
918
|
+
</div>
|
|
919
|
+
{/if}
|
|
920
|
+
</PasswordStrengthIndicator>
|
|
921
|
+
</div>
|
|
922
|
+
</div>
|
|
923
|
+
</div>
|
|
924
|
+
|
|
925
|
+
<div class="space-y-4">
|
|
926
|
+
<Button
|
|
927
|
+
size="full"
|
|
928
|
+
type="submit"
|
|
929
|
+
loading={isLoading}
|
|
930
|
+
disabled={!isSetupValid}
|
|
931
|
+
>
|
|
932
|
+
Complete setup
|
|
933
|
+
</Button>
|
|
934
|
+
|
|
935
|
+
<div class="text-center">
|
|
936
|
+
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
937
|
+
By setting up your account, you agree to Micdrop's <a
|
|
938
|
+
href={tosUrl}
|
|
939
|
+
target="_blank"
|
|
940
|
+
rel="noopener noreferrer"
|
|
941
|
+
class="text-blue-600 hover:underline dark:text-blue-400">terms of service</a
|
|
942
|
+
>.
|
|
943
|
+
</p>
|
|
944
|
+
</div>
|
|
945
|
+
</div>
|
|
946
|
+
</form>
|
|
947
|
+
</div>
|
|
948
|
+
{:else if view === "reset"}
|
|
949
|
+
<div in:fade={{ duration: 200 }} class="space-y-6">
|
|
950
|
+
<div class="space-y-2">
|
|
951
|
+
<h2 class="text-2xl font-normal text-gray-900 dark:text-white">
|
|
952
|
+
Reset your password
|
|
953
|
+
</h2>
|
|
954
|
+
<p class="text-sm text-gray-600 dark:text-gray-300">
|
|
955
|
+
Enter the email address associated with your account and we'll
|
|
956
|
+
send you a link to reset your password.
|
|
957
|
+
</p>
|
|
958
|
+
</div>
|
|
959
|
+
|
|
960
|
+
<form
|
|
961
|
+
class="space-y-6"
|
|
962
|
+
onsubmit={(e) => { e.preventDefault(); handleResetSubmit(); }}
|
|
963
|
+
>
|
|
964
|
+
<div class="space-y-5">
|
|
965
|
+
<div>
|
|
966
|
+
<div class="mb-6">
|
|
967
|
+
<label
|
|
968
|
+
for="reset-email"
|
|
969
|
+
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
|
970
|
+
>Email</label>
|
|
971
|
+
<Input
|
|
972
|
+
id="reset-email"
|
|
973
|
+
placeholder=""
|
|
974
|
+
bind:value={email}
|
|
975
|
+
autocomplete="username"
|
|
976
|
+
/>
|
|
977
|
+
</div>
|
|
978
|
+
<div class="error-slot">
|
|
979
|
+
{#if errors.email && showErrors}
|
|
980
|
+
<div
|
|
981
|
+
transition:slide={{ duration: 300, easing: cubicOut }}
|
|
982
|
+
class="flex items-center gap-1.5 text-red-500"
|
|
983
|
+
role="alert"
|
|
984
|
+
>
|
|
985
|
+
<WarningIcon className="shrink-0" />
|
|
986
|
+
<p class="text-sm font-medium">
|
|
987
|
+
{errors.email}
|
|
988
|
+
</p>
|
|
989
|
+
</div>
|
|
990
|
+
{/if}
|
|
991
|
+
</div>
|
|
992
|
+
</div>
|
|
993
|
+
</div>
|
|
994
|
+
|
|
995
|
+
<div class="space-y-4">
|
|
996
|
+
<Button
|
|
997
|
+
size="full"
|
|
998
|
+
type="submit"
|
|
999
|
+
loading={isLoading}
|
|
1000
|
+
disabled={!email}
|
|
1001
|
+
>
|
|
1002
|
+
Send reset link
|
|
1003
|
+
</Button>
|
|
1004
|
+
|
|
1005
|
+
<div class="text-center">
|
|
1006
|
+
<button
|
|
1007
|
+
type="button"
|
|
1008
|
+
onclick={handleReturnToSignIn}
|
|
1009
|
+
class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400"
|
|
1010
|
+
>
|
|
1011
|
+
Return to log in
|
|
1012
|
+
</button>
|
|
1013
|
+
</div>
|
|
1014
|
+
</div>
|
|
1015
|
+
</form>
|
|
1016
|
+
</div>
|
|
1017
|
+
{:else if view === "login-link"}
|
|
1018
|
+
<div in:fade={{ duration: 200 }} class="space-y-6">
|
|
1019
|
+
<div class="space-y-2">
|
|
1020
|
+
<h2 class="text-2xl font-normal text-gray-900 dark:text-white">
|
|
1021
|
+
Send a login link
|
|
1022
|
+
</h2>
|
|
1023
|
+
<p class="text-sm text-gray-600 dark:text-gray-300">
|
|
1024
|
+
Enter the email address associated with your account and we'll
|
|
1025
|
+
send you a link to sign in.
|
|
1026
|
+
</p>
|
|
1027
|
+
</div>
|
|
1028
|
+
|
|
1029
|
+
<form
|
|
1030
|
+
class="space-y-6"
|
|
1031
|
+
onsubmit={(e) => { e.preventDefault(); handleLoginLinkSubmit(); }}
|
|
1032
|
+
>
|
|
1033
|
+
<div class="space-y-5">
|
|
1034
|
+
<div>
|
|
1035
|
+
<div class="mb-6">
|
|
1036
|
+
<label
|
|
1037
|
+
for="login-link-email"
|
|
1038
|
+
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
|
1039
|
+
>Email</label>
|
|
1040
|
+
<Input
|
|
1041
|
+
id="login-link-email"
|
|
1042
|
+
placeholder=""
|
|
1043
|
+
bind:value={email}
|
|
1044
|
+
autocomplete="username"
|
|
1045
|
+
/>
|
|
1046
|
+
</div>
|
|
1047
|
+
<div class="error-slot">
|
|
1048
|
+
{#if errors.email && showErrors}
|
|
1049
|
+
<div
|
|
1050
|
+
transition:slide={{ duration: 300, easing: cubicOut }}
|
|
1051
|
+
class="flex items-center gap-1.5 text-red-500"
|
|
1052
|
+
role="alert"
|
|
1053
|
+
>
|
|
1054
|
+
<WarningIcon className="shrink-0" />
|
|
1055
|
+
<p class="text-sm font-medium">
|
|
1056
|
+
{errors.email}
|
|
1057
|
+
</p>
|
|
1058
|
+
</div>
|
|
1059
|
+
{/if}
|
|
1060
|
+
</div>
|
|
1061
|
+
</div>
|
|
1062
|
+
</div>
|
|
1063
|
+
|
|
1064
|
+
{#if loginLinkMessage}
|
|
1065
|
+
<p
|
|
1066
|
+
class="text-sm {loginLinkMessage.includes('sent') ||
|
|
1067
|
+
loginLinkMessage.includes('check your email')
|
|
1068
|
+
? 'text-green-600 bg-green-50'
|
|
1069
|
+
: 'text-red-600 bg-red-50'} p-3 rounded-md"
|
|
1070
|
+
role="alert"
|
|
1071
|
+
>
|
|
1072
|
+
{loginLinkMessage}
|
|
1073
|
+
</p>
|
|
1074
|
+
{/if}
|
|
1075
|
+
|
|
1076
|
+
<div class="space-y-4">
|
|
1077
|
+
<Button
|
|
1078
|
+
size="full"
|
|
1079
|
+
type="submit"
|
|
1080
|
+
loading={isLoading}
|
|
1081
|
+
disabled={!email}
|
|
1082
|
+
>
|
|
1083
|
+
Send login link
|
|
1084
|
+
</Button>
|
|
1085
|
+
|
|
1086
|
+
<div class="text-center">
|
|
1087
|
+
<button
|
|
1088
|
+
type="button"
|
|
1089
|
+
onclick={handleReturnToSignIn}
|
|
1090
|
+
class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400"
|
|
1091
|
+
>
|
|
1092
|
+
Return to log in
|
|
1093
|
+
</button>
|
|
1094
|
+
</div>
|
|
1095
|
+
</div>
|
|
1096
|
+
</form>
|
|
1097
|
+
</div>
|
|
1098
|
+
{:else if view === "success"}
|
|
1099
|
+
<div in:fade={{ duration: 200 }} class="space-y-6 text-center">
|
|
1100
|
+
<div class="space-y-4">
|
|
1101
|
+
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
|
|
1102
|
+
Check your email
|
|
1103
|
+
</h2>
|
|
1104
|
+
{#if isResendSuccess}
|
|
1105
|
+
<p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed text-left">
|
|
1106
|
+
We've resent {successType === "reset"
|
|
1107
|
+
? "password reset instructions"
|
|
1108
|
+
: "a login link"} to
|
|
1109
|
+
<span class="font-bold text-gray-900 dark:text-white"
|
|
1110
|
+
>{successEmail}</span
|
|
1111
|
+
> if it is an email on file.
|
|
1112
|
+
</p>
|
|
1113
|
+
<p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed text-left">
|
|
1114
|
+
Please check again. If you still haven't received an email, <button
|
|
1115
|
+
type="button"
|
|
1116
|
+
class="text-blue-600 font-medium dark:text-blue-400"
|
|
1117
|
+
onclick={handleTryDifferentEmail}
|
|
1118
|
+
>try a different email</button
|
|
1119
|
+
>.
|
|
1120
|
+
</p>
|
|
1121
|
+
{:else}
|
|
1122
|
+
<p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed text-left">
|
|
1123
|
+
Thanks! If <span
|
|
1124
|
+
class="font-bold text-gray-900 dark:text-white"
|
|
1125
|
+
>{successEmail}</span
|
|
1126
|
+
>
|
|
1127
|
+
matches an email we have on file, then we've sent you an email
|
|
1128
|
+
containing further instructions for {successType === "reset"
|
|
1129
|
+
? "resetting your password"
|
|
1130
|
+
: "signing in"}.
|
|
1131
|
+
</p>
|
|
1132
|
+
<p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed text-left">
|
|
1133
|
+
If you haven't received an email in 5 minutes, check your
|
|
1134
|
+
spam, <button
|
|
1135
|
+
type="button"
|
|
1136
|
+
class="text-blue-600 font-medium dark:text-blue-400"
|
|
1137
|
+
onclick={handleResend}>resend</button
|
|
1138
|
+
>, or
|
|
1139
|
+
<button
|
|
1140
|
+
type="button"
|
|
1141
|
+
class="text-blue-600 font-medium dark:text-blue-400"
|
|
1142
|
+
onclick={handleTryDifferentEmail}
|
|
1143
|
+
>try a different email</button
|
|
1144
|
+
>.
|
|
1145
|
+
</p>
|
|
1146
|
+
{/if}
|
|
1147
|
+
</div>
|
|
1148
|
+
|
|
1149
|
+
<div class="pt-2">
|
|
1150
|
+
<button
|
|
1151
|
+
type="button"
|
|
1152
|
+
onclick={handleReturnToSignIn}
|
|
1153
|
+
class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400"
|
|
1154
|
+
>
|
|
1155
|
+
Return to sign in
|
|
1156
|
+
</button>
|
|
1157
|
+
</div>
|
|
1158
|
+
</div>
|
|
1159
|
+
{/if}
|
|
1160
|
+
</div>
|
|
1161
|
+
</div>
|
|
1162
|
+
</div>
|
|
1163
|
+
</div>
|
|
1164
|
+
|
|
1165
|
+
{#if showDarkModeToggle}
|
|
1166
|
+
<div class="fixed bottom-2 flex justify-center w-full">
|
|
1167
|
+
<DarkModeToggle />
|
|
1168
|
+
</div>
|
|
1169
|
+
{/if}
|
|
1170
|
+
|
|
1171
|
+
<style>
|
|
1172
|
+
.superlogin-container :global(body) {
|
|
1173
|
+
background-color: #f8fafc;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
.animated-gradient {
|
|
1177
|
+
background: linear-gradient(
|
|
1178
|
+
-45deg,
|
|
1179
|
+
#c084fc,
|
|
1180
|
+
#f9a8d4,
|
|
1181
|
+
#93c5fd,
|
|
1182
|
+
#a78bfa,
|
|
1183
|
+
#f0abfc
|
|
1184
|
+
);
|
|
1185
|
+
background-size: 400% 400%;
|
|
1186
|
+
animation: gradientShift 15s ease infinite;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
@keyframes gradientShift {
|
|
1190
|
+
0% {
|
|
1191
|
+
background-position: 0% 0%;
|
|
1192
|
+
}
|
|
1193
|
+
50% {
|
|
1194
|
+
background-position: 100% 100%;
|
|
1195
|
+
}
|
|
1196
|
+
100% {
|
|
1197
|
+
background-position: 0% 0%;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
.transition-height {
|
|
1202
|
+
transition: height 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
.shake {
|
|
1206
|
+
animation: shake 0.625s linear;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
@keyframes shake {
|
|
1210
|
+
8%,
|
|
1211
|
+
41% {
|
|
1212
|
+
transform: translateX(-10px);
|
|
1213
|
+
}
|
|
1214
|
+
25%,
|
|
1215
|
+
58% {
|
|
1216
|
+
transform: translateX(10px);
|
|
1217
|
+
}
|
|
1218
|
+
75% {
|
|
1219
|
+
transform: translateX(-5px);
|
|
1220
|
+
}
|
|
1221
|
+
92% {
|
|
1222
|
+
transform: translateX(5px);
|
|
1223
|
+
}
|
|
1224
|
+
0%,
|
|
1225
|
+
100% {
|
|
1226
|
+
transform: translateX(0);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
.form-fields-container {
|
|
1231
|
+
display: flex;
|
|
1232
|
+
flex-direction: column;
|
|
1233
|
+
gap: 0;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
.error-slot {
|
|
1237
|
+
margin-top: 4px;
|
|
1238
|
+
margin-bottom: 0;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/* Card fade-in animation */
|
|
1242
|
+
.card-hidden {
|
|
1243
|
+
opacity: 0;
|
|
1244
|
+
transform: translateY(12px);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
.card-visible {
|
|
1248
|
+
opacity: 1;
|
|
1249
|
+
transform: translateY(0);
|
|
1250
|
+
transition: opacity 0.5s ease-out, transform 0.5s ease-out;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/* Stripe-matching responsive card sizing */
|
|
1254
|
+
/* Desktop mode - fixed 541px width */
|
|
1255
|
+
.login-card {
|
|
1256
|
+
width: 541px;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
.login-card-padding {
|
|
1260
|
+
padding: 48px;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/* Mobile breakpoint - snaps at 846px viewport width, matches Stripe */
|
|
1264
|
+
@media (max-width: 846px) {
|
|
1265
|
+
.login-card {
|
|
1266
|
+
width: 383px;
|
|
1267
|
+
}
|
|
1268
|
+
.login-card-padding {
|
|
1269
|
+
padding: 32px;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
@media (max-width: 480px) {
|
|
1274
|
+
.login-card {
|
|
1275
|
+
width: 100%;
|
|
1276
|
+
margin: 0 16px;
|
|
1277
|
+
}
|
|
1278
|
+
.login-card-padding {
|
|
1279
|
+
padding: 24px;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
</style>
|