feat(developers): simplify timezone dropdown and make email optional
All checks were successful
Build and Deploy / deploy (push) Successful in 42s

Replace 100+ raw system timezones with curated list of 26 major zones
with browser auto-detection via Intl API. Remove email requirement since
applicants receive a @silverlabs.uk address — fallback to username@silverlabs.uk
when no personal email is provided.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 23:55:08 +00:00
parent 44e3ad94e0
commit dc9a60a7a2
3 changed files with 70 additions and 15 deletions

View File

@@ -2,6 +2,7 @@
@using SilverLabs.Website.Models @using SilverLabs.Website.Models
@using SilverLabs.Website.Services @using SilverLabs.Website.Services
@inject DeveloperApplicationService ApplicationService @inject DeveloperApplicationService ApplicationService
@inject IJSRuntime JS
@rendermode InteractiveServer @rendermode InteractiveServer
<PageTitle>Join the Team - SilverLabs</PageTitle> <PageTitle>Join the Team - SilverLabs</PageTitle>
@@ -85,8 +86,9 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="email">Email Address</label> <label for="email">Email Address (optional)</label>
<InputText id="email" @bind-Value="_application.Email" class="form-input" placeholder="jane@example.com" /> <InputText id="email" @bind-Value="_application.Email" class="form-input" placeholder="jane@example.com" />
<span class="form-hint">Leave blank to use your @@silverlabs.uk address</span>
<ValidationMessage For="() => _application.Email" /> <ValidationMessage For="() => _application.Email" />
</div> </div>
@@ -256,10 +258,35 @@
private readonly string[] _availablePlatforms = { "Windows", "macOS", "Linux", "Android", "iOS", "Other" }; private readonly string[] _availablePlatforms = { "Windows", "macOS", "Linux", "Android", "iOS", "Other" };
private static readonly List<(string Id, string Label)> _timezones = TimeZoneInfo.GetSystemTimeZones() private static readonly List<(string Id, string Label)> _timezones = new()
.OrderBy(tz => tz.BaseUtcOffset) {
.Select(tz => (tz.Id, $"(UTC{(tz.BaseUtcOffset >= TimeSpan.Zero ? "+" : "")}{tz.BaseUtcOffset:hh\\:mm}) {tz.DisplayName}")) ("Pacific/Midway", "(UTC-11:00) Midway Island"),
.ToList(); ("Pacific/Honolulu", "(UTC-10:00) Hawaii"),
("America/Anchorage", "(UTC-09:00) Alaska"),
("America/Los_Angeles", "(UTC-08:00) Pacific Time (US & Canada)"),
("America/Denver", "(UTC-07:00) Mountain Time (US & Canada)"),
("America/Chicago", "(UTC-06:00) Central Time (US & Canada)"),
("America/New_York", "(UTC-05:00) Eastern Time (US & Canada)"),
("America/Caracas", "(UTC-04:00) Venezuela"),
("America/Halifax", "(UTC-04:00) Atlantic Time (Canada)"),
("America/Sao_Paulo", "(UTC-03:00) Brazil"),
("Atlantic/South_Georgia","(UTC-02:00) Mid-Atlantic"),
("Atlantic/Azores", "(UTC-01:00) Azores"),
("Europe/London", "(UTC+00:00) London, Dublin, Lisbon"),
("Europe/Berlin", "(UTC+01:00) Berlin, Paris, Amsterdam"),
("Europe/Bucharest", "(UTC+02:00) Bucharest, Helsinki, Athens"),
("Europe/Moscow", "(UTC+03:00) Moscow, Istanbul"),
("Asia/Dubai", "(UTC+04:00) Dubai, Baku"),
("Asia/Karachi", "(UTC+05:00) Karachi, Tashkent"),
("Asia/Kolkata", "(UTC+05:30) Mumbai, New Delhi"),
("Asia/Dhaka", "(UTC+06:00) Dhaka, Almaty"),
("Asia/Bangkok", "(UTC+07:00) Bangkok, Jakarta"),
("Asia/Shanghai", "(UTC+08:00) Beijing, Singapore, Perth"),
("Asia/Tokyo", "(UTC+09:00) Tokyo, Seoul"),
("Australia/Sydney", "(UTC+10:00) Sydney, Melbourne"),
("Pacific/Noumea", "(UTC+11:00) Solomon Islands"),
("Pacific/Auckland", "(UTC+12:00) Auckland, Fiji"),
};
private static readonly System.Text.RegularExpressions.Regex UsernamePattern = private static readonly System.Text.RegularExpressions.Regex UsernamePattern =
new(@"^[a-zA-Z0-9_-]{3,30}$", System.Text.RegularExpressions.RegexOptions.Compiled); new(@"^[a-zA-Z0-9_-]{3,30}$", System.Text.RegularExpressions.RegexOptions.Compiled);
@@ -269,6 +296,26 @@
private bool IsSubmitDisabled => private bool IsSubmitDisabled =>
_submitting || _usernameCheckState == UsernameCheckState.Taken || _usernameCheckState == UsernameCheckState.Checking; _submitting || _usernameCheckState == UsernameCheckState.Taken || _usernameCheckState == UsernameCheckState.Checking;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && string.IsNullOrEmpty(_application.Timezone))
{
try
{
var detectedTz = await JS.InvokeAsync<string>("eval", "Intl.DateTimeFormat().resolvedOptions().timeZone");
if (!string.IsNullOrEmpty(detectedTz) && _timezones.Any(tz => tz.Id == detectedTz))
{
_application.Timezone = detectedTz;
StateHasChanged();
}
}
catch
{
// Browser may not support Intl API — ignore
}
}
}
private void SelectRole(ApplicationRole role) private void SelectRole(ApplicationRole role)
{ {
_application.Role = role; _application.Role = role;

View File

@@ -8,9 +8,8 @@ public class DeveloperApplication
[StringLength(100, MinimumLength = 2)] [StringLength(100, MinimumLength = 2)]
public string FullName { get; set; } = string.Empty; public string FullName { get; set; } = string.Empty;
[Required(ErrorMessage = "Email is required")]
[EmailAddress(ErrorMessage = "Invalid email address")] [EmailAddress(ErrorMessage = "Invalid email address")]
public string Email { get; set; } = string.Empty; public string? Email { get; set; }
[Required(ErrorMessage = "Username is required")] [Required(ErrorMessage = "Username is required")]
[RegularExpression(@"^[a-zA-Z0-9_-]{3,30}$", ErrorMessage = "Username must be 3-30 characters, letters, numbers, hyphens and underscores only")] [RegularExpression(@"^[a-zA-Z0-9_-]{3,30}$", ErrorMessage = "Username must be 3-30 characters, letters, numbers, hyphens and underscores only")]

View File

@@ -48,11 +48,16 @@ public class DeveloperApplicationService
{ {
try try
{ {
// Use silverlabs.uk address when no personal email provided
var effectiveEmail = string.IsNullOrWhiteSpace(application.Email)
? $"{application.DesiredUsername}@silverlabs.uk"
: application.Email.Trim();
// 1. Register user on SilverDESK // 1. Register user on SilverDESK
var registerPayload = new var registerPayload = new
{ {
username = application.DesiredUsername, username = application.DesiredUsername,
email = application.Email, email = effectiveEmail,
password = application.Password, password = application.Password,
fullName = application.FullName fullName = application.FullName
}; };
@@ -111,7 +116,7 @@ public class DeveloperApplicationService
userId, userId,
ticketId, ticketId,
fullName = application.FullName, fullName = application.FullName,
email = application.Email, email = effectiveEmail,
desiredUsername = application.DesiredUsername, desiredUsername = application.DesiredUsername,
timezone = application.Timezone, timezone = application.Timezone,
appliedRole = application.Role.ToString(), appliedRole = application.Role.ToString(),
@@ -126,29 +131,29 @@ public class DeveloperApplicationService
if (appResponse.IsSuccessStatusCode) if (appResponse.IsSuccessStatusCode)
{ {
_logger.LogInformation("DeveloperApplication record created for {Email}", application.Email); _logger.LogInformation("DeveloperApplication record created for {Email}", effectiveEmail);
} }
else else
{ {
var appError = await appResponse.Content.ReadAsStringAsync(); var appError = await appResponse.Content.ReadAsStringAsync();
_logger.LogWarning("Failed to create DeveloperApplication record for {Email}: {StatusCode} - {Body}", _logger.LogWarning("Failed to create DeveloperApplication record for {Email}: {StatusCode} - {Body}",
application.Email, appResponse.StatusCode, appError); effectiveEmail, appResponse.StatusCode, appError);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning(ex, "Failed to create DeveloperApplication record for {Email} — user and ticket were created successfully", _logger.LogWarning(ex, "Failed to create DeveloperApplication record for {Email} — user and ticket were created successfully",
application.Email); effectiveEmail);
} }
_logger.LogInformation("Developer application submitted for {Email} as {Role} — user registered and ticket created", _logger.LogInformation("Developer application submitted for {Email} as {Role} — user registered and ticket created",
application.Email, application.Role); effectiveEmail, application.Role);
return (true, "Application submitted successfully! Your SilverDESK account has been created.", token); return (true, "Application submitted successfully! Your SilverDESK account has been created.", token);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error submitting developer application for {Email}", application.Email); _logger.LogError(ex, "Error submitting developer application for {Username}", application.DesiredUsername);
return (false, "Unable to connect to the application service. Please try again later.", null); return (false, "Unable to connect to the application service. Please try again later.", null);
} }
} }
@@ -177,12 +182,16 @@ public class DeveloperApplicationService
private static string FormatTicketBody(DeveloperApplication app) private static string FormatTicketBody(DeveloperApplication app)
{ {
var effectiveEmail = string.IsNullOrWhiteSpace(app.Email)
? $"{app.DesiredUsername}@silverlabs.uk"
: app.Email.Trim();
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.AppendLine("## Developer Program Application"); sb.AppendLine("## Developer Program Application");
sb.AppendLine(); sb.AppendLine();
sb.AppendLine($"**Role:** {app.Role}"); sb.AppendLine($"**Role:** {app.Role}");
sb.AppendLine($"**Full Name:** {app.FullName}"); sb.AppendLine($"**Full Name:** {app.FullName}");
sb.AppendLine($"**Email:** {app.Email}"); sb.AppendLine($"**Email:** {effectiveEmail}");
sb.AppendLine($"**Desired Username:** {app.DesiredUsername}"); sb.AppendLine($"**Desired Username:** {app.DesiredUsername}");
sb.AppendLine($"**Timezone:** {app.Timezone}"); sb.AppendLine($"**Timezone:** {app.Timezone}");
sb.AppendLine(); sb.AppendLine();