This commit is contained in:
@@ -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>
|
||||
|
||||
@if (_application.Role == ApplicationRole.Developer)
|
||||
<!-- Role-Specific Assessment -->
|
||||
@if (_application.Role == ApplicationRole.Tester)
|
||||
{
|
||||
<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>
|
||||
<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="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" />
|
||||
<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">
|
||||
@@ -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() ?? "";
|
||||
|
||||
@@ -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
|
||||
|
||||
37
BlazorApp/Models/SkillCatalog.cs
Normal file
37
BlazorApp/Models/SkillCatalog.cs
Normal 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"
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user