From 90fdd8e1a5bc8c374cd1ea405f5216603f252df0 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Mon, 23 Mar 2026 19:49:41 +0100 Subject: [PATCH] Track cleanup progress and polish profile/system flows --- JobTrackerApi/Controllers/AuthController.cs | 6 +- .../Controllers/JobApplicationsController.cs | 38 ++++++++-- .../Controllers/ProfileCvController.cs | 7 +- JobTrackerApi/Controllers/UsersController.cs | 6 +- JobTrackerApi/Services/SmtpEmailSender.cs | 2 +- docs/jobbjakt-cleanup-tracker.md | 70 +++++++++++++++++++ .../src/components/AuthStatusCard.tsx | 2 +- .../src/components/Correspondence.tsx | 31 ++++---- .../src/components/GoogleAuthCard.tsx | 3 +- .../src/components/JobDetailsDialog.tsx | 16 ++--- job-tracker-ui/src/i18n/translations.ts | 8 +-- job-tracker-ui/src/pages/AdminSystemPage.tsx | 8 +-- job-tracker-ui/src/pages/ProfilePage.tsx | 2 +- job-tracker-ui/src/theme.ts | 6 ++ 14 files changed, 156 insertions(+), 49 deletions(-) create mode 100644 docs/jobbjakt-cleanup-tracker.md diff --git a/JobTrackerApi/Controllers/AuthController.cs b/JobTrackerApi/Controllers/AuthController.cs index fba42d9..6abf564 100644 --- a/JobTrackerApi/Controllers/AuthController.cs +++ b/JobTrackerApi/Controllers/AuthController.cs @@ -133,7 +133,7 @@ public sealed class AuthController : ControllerBase if (user is null) { - return Unauthorized("This Google account is not linked to a Job Tracker user yet."); + return Unauthorized("This Google account is not linked to a Jobbjakt user yet."); } if (string.IsNullOrWhiteSpace(user.GoogleSubject) || !string.Equals(user.GoogleSubject, google.Subject, StringComparison.Ordinal)) @@ -237,7 +237,7 @@ public sealed class AuthController : ControllerBase .FirstOrDefaultAsync(x => x.GoogleSubject == google.Subject || (!string.IsNullOrWhiteSpace(google.Email) && x.GoogleEmail == google.Email), cancellationToken); if (conflict is not null) { - return Conflict("That Google account is already linked to another Job Tracker user."); + return Conflict("That Google account is already linked to another Jobbjakt user."); } user.GoogleSubject = google.Subject; @@ -393,7 +393,7 @@ public sealed class AuthController : ControllerBase await _email.SendAsync( user.Email, "Password reset", - $"You requested a password reset for Job Tracker.\n\nReset link:\n{link}\n\nIf you did not request this, you can ignore this email.", + $"You requested a password reset for Jobbjakt.\n\nReset link:\n{link}\n\nIf you did not request this, you can ignore this email.", cancellationToken ); diff --git a/JobTrackerApi/Controllers/JobApplicationsController.cs b/JobTrackerApi/Controllers/JobApplicationsController.cs index 540cf1d..97c6bb2 100644 --- a/JobTrackerApi/Controllers/JobApplicationsController.cs +++ b/JobTrackerApi/Controllers/JobApplicationsController.cs @@ -6,6 +6,7 @@ using JobTrackerApi.Services; using JobTrackerApi.Services.JobImport; using System.Security.Claims; using System.Text.Json; +using Microsoft.AspNetCore.Identity; namespace JobTrackerApi.Controllers { @@ -16,17 +17,44 @@ namespace JobTrackerApi.Controllers private readonly JobTrackerContext _db; private readonly ISummarizerService _summarizer; private readonly IAppEmailSender _email; + private readonly UserManager _users; - public JobApplicationsController(JobTrackerContext db, ISummarizerService summarizer, IAppEmailSender email) + public JobApplicationsController(JobTrackerContext db, ISummarizerService summarizer, IAppEmailSender email, UserManager users) { _db = db; _summarizer = summarizer; _email = email; + _users = users; } private string? CurrentUserId => User?.FindFirstValue(ClaimTypes.NameIdentifier) ?? User?.FindFirstValue("sub"); + private async Task GetCurrentUserAsync(CancellationToken cancellationToken) + { + var userId = CurrentUserId; + if (string.IsNullOrWhiteSpace(userId)) return null; + return await _users.FindByIdAsync(userId); + } + + private static string GetPreferredDisplayName(ApplicationUser? user) + { + if (user is null) return "Your Name"; + if (!string.IsNullOrWhiteSpace(user.DisplayName)) return user.DisplayName.Trim(); + var fullName = string.Join(" ", new[] { user.FirstName?.Trim(), user.LastName?.Trim() }.Where(x => !string.IsNullOrWhiteSpace(x))); + if (!string.IsNullOrWhiteSpace(fullName)) return fullName; + if (!string.IsNullOrWhiteSpace(user.UserName)) return user.UserName.Trim(); + if (!string.IsNullOrWhiteSpace(user.Email)) return user.Email.Trim(); + return "Your Name"; + } + + private static string BuildGreeting(JobApplication job) + { + if (!string.IsNullOrWhiteSpace(job.Company?.RecruiterName)) return $"Hi {job.Company.RecruiterName.Trim()},"; + if (!string.IsNullOrWhiteSpace(job.Company?.Name)) return $"Hi {job.Company.Name.Trim()} team,"; + return "Hi there,"; + } + private static IEnumerable SplitTags(string? s) { if (string.IsNullOrWhiteSpace(s)) yield break; @@ -1747,18 +1775,20 @@ Candidate master CV: ? (job.FollowUpAt is not null && job.FollowUpAt.Value.Date <= DateTime.Today ? "Scheduled follow-up is due." : "No recent response has been logged.") : job.NextAction!; + var currentUser = await GetCurrentUserAsync(cancellationToken); + var signerName = GetPreferredDisplayName(currentUser); + var greeting = BuildGreeting(job); var subject = $"Following up on {job.JobTitle} application"; - var companyName = job.Company?.Name ?? "your team"; var reference = lastMessage?.Subject ?? job.JobTitle; var summary = job.ShortSummary; var body = string.Join("\n\n", new[] { - $"Hi {companyName},", + greeting, $"I wanted to follow up on my application for the {job.JobTitle} role. I'm still very interested in the opportunity and would love to hear if there are any updates on next steps.", !string.IsNullOrWhiteSpace(summary) ? $"Quick reminder of fit: {summary}" : null, $"Context: {reason}", $"If helpful, I can also provide any additional information related to {reference}.", - "Thanks for your time,\n[Your name]" + $"Thanks for your time,\n{signerName}" }.Where(x => !string.IsNullOrWhiteSpace(x))); return Ok(new FollowUpDraftDto(subject, body, reason, DateTime.Today)); diff --git a/JobTrackerApi/Controllers/ProfileCvController.cs b/JobTrackerApi/Controllers/ProfileCvController.cs index 6d2c161..674c755 100644 --- a/JobTrackerApi/Controllers/ProfileCvController.cs +++ b/JobTrackerApi/Controllers/ProfileCvController.cs @@ -20,7 +20,7 @@ public sealed class ProfileCvController : ControllerBase ".docx", }; - private const long MaxFileSizeBytes = 512 * 1024; + private const long MaxFileSizeBytes = 5 * 1024 * 1024; private readonly UserManager _users; @@ -36,7 +36,7 @@ public sealed class ProfileCvController : ControllerBase var user = await _users.GetUserAsync(User); if (user is null) return Unauthorized(); if (file is null || file.Length == 0) return BadRequest("Select a CV file to upload."); - if (file.Length > MaxFileSizeBytes) return BadRequest("CV import file is too large."); + if (file.Length > MaxFileSizeBytes) return BadRequest("CV import file is too large. Keep it under 5 MB."); var extension = Path.GetExtension(file.FileName ?? string.Empty); if (!AllowedExtensions.Contains(extension)) @@ -78,7 +78,10 @@ public sealed class ProfileCvController : ControllerBase var raw = Encoding.UTF8.GetString(bytes); var textMatches = Regex.Matches(raw, @"\((.*?)\)Tj", RegexOptions.Singleline) .Select(match => match.Groups[1].Value) + .Concat(Regex.Matches(raw, @"\[(.*?)\]TJ", RegexOptions.Singleline) + .SelectMany(match => Regex.Matches(match.Groups[1].Value, @"\((.*?)\)", RegexOptions.Singleline).Select(x => x.Groups[1].Value))) .Where(value => !string.IsNullOrWhiteSpace(value)) + .Select(value => Regex.Unescape(value)) .ToList(); var joined = textMatches.Count > 0 ? string.Join(" ", textMatches) : raw; diff --git a/JobTrackerApi/Controllers/UsersController.cs b/JobTrackerApi/Controllers/UsersController.cs index 5320d13..20bc74f 100644 --- a/JobTrackerApi/Controllers/UsersController.cs +++ b/JobTrackerApi/Controllers/UsersController.cs @@ -153,7 +153,7 @@ public sealed class UsersController : ControllerBase await _email.SendAsync( u.Email, "Password reset", - $"An admin initiated a password reset for your Job Tracker account.\n\nReset link:\n{link}\n", + $"An admin initiated a password reset for your Jobbjakt account.\n\nReset link:\n{link}\n", cancellationToken ); @@ -171,9 +171,9 @@ public sealed class UsersController : ControllerBase var toEmail = (request?.ToEmail ?? currentUser?.Email ?? "").Trim(); if (string.IsNullOrWhiteSpace(toEmail)) return BadRequest("Recipient email is required."); - var subject = string.IsNullOrWhiteSpace(request?.Subject) ? "Job Tracker test email" : request!.Subject!.Trim(); + var subject = string.IsNullOrWhiteSpace(request?.Subject) ? "Jobbjakt test email" : request!.Subject!.Trim(); var message = string.IsNullOrWhiteSpace(request?.Message) - ? "This is a test email from the Job Tracker admin panel.\n\nIf you received this, the SMTP configuration is working." + ? "This is a test email from the Jobbjakt admin panel.\n\nIf you received this, the SMTP configuration is working." : request!.Message!.Trim(); await _email.SendAsync( diff --git a/JobTrackerApi/Services/SmtpEmailSender.cs b/JobTrackerApi/Services/SmtpEmailSender.cs index 5c20db8..18e5b82 100644 --- a/JobTrackerApi/Services/SmtpEmailSender.cs +++ b/JobTrackerApi/Services/SmtpEmailSender.cs @@ -25,7 +25,7 @@ public sealed class SmtpEmailSender : IAppEmailSender var user = (_cfg["Email:SmtpUser"] ?? "").Trim(); var pass = (_cfg["Email:SmtpPassword"] ?? "").Trim(); var from = (_cfg["Email:From"] ?? user).Trim(); - var fromName = (_cfg["Email:FromName"] ?? "Job Tracker").Trim(); + var fromName = (_cfg["Email:FromName"] ?? "Jobbjakt").Trim(); var port = _cfg.GetValue("Email:SmtpPort", 587); if (port <= 0) port = 587; diff --git a/docs/jobbjakt-cleanup-tracker.md b/docs/jobbjakt-cleanup-tracker.md new file mode 100644 index 0000000..4d85e32 --- /dev/null +++ b/docs/jobbjakt-cleanup-tracker.md @@ -0,0 +1,70 @@ +# Jobbjakt cleanup tracker + +Last updated: 2026-03-23 + +## Build / UI Issues +- [x] Fix visible build error text appearing on page load/footer +- [x] Resolve naming inconsistency: `jobtrack` → `Jobbjakt` +- [ ] Audit placeholder vertical alignment in all text fields +- [ ] Complete final UI overhaul / visual consistency pass +- [x] Add frontend 404 page +- [x] Add frontend route error page + +## Job Creation / Company Features +- [x] Remove “Next Action” from create job form +- [x] Remove “Follow-up Date” from create job form +- [x] Replace “Save and add another” checkbox with action buttons +- [x] Add modal Close button at bottom +- [x] Add modal top-right close button +- [x] Remove file-type checkboxes +- [x] Add categorized file uploads for CV / portfolio / other files +- [x] Prefer document formats while allowing text/image uploads +- [x] Replace inline cover text field with upload-based flow +- [ ] Verify translated text area is shown only when content is not English +- [x] Improve create-company error visibility in frontend +- [ ] Verify company creation works in production after deploy +- [x] Add backend production schema guard for company/job ownership columns + +## Profile Page +- [x] Remove duplicate “Google account not linked” messages +- [x] Add profile image upload +- [x] Add crop dialog for profile image +- [x] Add zoom in/out support for image cropping +- [x] Use square cropped avatar output +- [x] Add CV upload support +- [ ] Verify/complete OCR/text extraction for uploaded CV PDFs + +## Settings & System +- [x] Restore missing follow-up days settings +- [x] Move SMTP test from user page to admin/system page +- [x] Move language selector into Settings +- [x] Make language selection apply globally +- [ ] Audit app-wide preference for username over email +- [ ] Verify auto-fill of email and full name from profile where relevant + +## Dashboard / System Pages +- [x] Reduce duplicate system data on dashboard +- [x] Consolidate system-related information into system/admin area +- [ ] Audit summarizer/system state consistency across dashboard and system page +- [x] Verify there is no separate pipeline page left outside the system/company context +- [ ] Remove remaining redundant dashboard/system duplication + +## Translations / Localization +- [x] Expand translation infrastructure with interpolation support +- [x] Move major app shell/settings/profile/admin/create-job views onto translation system +- [x] Translate major table/kanban/edit flows further +- [ ] Finish remaining translation coverage in lingering dialogs/components +- [ ] Review English/Norwegian phrasing consistency across updated UI + +## Production / Deployment +- [x] Audit production API base URL / proxy setup +- [x] Document recommendation to leave `REACT_APP_API_BASE_URL` empty when using bundled proxy +- [x] Replace committed dev secret-like values with placeholders in dev config +- [ ] Rotate any real secrets that may previously have been committed/exposed +- [ ] Verify production environment variables match documented setup + +## UX / Consistency +- [x] Simplify create-job workflow +- [x] Reduce duplicated UI/data across multiple pages +- [ ] Perform final UX clarity pass across major screens +- [ ] Perform final consistency pass on labels, spacing, empty states, and feedback messages diff --git a/job-tracker-ui/src/components/AuthStatusCard.tsx b/job-tracker-ui/src/components/AuthStatusCard.tsx index 3c10255..e09170d 100644 --- a/job-tracker-ui/src/components/AuthStatusCard.tsx +++ b/job-tracker-ui/src/components/AuthStatusCard.tsx @@ -37,7 +37,7 @@ export default function AuthStatusCard() { .catch(() => setMe(null)); }, [token]); - const label = useMemo(() => me?.displayName || [me?.firstName, me?.lastName].filter(Boolean).join(" ") || me?.email, [me]); + const label = useMemo(() => me?.displayName || [me?.firstName, me?.lastName].filter(Boolean).join(" ") || me?.userName || me?.email, [me]); return ( diff --git a/job-tracker-ui/src/components/Correspondence.tsx b/job-tracker-ui/src/components/Correspondence.tsx index 6d691fb..383498c 100644 --- a/job-tracker-ui/src/components/Correspondence.tsx +++ b/job-tracker-ui/src/components/Correspondence.tsx @@ -27,7 +27,7 @@ import AutoAwesomeIcon from "@mui/icons-material/AutoAwesome"; import MailOutlineIcon from "@mui/icons-material/MailOutline"; import { IconButton } from "@mui/material"; -import { api } from "../api"; +import { api, getApiErrorMessage } from "../api"; import { useToast } from "../toast"; import { CorrespondenceMessage, GmailMessageSummary, GmailStatus } from "../types"; import { useDialogActions } from "../dialogs"; @@ -126,8 +126,7 @@ export default function Correspondence({ jobId }: { jobId: number }) { }); setGmailMessages(res.data); } catch (error: any) { - const message = error?.response?.data && typeof error.response.data === "string" ? error.response.data : "Failed to load Gmail messages."; - toast(message, "error"); + toast(getApiErrorMessage(error, "Failed to load Gmail messages."), "error"); } finally { setGmailMessagesLoading(false); } @@ -206,8 +205,8 @@ export default function Correspondence({ jobId }: { jobId: number }) { await api.post("/correspondence", { jobApplicationId: jobId, from, content: text }); setText(""); await load(); - } catch { - toast("Failed to add message.", "error"); + } catch (error) { + toast(getApiErrorMessage(error, "Failed to add message."), "error"); } }; @@ -231,8 +230,8 @@ export default function Correspondence({ jobId }: { jobId: number }) { setRawEmail(""); await load(); toast("Email logged.", "success"); - } catch { - toast("Failed to import email.", "error"); + } catch (error) { + toast(getApiErrorMessage(error, "Failed to import email."), "error"); } }; @@ -241,8 +240,8 @@ export default function Correspondence({ jobId }: { jobId: number }) { const res = await api.get<{ url: string }>("/gmail/connect-url"); const popup = window.open(res.data.url, "jobtracker-gmail-connect", "width=620,height=760,resizable=yes,scrollbars=yes"); if (!popup) toast("Your browser blocked the Gmail popup.", "error"); - } catch { - toast("Failed to start Gmail connection.", "error"); + } catch (error) { + toast(getApiErrorMessage(error, "Failed to start Gmail connection."), "error"); } }; @@ -252,8 +251,8 @@ export default function Correspondence({ jobId }: { jobId: number }) { setGmailStatus({ connected: false }); setGmailMessages([]); toast("Gmail disconnected.", "success"); - } catch { - toast("Failed to disconnect Gmail.", "error"); + } catch (error) { + toast(getApiErrorMessage(error, "Failed to disconnect Gmail."), "error"); } }; @@ -263,8 +262,8 @@ export default function Correspondence({ jobId }: { jobId: number }) { await api.delete(`/correspondence/${messageId}`); await load(); toast("Message removed.", "success"); - } catch { - toast("Failed to remove message.", "error"); + } catch (error) { + toast(getApiErrorMessage(error, "Failed to remove message."), "error"); } }; @@ -275,8 +274,7 @@ export default function Correspondence({ jobId }: { jobId: number }) { await load(); toast("Email imported from Gmail.", "success"); } catch (error: any) { - const message = error?.response?.data && typeof error.response.data === "string" ? error.response.data : "Failed to import Gmail message."; - toast(message, "error"); + toast(getApiErrorMessage(error, "Failed to import Gmail message."), "error"); } finally { setImportingMessageId(null); } @@ -289,8 +287,7 @@ export default function Correspondence({ jobId }: { jobId: number }) { await load(); toast(`Imported ${res.data.imported} messages${res.data.skipped ? `, skipped ${res.data.skipped} duplicates` : ""}.`, "success"); } catch (error: any) { - const message = error?.response?.data && typeof error.response.data === "string" ? error.response.data : "Failed to import Gmail thread."; - toast(message, "error"); + toast(getApiErrorMessage(error, "Failed to import Gmail thread."), "error"); } finally { setImportingThreadId(null); } diff --git a/job-tracker-ui/src/components/GoogleAuthCard.tsx b/job-tracker-ui/src/components/GoogleAuthCard.tsx index 9e6b0e7..8fbf654 100644 --- a/job-tracker-ui/src/components/GoogleAuthCard.tsx +++ b/job-tracker-ui/src/components/GoogleAuthCard.tsx @@ -16,6 +16,7 @@ declare global { type MeResponse = { provider?: "local" | "google" | "external"; email?: string; + userName?: string; displayName?: string; firstName?: string; lastName?: string; @@ -153,7 +154,7 @@ export default function GoogleAuthCard({ onSignedIn }: { onSignedIn?: () => void }; }, [clientId, me?.provider, me?.googleLink?.linked, onSignedIn, isRawGoogleToken, token, toast, t]); - const signedInName = me?.displayName || [me?.firstName, me?.lastName].filter(Boolean).join(" ") || me?.email || ""; + const signedInName = me?.displayName || [me?.firstName, me?.lastName].filter(Boolean).join(" ") || me?.userName || me?.email || ""; return ( diff --git a/job-tracker-ui/src/components/JobDetailsDialog.tsx b/job-tracker-ui/src/components/JobDetailsDialog.tsx index e4f6897..b530588 100644 --- a/job-tracker-ui/src/components/JobDetailsDialog.tsx +++ b/job-tracker-ui/src/components/JobDetailsDialog.tsx @@ -18,7 +18,7 @@ import { Typography, } from "@mui/material"; -import { api } from "../api"; +import { api, getApiErrorMessage } from "../api"; import { ApplicationPackageResponse, CandidateFit, InterviewPrepResponse, JobApplication, ReadinessResponse } from "../types"; import { useToast } from "../toast"; import { useDialogActions } from "../dialogs"; @@ -216,7 +216,7 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) { setJob(res.data); toast("Summary and skills refreshed.", "success"); } catch (error: any) { - toast(error?.response?.data || "Failed to refresh summary and skills.", "error"); + toast(getApiErrorMessage(error, "Failed to refresh summary and skills."), "error"); } finally { setRefreshingAi(false); } @@ -277,7 +277,7 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) { setTailoredCvText(res.data.tailoredCvText ?? ""); toast("Application package generated.", "success"); } catch (error: any) { - toast(error?.response?.data || "Failed to generate application package.", "error"); + toast(getApiErrorMessage(error, "Failed to generate application package."), "error"); } finally { setGeneratingPackage(false); } @@ -294,7 +294,7 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) { setInterviewPrep(null); toast("Tailored CV saved.", "success"); } catch (error: any) { - toast(error?.response?.data || "Failed to save tailored CV.", "error"); + toast(getApiErrorMessage(error, "Failed to save tailored CV."), "error"); } finally { setSavingTailoredCv(false); } @@ -317,7 +317,7 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) { setReadiness(null); toast("Cover letter saved to this job.", "success"); } catch (error: any) { - toast(error?.response?.data || "Failed to save cover letter.", "error"); + toast(getApiErrorMessage(error, "Failed to save cover letter."), "error"); } finally { setSavingApplicationDrafts(false); } @@ -330,7 +330,7 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) { setReadiness(null); toast("Application answer saved to notes.", "success"); } catch (error: any) { - toast(error?.response?.data || "Failed to save application answer.", "error"); + toast(getApiErrorMessage(error, "Failed to save application answer."), "error"); } finally { setSavingApplicationDrafts(false); } @@ -343,7 +343,7 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) { setJob((prev) => prev ? { ...prev, recruiterMessageDraft: content } : prev); toast("Recruiter message saved to this job.", "success"); } catch (error: any) { - toast(error?.response?.data || "Failed to save recruiter message.", "error"); + toast(getApiErrorMessage(error, "Failed to save recruiter message."), "error"); } finally { setSavingApplicationDrafts(false); } @@ -374,7 +374,7 @@ export default function JobDetailsDialog({ open, jobId, onClose }: Props) { setReadiness(null); toast("Follow-up sent and logged.", "success"); } catch (error: any) { - toast(error?.response?.data || "Failed to send follow-up.", "error"); + toast(getApiErrorMessage(error, "Failed to send follow-up."), "error"); } finally { setSendingDraft(false); } diff --git a/job-tracker-ui/src/i18n/translations.ts b/job-tracker-ui/src/i18n/translations.ts index ea7fa07..6d3f4ec 100644 --- a/job-tracker-ui/src/i18n/translations.ts +++ b/job-tracker-ui/src/i18n/translations.ts @@ -171,7 +171,7 @@ export const translations = { profileHeadline: "Profile headline", profileHeadlineHelp: "Stored only in this browser to personalize your workspace.", profileMasterCv: "Master CV", - profileMasterCvBody: "Upload a PDF, Word document, plain text file, markdown file, or image scan. Where supported, the app can extract text automatically and populate your master CV text for tailoring and outreach.", + profileMasterCvBody: "Upload a PDF, DOCX, plain text file, or markdown file. The app extracts text where supported and populates your master CV text for tailoring and outreach.", profileUploadCv: "Upload CV", profileUploading: "Uploading...", profileCopyCvText: "Copy CV text", @@ -179,7 +179,7 @@ export const translations = { profileCvUploadFailed: "Failed to upload CV.", profileCvTextLabel: "Profile CV / master resume text", profileCvTextHelp: "Keep this updated and specific. Include recent roles, tools, achievements, measurable outcomes, and the work you want to be hired for next. If extraction misses something, edit it here manually.", - profileCvPreferredUploads: "Preferred uploads: PDF, DOC, DOCX. Text and image files are also accepted.", + profileCvPreferredUploads: "Supported uploads: PDF, DOCX, TXT, MD.", profileSaveChanges: "Save changes", profileUpdated: "Profile updated.", profileUpdateFailed: "Failed to update profile.", @@ -591,7 +591,7 @@ export const translations = { profileHeadline: "Profiloverskrift", profileHeadlineHelp: "Lagres bare i denne nettleseren for å gjøre arbeidsområdet mer personlig.", profileMasterCv: "Hoved-CV", - profileMasterCvBody: "Last opp en PDF, et Word-dokument, en ren tekstfil, en markdown-fil eller et bildeskann. Der det støttes kan appen automatisk hente ut tekst og fylle inn hoved-CV-en din for tilpasning og kontakt.", + profileMasterCvBody: "Last opp en PDF, DOCX, ren tekstfil eller markdown-fil. Appen henter ut tekst der det støttes og fyller inn hoved-CV-en din for tilpasning og kontakt.", profileUploadCv: "Last opp CV", profileUploading: "Laster opp...", profileCopyCvText: "Kopier CV-tekst", @@ -599,7 +599,7 @@ export const translations = { profileCvUploadFailed: "Kunne ikke laste opp CV.", profileCvTextLabel: "Profil-CV / hovedtekst for CV", profileCvTextHelp: "Hold denne oppdatert og konkret. Ta med nylige roller, verktøy, prestasjoner, målbare resultater og arbeidet du vil bli ansatt for neste gang. Hvis tekstuttrekket mangler noe, kan du redigere manuelt her.", - profileCvPreferredUploads: "Foretrukne opplastinger: PDF, DOC, DOCX. Tekst- og bildefiler aksepteres også.", + profileCvPreferredUploads: "Støttede opplastinger: PDF, DOCX, TXT, MD.", profileSaveChanges: "Lagre endringer", profileUpdated: "Profil oppdatert.", profileUpdateFailed: "Kunne ikke oppdatere profil.", diff --git a/job-tracker-ui/src/pages/AdminSystemPage.tsx b/job-tracker-ui/src/pages/AdminSystemPage.tsx index 7489ffc..68fd011 100644 --- a/job-tracker-ui/src/pages/AdminSystemPage.tsx +++ b/job-tracker-ui/src/pages/AdminSystemPage.tsx @@ -11,7 +11,7 @@ import { Typography, } from "@mui/material"; -import { api } from "../api"; +import { api, getApiErrorMessage } from "../api"; import { useI18n } from "../i18n/I18nProvider"; type SummarizerMetrics = { @@ -130,7 +130,7 @@ export default function AdminSystemPage() { const res = await api.get("/admin/system"); setStatus(res.data); } catch (e: any) { - setError(e?.response?.data || e?.message || "Failed to load system status."); + setError(getApiErrorMessage(e, "Failed to load system status.")); setStatus(null); } finally { setLoading(false); @@ -164,7 +164,7 @@ export default function AdminSystemPage() { message: testEmailMessage.trim() || null, }); } catch (e: any) { - setError(e?.response?.data || e?.message || "Failed to send test email."); + setError(getApiErrorMessage(e, "Failed to send test email.")); } finally { setSendingTestEmail(false); } @@ -187,7 +187,7 @@ export default function AdminSystemPage() { await api.post("/admin/system/summarizer/probe"); await load(); } catch (e: any) { - setError(e?.response?.data || e?.message || "Failed to run summarizer probe."); + setError(getApiErrorMessage(e, "Failed to run summarizer probe.")); } finally { setRunningProbe(false); } diff --git a/job-tracker-ui/src/pages/ProfilePage.tsx b/job-tracker-ui/src/pages/ProfilePage.tsx index 01881bd..8eb6397 100644 --- a/job-tracker-ui/src/pages/ProfilePage.tsx +++ b/job-tracker-ui/src/pages/ProfilePage.tsx @@ -29,7 +29,7 @@ type MeResponse = { } | null; }; -const CV_UPLOAD_ACCEPT = ".pdf,.doc,.docx,.txt,.md,image/*,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/plain,text/markdown"; +const CV_UPLOAD_ACCEPT = ".pdf,.docx,.txt,.md,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/plain,text/markdown"; const AVATAR_UPLOAD_ACCEPT = "image/png,image/jpeg,image/webp"; function initialsFrom(values: Array) { diff --git a/job-tracker-ui/src/theme.ts b/job-tracker-ui/src/theme.ts index dffc9ec..8495143 100644 --- a/job-tracker-ui/src/theme.ts +++ b/job-tracker-ui/src/theme.ts @@ -285,6 +285,7 @@ export const getTheme = (_mode: "light" | "dark", accentColor: string) => { paddingRight: 0, paddingTop: 10, paddingBottom: 10, + lineHeight: 1.45, }, inputMultiline: { paddingTop: 0, @@ -295,6 +296,11 @@ export const getTheme = (_mode: "light" | "dark", accentColor: string) => { }, MuiInputBase: { styleOverrides: { + input: { + "&::placeholder": { + opacity: 0.72, + }, + }, inputMultiline: { "&::placeholder": { opacity: 0.72,