app update
All checks were successful
Build and Deploy / deploy (push) Successful in 42s

This commit is contained in:
2026-02-24 14:48:02 +00:00
parent cd2994d7eb
commit 502d48da99
5 changed files with 361 additions and 33 deletions

View File

@@ -172,30 +172,101 @@
<ValidationMessage For="() => _application.Platforms" />
</div>
<!-- Skills & What You Bring -->
<div class="dev-section">
<h2 class="dev-section-title">What You Bring</h2>
<p class="dev-section-desc">Tell us about your skills and what you'd bring to the team.</p>
<!-- 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>
@if (_application.Role == ApplicationRole.Developer)
{
<div class="form-group" style="margin-bottom: 1.25rem;">
<label for="skills">Technical Skills</label>
<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="4" />
<span class="form-hint">Languages, frameworks, tools, and any open-source contributions</span>
<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">
<label for="motivation">How will you contribute?</label>
<InputTextArea id="motivation" @bind-Value="_application.Motivation" class="form-input form-textarea"
placeholder="@(_application.Role == ApplicationRole.Developer
? "What areas interest you? Architecture, frontend, backend, DevOps, security..."
: "What devices/platforms can you test on? What kind of testing experience do you have?")" rows="4" />
<ValidationMessage For="() => _application.Motivation" />
<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>
</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">
@@ -251,6 +322,8 @@
private bool _submitted;
private string? _resultMessage;
private string? _errorMessage;
private int _internetHover;
private int _testingHover;
private UsernameCheckState _usernameCheckState = UsernameCheckState.None;
private string? _usernameFormatError;
@@ -329,6 +402,14 @@
_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() ?? "";

View File

@@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations;
namespace SilverLabs.Website.Models;
public class DeveloperApplication
public class DeveloperApplication : IValidatableObject
{
[Required(ErrorMessage = "Full name is required")]
[StringLength(100, MinimumLength = 2)]
@@ -25,8 +25,6 @@ public class DeveloperApplication
[MinLength(1, ErrorMessage = "Please select at least one platform")]
public List<string> Platforms { get; set; } = new();
public string? Skills { get; set; }
[Required(ErrorMessage = "Password is required")]
[StringLength(100, MinimumLength = 8, ErrorMessage = "Password must be at least 8 characters")]
public string Password { get; set; } = string.Empty;
@@ -35,9 +33,36 @@ public class DeveloperApplication
[Compare("Password", ErrorMessage = "Passwords do not match")]
public string ConfirmPassword { get; set; } = string.Empty;
[Required(ErrorMessage = "Please tell us how you'll contribute")]
[StringLength(2000, MinimumLength = 20, ErrorMessage = "Please write at least 20 characters")]
public string Motivation { get; set; } = string.Empty;
// Tester-specific
public int? InternetUnderstanding { get; set; }
public int? EnjoysTesting { get; set; }
// Developer-specific
public string? ExperienceRange { get; set; }
public List<string> SelectedSkills { get; set; } = new();
// Shared optional
public string? AdditionalNotes { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Role == ApplicationRole.Tester)
{
if (!InternetUnderstanding.HasValue || InternetUnderstanding < 1 || InternetUnderstanding > 5)
yield return new ValidationResult("Please rate your internet understanding", new[] { nameof(InternetUnderstanding) });
if (!EnjoysTesting.HasValue || EnjoysTesting < 1 || EnjoysTesting > 5)
yield return new ValidationResult("Please rate your enthusiasm for testing", new[] { nameof(EnjoysTesting) });
}
else if (Role == ApplicationRole.Developer)
{
if (string.IsNullOrWhiteSpace(ExperienceRange))
yield return new ValidationResult("Please select your experience level", new[] { nameof(ExperienceRange) });
if (SelectedSkills.Count == 0)
yield return new ValidationResult("Please select at least one skill", new[] { nameof(SelectedSkills) });
}
}
}
public enum ApplicationRole

View File

@@ -0,0 +1,37 @@
namespace SilverLabs.Website.Models;
public static class SkillCatalog
{
public static readonly string[] ExperienceRanges =
{
"< 1 year",
"1-3 years",
"3-5 years",
"5-10 years",
"10+ years"
};
public static readonly Dictionary<string, string[]> SkillCategories = new()
{
["Languages"] = new[]
{
"C#", "Python", "JavaScript", "TypeScript", "Go", "Rust",
"Java", "C/C++", "PHP", "Ruby", "Swift", "Kotlin"
},
["Frameworks"] = new[]
{
".NET/Blazor", "React", "Angular", "Vue", "Django",
"Node.js", "Next.js", "Svelte", "Spring Boot", "Flask"
},
["Infrastructure"] = new[]
{
"Docker", "Kubernetes", "Linux", "Nginx", "Terraform",
"CI/CD", "AWS", "Azure", "Proxmox"
},
["Databases"] = new[]
{
"PostgreSQL", "MySQL", "SQLite", "MongoDB", "Redis",
"SQL Server", "Elasticsearch"
}
};
}

View File

