Files
Website/BlazorApp/Components/Pages/Developers.razor
SysAdmin 502d48da99
All checks were successful
Build and Deploy / deploy (push) Successful in 42s
app update
2026-02-24 14:48:02 +00:00

493 lines
25 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@page "/developers"
@using SilverLabs.Website.Models
@using SilverLabs.Website.Services
@inject DeveloperApplicationService ApplicationService
@inject IJSRuntime JS
@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. Log in with the password you just chose to track your application.</p>
<div class="dev-success-actions">
<a href="https://silverdesk.silverlabs.uk/login?username=@(Uri.EscapeDataString(_application.DesiredUsername ?? ""))" target="_blank" class="dev-btn dev-btn-primary">Log in to 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 (optional)</label>
<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" />
</div>
<div class="form-group">
<label for="username">Desired Username</label>
<input id="username" class="form-input" placeholder="janedoe" value="@_application.DesiredUsername"
@oninput="OnUsernameInput" @onfocusout="OnUsernameBlur" autocomplete="off" />
<span class="form-hint">330 characters: letters, numbers, hyphens and underscores</span>
@if (!string.IsNullOrEmpty(_usernameFormatError))
{
<span class="username-status username-format-error">@_usernameFormatError</span>
}
else 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">Timezone</label>
<InputSelect id="timezone" @bind-Value="_application.Timezone" class="form-input">
<option value="">Select your timezone...</option>
@foreach (var tz in _timezones)
{
<option value="@tz.Id">@tz.Label</option>
}
</InputSelect>
<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>
<!-- Role-Specific Assessment -->
@if (_application.Role == ApplicationRole.Tester)
{
<div class="dev-section">
<h2 class="dev-section-title">About Your Experience</h2>
<p class="dev-section-desc">Help us understand your background — there are no wrong answers.</p>
<div class="form-group" style="margin-bottom: 1.5rem;">
<label>How well do you understand the internet and online services?</label>
<div class="star-rating">
@for (int i = 1; i <= 5; i++)
{
var rating = i;
<span class="star-rating-star @(rating <= (_internetHover > 0 ? _internetHover : (_application.InternetUnderstanding ?? 0)) ? "star-filled" : "")"
@onclick="() => _application.InternetUnderstanding = rating"
@onmouseover="() => _internetHover = rating"
@onmouseout="() => _internetHover = 0">@(rating <= (_internetHover > 0 ? _internetHover : (_application.InternetUnderstanding ?? 0)) ? "\u2605" : "\u2606")</span>
}
</div>
<div class="rating-labels">
<span>Beginner</span>
<span>Expert</span>
</div>
<ValidationMessage For="() => _application.InternetUnderstanding" />
</div>
<div class="form-group" style="margin-bottom: 1.5rem;">
<label>How much do you enjoy trying new software and finding issues?</label>
<div class="star-rating">
@for (int i = 1; i <= 5; i++)
{
var rating = i;
<span class="star-rating-star @(rating <= (_testingHover > 0 ? _testingHover : (_application.EnjoysTesting ?? 0)) ? "star-filled" : "")"
@onclick="() => _application.EnjoysTesting = rating"
@onmouseover="() => _testingHover = rating"
@onmouseout="() => _testingHover = 0">@(rating <= (_testingHover > 0 ? _testingHover : (_application.EnjoysTesting ?? 0)) ? "\u2605" : "\u2606")</span>
}
</div>
<div class="rating-labels">
<span>Not really</span>
<span>Love it</span>
</div>
<ValidationMessage For="() => _application.EnjoysTesting" />
</div>
<div class="form-group">
<label for="additionalNotes">Anything else you'd like us to know? (optional)</label>
<InputTextArea id="additionalNotes" @bind-Value="_application.AdditionalNotes" class="form-input form-textarea"
placeholder="Previous testing experience, specific interests, etc." rows="3" />
</div>
</div>
}
else
{
<div class="dev-section">
<h2 class="dev-section-title">Your Skills</h2>
<p class="dev-section-desc">Select your experience level and the technologies you work with.</p>
<div class="form-group" style="margin-bottom: 1.5rem;">
<label>Experience Level</label>
<div class="experience-selector">
@foreach (var range in SkillCatalog.ExperienceRanges)
{
<button type="button"
class="exp-btn @(_application.ExperienceRange == range ? "exp-active" : "")"
@onclick="() => _application.ExperienceRange = range">@range</button>
}
</div>
<ValidationMessage For="() => _application.ExperienceRange" />
</div>
<div class="form-group" style="margin-bottom: 1.5rem;">
<label>Technologies (select all that apply)</label>
@foreach (var category in SkillCatalog.SkillCategories)
{
<div class="skill-category-label">@category.Key</div>
<div class="skill-bubbles">
@foreach (var skill in category.Value)
{
<button type="button"
class="skill-bubble @(_application.SelectedSkills.Contains(skill) ? "skill-active" : "")"
@onclick="() => ToggleSkill(skill)">@skill</button>
}
</div>
}
<ValidationMessage For="() => _application.SelectedSkills" />
</div>
<div class="form-group">
<label for="additionalNotes">Anything not listed above? (optional)</label>
<InputTextArea id="additionalNotes" @bind-Value="_application.AdditionalNotes" class="form-input form-textarea"
placeholder="Other skills, open-source contributions, areas of interest..." rows="3" />
</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="@IsSubmitDisabled">
@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 int _internetHover;
private int _testingHover;
private UsernameCheckState _usernameCheckState = UsernameCheckState.None;
private string? _usernameFormatError;
private string? _lastCheckedUsername;
private readonly string[] _availablePlatforms = { "Windows", "macOS", "Linux", "Android", "iOS", "Other" };
private static readonly List<(string Id, string Label)> _timezones = new()
{
("Pacific/Midway", "(UTC-11:00) Midway Island"),
("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 =
new(@"^[a-zA-Z0-9_-]{3,30}$", System.Text.RegularExpressions.RegexOptions.Compiled);
private enum UsernameCheckState { None, Checking, Available, Taken, Error }
private bool IsSubmitDisabled =>
_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)
{
_application.Role = role;
}
private void TogglePlatform(string platform)
{
if (_application.Platforms.Contains(platform))
_application.Platforms.Remove(platform);
else
_application.Platforms.Add(platform);
}
private void ToggleSkill(string skill)
{
if (_application.SelectedSkills.Contains(skill))
_application.SelectedSkills.Remove(skill);
else
_application.SelectedSkills.Add(skill);
}
private void OnUsernameInput(ChangeEventArgs e)
{
var username = e.Value?.ToString() ?? "";
_application.DesiredUsername = username;
// Reset API check state while typing — we'll check on blur
_usernameCheckState = UsernameCheckState.None;
_usernameFormatError = null;
// Show inline format feedback as they type
if (username.Length > 0 && username.Length < 3)
{
_usernameFormatError = "Username must be at least 3 characters";
}
else if (username.Length > 30)
{
_usernameFormatError = "Username must be 30 characters or fewer";
}
else if (username.Length >= 3 && !UsernamePattern.IsMatch(username))
{
_usernameFormatError = "Only letters, numbers, hyphens and underscores allowed";
}
}
private async Task OnUsernameBlur()
{
var username = _application.DesiredUsername?.Trim() ?? "";
// Don't check if empty, invalid format, or already checked this exact username
if (string.IsNullOrEmpty(username) || !UsernamePattern.IsMatch(username))
return;
if (username == _lastCheckedUsername && _usernameCheckState != UsernameCheckState.Error)
return;
_usernameCheckState = UsernameCheckState.Checking;
StateHasChanged();
var available = await ApplicationService.CheckUsernameAsync(username);
_lastCheckedUsername = username;
_usernameCheckState = available switch
{
true => UsernameCheckState.Available,
false => UsernameCheckState.Taken,
null => UsernameCheckState.Error
};
StateHasChanged();
}
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;
}
}
}