Files
Website/BlazorApp/Components/Pages/Developers.razor
SysAdmin 33b21959d8
All checks were successful
Build and Deploy / deploy (push) Successful in 40s
fix(developers): distinguish API errors from taken usernames in availability check
CheckUsernameAsync returned false (taken) on any API failure, making every
username appear taken when SilverDESK was unreachable. Now returns nullable
bool so errors show a warning instead of blocking submission.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:00:46 +00:00

330 lines
16 KiB
Plaintext

@page "/developers"
@using SilverLabs.Website.Models
@using SilverLabs.Website.Services
@inject DeveloperApplicationService ApplicationService
@rendermode InteractiveServer
<PageTitle>Join the Team - SilverLabs</PageTitle>
<div class="main-content visible">
<header class="header">
<img src="logo.png" alt="SilverLabs Logo" class="logo">
</header>
<div class="dev-container">
<div class="dev-header">
<h1>Join the SilverLabs Team</h1>
<p class="dev-subtitle">Help us build privacy-first infrastructure. Whether you test our products or write code, there's a place for you.</p>
</div>
@if (_submitted)
{
<div class="dev-success-panel">
<div class="success-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
</div>
<h2>Application Submitted</h2>
<p>@_resultMessage</p>
<p class="dev-account-note">Your SilverDESK account has been created. You can log in with the credentials you just chose.</p>
<div class="dev-success-actions">
<a href="https://silverdesk.silverlabs.uk" target="_blank" class="dev-btn dev-btn-primary">Track Your Application on SilverDESK</a>
<a href="/" class="dev-btn dev-btn-secondary">Back to Home</a>
</div>
</div>
}
else
{
<EditForm Model="_application" OnValidSubmit="HandleSubmit" FormName="developer-application">
<DataAnnotationsValidator />
<!-- Role Selector -->
<div class="dev-section">
<h2 class="dev-section-title">Choose Your Role</h2>
<div class="role-selector">
<div class="role-card @(_application.Role == ApplicationRole.Tester ? "role-active" : "")"
@onclick="() => SelectRole(ApplicationRole.Tester)">
<div class="role-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
</div>
<h3>Product Tester</h3>
<p>Test our apps across devices, find bugs, and provide feedback that shapes our products.</p>
</div>
<div class="role-card @(_application.Role == ApplicationRole.Developer ? "role-active" : "")"
@onclick="() => SelectRole(ApplicationRole.Developer)">
<div class="role-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="16 18 22 12 16 6"></polyline>
<polyline points="8 6 2 12 8 18"></polyline>
</svg>
</div>
<h3>Developer</h3>
<p>Contribute code, build modules, and help architect privacy-first solutions.</p>
</div>
</div>
<ValidationMessage For="() => _application.Role" />
</div>
<!-- Personal Details -->
<div class="dev-section">
<h2 class="dev-section-title">About You</h2>
<div class="form-grid">
<div class="form-group">
<label for="fullName">Full Name</label>
<InputText id="fullName" @bind-Value="_application.FullName" class="form-input" placeholder="Jane Doe" />
<ValidationMessage For="() => _application.FullName" />
</div>
<div class="form-group">
<label for="email">Email Address</label>
<InputText id="email" @bind-Value="_application.Email" class="form-input" placeholder="jane@example.com" />
<ValidationMessage For="() => _application.Email" />
</div>
<div class="form-group">
<label for="username">Desired Username</label>
<InputText id="username" @bind-Value="_application.DesiredUsername" class="form-input" placeholder="janedoe"
@oninput="OnUsernameInput" />
<span class="form-hint">This will be your handle across SilverLabs services</span>
@if (_usernameCheckState == UsernameCheckState.Checking)
{
<span class="username-status username-checking">Checking availability...</span>
}
else if (_usernameCheckState == UsernameCheckState.Available)
{
<span class="username-status username-available">&#10003; Username is available</span>
}
else if (_usernameCheckState == UsernameCheckState.Taken)
{
<span class="username-status username-taken">&#10007; Username is already taken</span>
}
else if (_usernameCheckState == UsernameCheckState.Error)
{
<span class="username-status username-error">&#9888; Could not check availability — you can still submit</span>
}
<ValidationMessage For="() => _application.DesiredUsername" />
</div>
<div class="form-group">
<label for="timezone">Location / Timezone</label>
<InputText id="timezone" @bind-Value="_application.Timezone" class="form-input" placeholder="e.g. Europe/London, US/Eastern" />
<ValidationMessage For="() => _application.Timezone" />
</div>
</div>
</div>
<!-- Password -->
<div class="dev-section">
<h2 class="dev-section-title">Create Your Password</h2>
<p class="dev-section-desc">This will be your password for SilverDESK and associated services.</p>
<div class="form-grid">
<div class="form-group">
<label for="password">Password</label>
<InputText id="password" type="password" @bind-Value="_application.Password" class="form-input" placeholder="Min. 8 characters" />
<span class="form-hint">Must include uppercase, lowercase, and a number</span>
<ValidationMessage For="() => _application.Password" />
</div>
<div class="form-group">
<label for="confirmPassword">Confirm Password</label>
<InputText id="confirmPassword" type="password" @bind-Value="_application.ConfirmPassword" class="form-input" placeholder="Re-enter your password" />
<ValidationMessage For="() => _application.ConfirmPassword" />
</div>
</div>
</div>
<!-- Platforms -->
<div class="dev-section">
<h2 class="dev-section-title">Devices & Platforms</h2>
<p class="dev-section-desc">Which platforms do you use or have access to?</p>
<div class="platform-grid">
@foreach (var platform in _availablePlatforms)
{
var isChecked = _application.Platforms.Contains(platform);
<label class="platform-chip @(isChecked ? "platform-active" : "")">
<input type="checkbox" checked="@isChecked"
@onchange="() => TogglePlatform(platform)" />
<span>@platform</span>
</label>
}
</div>
<ValidationMessage For="() => _application.Platforms" />
</div>
<!-- Developer-only: Skills -->
@if (_application.Role == ApplicationRole.Developer)
{
<div class="dev-section dev-section-fade-in">
<h2 class="dev-section-title">Skills & Experience</h2>
<p class="dev-section-desc">Tell us about your technical background — languages, frameworks, and any open-source contributions.</p>
<div class="form-group">
<InputTextArea id="skills" @bind-Value="_application.Skills" class="form-input form-textarea"
placeholder="e.g. C#/.NET 5 years, Blazor, PostgreSQL, Docker, contributed to..." rows="5" />
</div>
</div>
}
<!-- Motivation -->
<div class="dev-section">
<h2 class="dev-section-title">Why SilverLabs?</h2>
<p class="dev-section-desc">What draws you to privacy-first development? What do you hope to contribute?</p>
<div class="form-group">
<InputTextArea id="motivation" @bind-Value="_application.Motivation" class="form-input form-textarea"
placeholder="Tell us what motivates you..." rows="5" />
<ValidationMessage For="() => _application.Motivation" />
</div>
</div>
<!-- What You Get -->
<div class="dev-section dev-perks">
<h2 class="dev-section-title">What You'll Get</h2>
<div class="perks-grid">
<div class="perk-item">
<strong>@@@("username")@@silverlabs.uk</strong>
<span>Your own SilverLabs email</span>
</div>
<div class="perk-item">
<strong>SilverDESK</strong>
<span>Project management & issue tracking</span>
</div>
<div class="perk-item">
<strong>Mattermost</strong>
<span>Team chat & collaboration</span>
</div>
<div class="perk-item">
<strong>Gitea Access</strong>
<span>Source code repositories</span>
</div>
</div>
</div>
<!-- Submit -->
<div class="dev-submit-area">
@if (!string.IsNullOrEmpty(_errorMessage))
{
<div class="dev-error">@_errorMessage</div>
}
<button type="submit" class="dev-btn dev-btn-primary" disabled="@(_submitting || _usernameCheckState == UsernameCheckState.Taken)">
@if (_submitting)
{
<span class="btn-spinner"></span>
<span>Submitting...</span>
}
else
{
<span>Submit Application</span>
}
</button>
</div>
</EditForm>
}
<a href="/" class="back-link">← Back to SilverLabs Home</a>
</div>
</div>
@code {
private DeveloperApplication _application = new() { Role = ApplicationRole.Tester };
private bool _submitting;
private bool _submitted;
private string? _resultMessage;
private string? _errorMessage;
private UsernameCheckState _usernameCheckState = UsernameCheckState.None;
private CancellationTokenSource? _usernameCheckCts;
private readonly string[] _availablePlatforms = { "Windows", "macOS", "Linux", "Android", "iOS", "Other" };
private enum UsernameCheckState { None, Checking, Available, Taken, Error }
private void SelectRole(ApplicationRole role)
{
_application.Role = role;
}
private void TogglePlatform(string platform)
{
if (_application.Platforms.Contains(platform))
_application.Platforms.Remove(platform);
else
_application.Platforms.Add(platform);
}
private async Task OnUsernameInput(ChangeEventArgs e)
{
var username = e.Value?.ToString() ?? "";
_application.DesiredUsername = username;
_usernameCheckCts?.Cancel();
if (username.Length < 3)
{
_usernameCheckState = UsernameCheckState.None;
return;
}
_usernameCheckState = UsernameCheckState.Checking;
_usernameCheckCts = new CancellationTokenSource();
var token = _usernameCheckCts.Token;
try
{
await Task.Delay(500, token);
var available = await ApplicationService.CheckUsernameAsync(username);
if (!token.IsCancellationRequested)
{
_usernameCheckState = available switch
{
true => UsernameCheckState.Available,
false => UsernameCheckState.Taken,
null => UsernameCheckState.Error
};
StateHasChanged();
}
}
catch (TaskCanceledException)
{
// Debounce cancelled — newer keystroke took over
}
}
private async Task HandleSubmit()
{
_errorMessage = null;
_submitting = true;
try
{
var (success, message, token) = await ApplicationService.SubmitApplicationAsync(_application);
if (success)
{
_resultMessage = message;
_submitted = true;
}
else
{
_errorMessage = message;
}
}
catch
{
_errorMessage = "An unexpected error occurred. Please try again later.";
}
finally
{
_submitting = false;
}
}
}