@@ -121,8 +121,8 @@ public class DeveloperApplicationService
timezone = application.Timezone,
appliedRole = application.Role.ToString(),
platforms = application.Platforms,
skills = application.Skills ?? "",
motivation = application.Motivation,
skills = SerializeAssessment(application),
motivation = GenerateMotivationSummary(application),
status = 0, // Pending
silverDeskProvisioned = true
};
@@ -158,6 +158,64 @@ public class DeveloperApplicationService
}
}
/// <summary>
/// Serializes structured assessment data as JSON for the Skills column.
/// </summary>
internal static string SerializeAssessment(DeveloperApplication app)
{
object data;
if (app.Role == ApplicationRole.Tester)
{
data = new
{
type = "tester",
internetUnderstanding = app.InternetUnderstanding ?? 0,
enjoysTesting = app.EnjoysTesting ?? 0,
additionalNotes = app.AdditionalNotes ?? ""
};
}
else
{
data = new
{
type = "developer",
experienceRange = app.ExperienceRange ?? "",
selectedSkills = app.SelectedSkills,
additionalNotes = app.AdditionalNotes ?? ""
};
}
return JsonSerializer.Serialize(data, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
}
/// <summary>
/// Generates a human-readable summary for the Motivation field (backward compatibility).
/// </summary>
internal static string GenerateMotivationSummary(DeveloperApplication app)
{
if (app.Role == ApplicationRole.Tester)
{
var summary = $"Internet understanding: {app.InternetUnderstanding}/5, Testing enthusiasm: {app.EnjoysTesting}/5";
if (!string.IsNullOrWhiteSpace(app.AdditionalNotes))
summary += $". Notes: {app.AdditionalNotes.Trim()}";
return summary;
}
else
{
var skills = app.SelectedSkills.Count > 0
? string.Join(", ", app.SelectedSkills)
: "None selected";
var summary = $"{app.ExperienceRange} experience. Skills: {skills}";
if (!string.IsNullOrWhiteSpace(app.AdditionalNotes))
summary += $". Notes: {app.AdditionalNotes.Trim()}";
return summary;
}
}
private static string ParseRegistrationError(string errorBody)
{
try
@@ -198,15 +256,29 @@ public class DeveloperApplicationService
sb.AppendLine($"**Platforms:** {string.Join(", ", app.Platforms)}");
sb.AppendLine();
if (app.Role == ApplicationRole.Developer && !string.IsNullOrWhiteSpace(app.Skills))
if (app.Role == ApplicationRole.Tester)
{
sb.AppendLine("**Skills & Experience:**");
sb.AppendLine(app.Skills);
sb.AppendLine("### Assessment");
sb.AppendLine($"- Internet understanding: {"*".PadLeft(app.InternetUnderstanding ?? 0, '*')}{new string('-', 5 - (app.InternetUnderstanding ?? 0))} ({app.InternetUnderstanding}/5)");
sb.AppendLine($"- Testing enthusiasm: {"*".PadLeft(app.EnjoysTesting ?? 0, '*')}{new string('-', 5 - (app.EnjoysTesting ?? 0))} ({app.EnjoysTesting}/5)");
}
else
{
sb.AppendLine("### Skills & Experience");
sb.AppendLine($"**Experience:** {app.ExperienceRange}");
sb.AppendLine();
if (app.SelectedSkills.Count > 0)
{
sb.AppendLine($"**Technologies:** {string.Join(", ", app.SelectedSkills)}");
}
}
sb.AppendLine("**Motivation:**");
sb.AppendLine(app.Motivation);
if (!string.IsNullOrWhiteSpace(app.AdditionalNotes))
{
sb.AppendLine();
sb.AppendLine("### Additional Notes");
sb.AppendLine(app.AdditionalNotes.Trim());
}
return sb.ToString();
}

View File

@@ -412,6 +412,119 @@
color: #00B8D4;
}
/* Star Rating */
.star-rating {
display: flex;
gap: 0.35rem;
margin-top: 0.4rem;
}
.star-rating-star {
font-size: 1.8rem;
cursor: pointer;
color: rgba(255, 255, 255, 0.2);
transition: color 0.15s ease, transform 0.15s ease;
user-select: none;
line-height: 1;
}
.star-rating-star:hover {
transform: scale(1.15);
}
.star-rating-star.star-filled {
color: #4DD0E1;
text-shadow: 0 0 8px rgba(77, 208, 225, 0.4);
}
.rating-labels {
display: flex;
justify-content: space-between;
margin-top: 0.3rem;
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.35);
max-width: 170px;
}
/* Experience Selector */
.experience-selector {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.4rem;
}
.exp-btn {
padding: 0.45rem 1.1rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 100px;
color: rgba(255, 255, 255, 0.7);
font-size: 0.88rem;
font-family: inherit;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
}
.exp-btn:hover {
border-color: rgba(77, 208, 225, 0.4);
background: rgba(77, 208, 225, 0.06);
}
.exp-btn.exp-active {
background: rgba(77, 208, 225, 0.12);
border-color: #4DD0E1;
color: #4DD0E1;
box-shadow: 0 0 12px rgba(77, 208, 225, 0.15);
}
/* Skill Bubbles */
.skill-category-label {
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: rgba(255, 255, 255, 0.4);
margin-top: 1rem;
margin-bottom: 0.4rem;
}
.skill-category-label:first-of-type {
margin-top: 0.5rem;
}
.skill-bubbles {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.skill-bubble {
padding: 0.35rem 0.85rem;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 100px;
color: rgba(255, 255, 255, 0.6);
font-size: 0.82rem;
font-family: inherit;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
}
.skill-bubble:hover {
border-color: rgba(77, 208, 225, 0.35);
background: rgba(77, 208, 225, 0.05);
color: rgba(255, 255, 255, 0.8);
}
.skill-bubble.skill-active {
background: rgba(77, 208, 225, 0.12);
border-color: #4DD0E1;
color: #4DD0E1;
}
/* Responsive */
@media (max-width: 768px) {
.dev-header h1 {