Skip to content
Merged

Dev #166

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
Binary file added public/images/discord-white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/discord.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/person-blue.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/user-account/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
200 changes: 151 additions & 49 deletions src/user-account/modal-login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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_) {
Expand All @@ -68,10 +76,13 @@ export class ModalLogin extends DraggableModal {

protected getModalContentHtml(): string {
return html`
${this.renderEmailForm()}
<div class="auth-divider">
<span class="auth-divider__text">or continue with</span>
</div>
<div class="oauth-section">
${this.renderOAuthButtons()}
</div>
${this.renderEmailForm()}
`;
}

Expand All @@ -94,42 +105,45 @@ export class ModalLogin extends DraggableModal {
}

return `
<div class="auth-divider">
<span class="auth-divider__text">or</span>
<div style="padding: var(--user-account-spacing-lg);">
<form id="auth-form" class="auth-form">
<div class="auth-form__field">
<input
type="email"
id="auth-email"
name="email"
placeholder="Email"
class="auth-form__input keyboard-priority"
autocomplete="email"
required
/>
</div>

<div class="auth-form__field">
<input
type="password"
id="auth-password"
name="password"
placeholder="Password"
minlength="6"
class="auth-form__input keyboard-priority"
autocomplete="new-password"
required
/>
</div>

<div id="auth-error" class="auth-form__error"></div>

<div class="auth-form__actions">
<button type="submit" id="auth-submit" class="auth-form__btn auth-form__btn--primary">Sign Up</button>
</div>

<p class="auth-toggle">
<span id="auth-toggle-text">Already have an account?</span>
<a href="#" id="auth-toggle-link" class="auth-toggle-link">Sign in</a>
</p>
</form>
</div>

<form id="login-form" class="auth-form">
<div class="auth-form__field">
<label for="email" class="auth-form__label">Email:</label>
<input
type="email"
id="email"
name="email"
class="auth-form__input keyboard-priority"
autoComplete="username"
required
/>
</div>

<div class="auth-form__field">
<label for="password" class="auth-form__label">Password:</label>
<input
type="password"
id="password"
name="password"
class="auth-form__input keyboard-priority"
autoComplete="current-password"
required
/>
</div>

<div class="auth-form__actions">
<button type="submit" class="auth-form__btn auth-form__btn--primary">Login</button>
<button type="button" id="signup-btn" class="auth-form__btn auth-form__btn--secondary">
Sign Up
</button>
</div>
</form>
`;
}

Expand All @@ -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<void> {
const button = this.getElement(buttonConfig.id) as HTMLButtonElement;

Expand Down Expand Up @@ -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<void> {
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<void> {
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);
}
}

Expand Down
51 changes: 48 additions & 3 deletions src/user-account/user-account.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -172,19 +180,18 @@

.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;
}

/* ==========================================================================
AUTH FORM
========================================================================== */

.auth-form {
padding: 0 var(--user-account-spacing-xl);
display: flex;
flex-direction: column;
gap: var(--user-account-spacing-lg);
Expand Down Expand Up @@ -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
========================================================================== */
Expand Down
Loading