feat: complete phase 3 career workspace
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace JobTrackerApi.Services;
|
||||
|
||||
public static class AvatarStorage
|
||||
{
|
||||
private const string FilePrefix = "file:";
|
||||
|
||||
public static async Task<string> StoreAsync(string dataRoot, string userId, byte[] bytes, string contentType, CancellationToken cancellationToken)
|
||||
{
|
||||
var userKey = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(userId))).ToLowerInvariant();
|
||||
var folder = Path.Combine(dataRoot, "Avatars", userKey);
|
||||
Directory.CreateDirectory(folder);
|
||||
var extension = contentType switch { "image/png" => ".png", "image/webp" => ".webp", _ => ".jpg" };
|
||||
var path = Path.Combine(folder, "avatar" + extension);
|
||||
foreach (var old in Directory.EnumerateFiles(folder, "avatar.*")) if (!string.Equals(old, path, StringComparison.OrdinalIgnoreCase)) File.Delete(old);
|
||||
await File.WriteAllBytesAsync(path, bytes, cancellationToken);
|
||||
return FilePrefix + path;
|
||||
}
|
||||
|
||||
public static string? Resolve(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value) || !value.StartsWith(FilePrefix, StringComparison.Ordinal)) return value;
|
||||
var path = value[FilePrefix.Length..];
|
||||
if (!File.Exists(path)) return null;
|
||||
var contentType = Path.GetExtension(path).ToLowerInvariant() switch { ".png" => "image/png", ".webp" => "image/webp", _ => "image/jpeg" };
|
||||
return $"data:{contentType};base64,{Convert.ToBase64String(File.ReadAllBytes(path))}";
|
||||
}
|
||||
|
||||
public static void Delete(string? value)
|
||||
{
|
||||
if (value?.StartsWith(FilePrefix, StringComparison.Ordinal) != true) return;
|
||||
var path = value[FilePrefix.Length..];
|
||||
if (File.Exists(path)) File.Delete(path);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,10 @@ public static class CareerProfileMapper
|
||||
public StructuredCvContact Contact { get; set; } = new();
|
||||
public List<string> Summary { get; set; } = new();
|
||||
public List<string> Interests { get; set; } = new();
|
||||
public List<string> Awards { get; set; } = new();
|
||||
public List<string> Publications { get; set; } = new();
|
||||
public List<string> Organisations { get; set; } = new();
|
||||
public List<string> References { get; set; } = new();
|
||||
public List<StructuredCvOtherSection> OtherSections { get; set; } = new();
|
||||
public List<StructuredCvSection> Sections { get; set; } = new();
|
||||
}
|
||||
@@ -35,6 +39,10 @@ public static class CareerProfileMapper
|
||||
Contact = p.Contact,
|
||||
Summary = p.Summary,
|
||||
Interests = p.Interests,
|
||||
Awards = p.Awards,
|
||||
Publications = p.Publications,
|
||||
Organisations = p.Organisations,
|
||||
References = p.References,
|
||||
OtherSections = p.OtherSections,
|
||||
Sections = p.Sections,
|
||||
}, JsonOptions);
|
||||
@@ -125,6 +133,10 @@ public static class CareerProfileMapper
|
||||
Contact = tail.Contact,
|
||||
Summary = tail.Summary,
|
||||
Interests = tail.Interests,
|
||||
Awards = tail.Awards,
|
||||
Publications = tail.Publications,
|
||||
Organisations = tail.Organisations,
|
||||
References = tail.References,
|
||||
OtherSections = tail.OtherSections,
|
||||
Sections = tail.Sections,
|
||||
Jobs = experiences.Select(x => new StructuredCvJob
|
||||
|
||||
@@ -71,6 +71,10 @@ public sealed class CvProfileDiffService : ICvProfileDiffService
|
||||
DiffLanguages(current.Languages, extracted.Languages),
|
||||
DiffScalars("Skills", current.Skills, extracted.Skills),
|
||||
DiffScalars("Interests", current.Interests, extracted.Interests),
|
||||
DiffScalars("Awards", current.Awards, extracted.Awards),
|
||||
DiffScalars("Publications", current.Publications, extracted.Publications),
|
||||
DiffScalars("Organisations", current.Organisations, extracted.Organisations),
|
||||
DiffScalars("References", current.References, extracted.References),
|
||||
}.Where(c => c is not null).Select(c => c!).ToList(),
|
||||
};
|
||||
}
|
||||
@@ -89,6 +93,10 @@ public sealed class CvProfileDiffService : ICvProfileDiffService
|
||||
MergeLanguages(merged.Languages, extracted.Languages);
|
||||
AppendUnique(merged.Skills, extracted.Skills);
|
||||
AppendUnique(merged.Interests, extracted.Interests);
|
||||
AppendUnique(merged.Awards, extracted.Awards);
|
||||
AppendUnique(merged.Publications, extracted.Publications);
|
||||
AppendUnique(merged.Organisations, extracted.Organisations);
|
||||
AppendUnique(merged.References, extracted.References);
|
||||
MergeList(merged.OtherSections, extracted.OtherSections, x => Norm(x.Title), (a, b) => AppendUnique(a.Items, b.Items));
|
||||
if (extracted.Sections.Count > 0) merged.Sections = extracted.Sections;
|
||||
foreach (var field in extracted.Metadata.Fields) merged.Metadata.Fields[field.Key] = field.Value;
|
||||
|
||||
@@ -79,6 +79,10 @@ public static class CvVariantResolver
|
||||
["certifications"] = CertificationSection(profile, settings),
|
||||
["languages"] = LanguageSection(profile),
|
||||
["interests"] = TagSection("interests", "Interests", profile.Interests),
|
||||
["awards"] = BulletSection("awards", "Awards", profile.Awards),
|
||||
["publications"] = BulletSection("publications", "Publications", profile.Publications),
|
||||
["organisations"] = BulletSection("organisations", "Organisations", profile.Organisations),
|
||||
["references"] = BulletSection("references", "References", profile.References),
|
||||
};
|
||||
|
||||
// OtherSections from the master profile become body sections keyed other:<n>.
|
||||
|
||||
Reference in New Issue
Block a user