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
+
-
-
`;
}
@@ -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
========================================================================== */