diff --git a/package-lock.json b/package-lock.json index 4694cfb8..d277bbfe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "signal-range", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "signal-range", - "version": "1.0.1", + "version": "1.0.2", "license": "AGPL-3.0", "dependencies": { "@supabase/supabase-js": "^2.81.1", diff --git a/package.json b/package.json index b8c7ff56..f7f4bbed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "signal-range", - "version": "1.0.1", + "version": "1.0.2", "description": "Signal Range: Space Electronic Warfare Lab", "main": "dist/index.js", "scripts": { diff --git a/public/images/discord-white.png b/public/images/discord-white.png new file mode 100644 index 00000000..fa059ee3 Binary files /dev/null and b/public/images/discord-white.png differ diff --git a/public/images/discord.png b/public/images/discord.png new file mode 100644 index 00000000..fe14a78f Binary files /dev/null and b/public/images/discord.png differ diff --git a/public/images/person-blue.png b/public/images/person-blue.png new file mode 100644 index 00000000..233d9e0e Binary files /dev/null and b/public/images/person-blue.png differ diff --git a/src/user-account/auth.ts b/src/user-account/auth.ts index a16afd27..568a41a1 100644 --- a/src/user-account/auth.ts +++ b/src/user-account/auth.ts @@ -69,7 +69,7 @@ export class Auth { // Sign in with OAuth provider (GitHub, Facebook, Google, LinkedIn) static signInWithOAuthProvider( - provider: 'github' | 'facebook' | 'google' | 'linkedin_oidc', + provider: 'github' | 'facebook' | 'google' | 'linkedin_oidc' | 'discord', popupName?: string, ): Promise<{ user: User | null; error: Error | null }> { return new Promise((resolve, reject) => { diff --git a/src/user-account/modal-login.ts b/src/user-account/modal-login.ts index 45dbb047..fd3bbd7b 100644 --- a/src/user-account/modal-login.ts +++ b/src/user-account/modal-login.ts @@ -45,12 +45,20 @@ const oauthButtons = [ text: 'Continue with Facebook', cssClass: 'oauth-btn oauth-btn--facebook', }, + { + id: 'discord-signin-btn', + provider: 'discord', + icon: '/images/discord-white.png', + text: 'Continue with Discord', + cssClass: 'oauth-btn oauth-btn--discord', + } ] as OAuthButton[]; export class ModalLogin extends DraggableModal { private static readonly id = 'modal-login'; - private static readonly isEmailSignInEnabled = false; + private static readonly isEmailSignInEnabled = true; private static instance_: ModalLogin | null = null; + private isSignUpMode_ = true; private constructor() { if (ModalLogin.instance_) { @@ -68,10 +76,13 @@ export class ModalLogin extends DraggableModal { protected getModalContentHtml(): string { return html` + ${this.renderEmailForm()} +
+ or continue with +
${this.renderOAuthButtons()}
- ${this.renderEmailForm()} `; } @@ -94,42 +105,45 @@ export class ModalLogin extends DraggableModal { } return ` -
- or +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +

+ Already have an account? + Sign in +

+
- -
-
- - -
- -
- - -
- -
- - -
-
`; } @@ -153,26 +167,56 @@ export class ModalLogin extends DraggableModal { } private initializeEmailForm(): void { - const loginForm = this.getElement('login-form') as HTMLFormElement; - const signupBtn = this.getElement('signup-btn') as HTMLButtonElement; - const emailInput = this.getElement('email') as HTMLInputElement; - const passwordInput = this.getElement('password') as HTMLInputElement; + const authForm = this.getElement('auth-form') as HTMLFormElement; + const toggleLink = this.getElement('auth-toggle-link') as HTMLAnchorElement; - if (signupBtn) { - signupBtn.addEventListener('click', (event) => { + if (toggleLink) { + toggleLink.addEventListener('click', (event) => { event.preventDefault(); - this.handleSignUp(emailInput.value.trim(), passwordInput.value.trim()); + this.isSignUpMode_ = !this.isSignUpMode_; + this.updateAuthModeUI_(); }); } - if (loginForm) { - loginForm.addEventListener('submit', (event) => { + if (authForm) { + authForm.addEventListener('submit', (event) => { event.preventDefault(); - this.handleEmailLogin(emailInput.value.trim(), passwordInput.value.trim()); + const emailInput = this.getElement('auth-email') as HTMLInputElement; + const passwordInput = this.getElement('auth-password') as HTMLInputElement; + const email = emailInput.value.trim(); + const password = passwordInput.value.trim(); + + if (this.isSignUpMode_) { + this.handleSignUp(email, password); + } else { + this.handleEmailLogin(email, password); + } }); } } + private updateAuthModeUI_(): void { + this.clearError_(); + + const submitBtn = this.getElement('auth-submit') as HTMLButtonElement; + const toggleText = this.getElement('auth-toggle-text') as HTMLSpanElement; + const toggleLink = this.getElement('auth-toggle-link') as HTMLAnchorElement; + const passwordInput = this.getElement('auth-password') as HTMLInputElement; + + if (submitBtn) { + submitBtn.textContent = this.isSignUpMode_ ? 'Sign Up' : 'Sign In'; + } + if (toggleText) { + toggleText.textContent = this.isSignUpMode_ ? 'Already have an account?' : "Don't have an account?"; + } + if (toggleLink) { + toggleLink.textContent = this.isSignUpMode_ ? 'Sign in' : 'Sign up'; + } + if (passwordInput) { + passwordInput.setAttribute('autocomplete', this.isSignUpMode_ ? 'new-password' : 'current-password'); + } + } + private async handleOAuthSignIn(buttonConfig: OAuthButton): Promise { const button = this.getElement(buttonConfig.id) as HTMLButtonElement; @@ -221,27 +265,85 @@ export class ModalLogin extends DraggableModal { return providerNames[provider] || provider; } + private showError_(message: string): void { + const errorEl = this.getElement('auth-error'); + + if (errorEl) { + errorEl.textContent = message; + errorEl.style.display = 'block'; + } + } + + private clearError_(): void { + const errorEl = this.getElement('auth-error'); + + if (errorEl) { + errorEl.style.display = 'none'; + } + } + + private getUserFriendlyError_(message: string): string { + if (message.includes('Invalid login credentials')) { + return 'Invalid email or password'; + } + if (message.includes('Email not confirmed')) { + return 'Please confirm your email before signing in'; + } + if (message.includes('already registered')) { + return 'An account with this email already exists'; + } + + return message; + } + + private setSubmitLoading_(isLoading: boolean): void { + const submitBtn = this.getElement('auth-submit') as HTMLButtonElement; + + if (!submitBtn) return; + + submitBtn.disabled = isLoading; + if (isLoading) { + submitBtn.textContent = this.isSignUpMode_ ? 'Signing up...' : 'Signing in...'; + } else { + submitBtn.textContent = this.isSignUpMode_ ? 'Sign Up' : 'Sign In'; + } + } + private async handleSignUp(email: string, password: string): Promise { if (!email || !password) { return; } + this.clearError_(); + this.setSubmitLoading_(true); try { await this.signUp_(email, password); errorManagerInstance.info('Sign up successful! Check email for confirmation.'); hideEl(this.boxEl!); } catch (error) { - errorManagerInstance.warn(`Sign up failed: ${(error as Error).message}`); + const message = (error as Error).message; + + this.showError_(this.getUserFriendlyError_(message)); + errorManagerInstance.warn(`Sign up failed: ${message}`); + } finally { + this.setSubmitLoading_(false); } } private async handleEmailLogin(email: string, password: string): Promise { + this.clearError_(); + this.setSubmitLoading_(true); try { await this.login_(email, password); SoundManager.getInstance().play(Sfx.POWER_ON); hideEl(this.boxEl!); } catch (error) { - errorManagerInstance.warn(`Login failed: ${(error as Error).message}`); + const message = (error as Error).message; + + this.showError_(this.getUserFriendlyError_(message)); + errorManagerInstance.warn(`Login failed: ${message}`); + } finally { + this.setSubmitLoading_(false); } } diff --git a/src/user-account/user-account.css b/src/user-account/user-account.css index 4e09005e..48e7b90e 100644 --- a/src/user-account/user-account.css +++ b/src/user-account/user-account.css @@ -150,13 +150,21 @@ background-color: #1567d2; } +.oauth-btn--discord { + background-color: #5865F2; + color: #ffffff; +} + +.oauth-btn--discord:hover:not(:disabled) { + background-color: #4752C4; +} + /* ========================================================================== AUTH DIVIDER ========================================================================== */ .auth-divider { text-align: center; - margin: var(--user-account-spacing-xl) 0; position: relative; } @@ -172,11 +180,11 @@ .auth-divider__text { background-color: var(--color-dark-background); - padding: 0 var(--user-account-spacing-lg); color: #ffffff; font-size: 12px; text-transform: uppercase; letter-spacing: 1px; + position: relative; } /* ========================================================================== @@ -184,7 +192,6 @@ ========================================================================== */ .auth-form { - padding: 0 var(--user-account-spacing-xl); display: flex; flex-direction: column; gap: var(--user-account-spacing-lg); @@ -256,6 +263,44 @@ color: #ffffff; } +.auth-form__btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.auth-form__error { + display: none; + background-color: rgba(183, 28, 12, 0.15); + border: 1px solid #b71c0c; + border-radius: var(--user-account-border-radius); + color: #ff8a8a; + font-size: 0.875rem; + padding: var(--user-account-spacing-sm) var(--user-account-spacing-md); + text-align: center; +} + +/* ========================================================================== + AUTH TOGGLE + ========================================================================== */ + +.auth-toggle { + text-align: center; + font-size: 0.875rem; + color: var(--color-dark-text-muted); + margin-top: var(--user-account-spacing-lg); +} + +.auth-toggle-link { + color: var(--color-primary); + text-decoration: none; + font-weight: 500; + margin-left: var(--user-account-spacing-xs); +} + +.auth-toggle-link:hover { + text-decoration: underline; +} + /* ========================================================================== PROFILE MODAL LAYOUT ========================================================================== */