mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-26 18:04:45 +10:00
feat(applications): close follow-up gaps + squash migrations
Finish the deferred/open items on the applications tracker: - Cover-letter upload re-enabled. Fix the storage blocker by deriving the key extension from content type (buildFileKey/EXTENSION_BY_CONTENT_TYPE) instead of hardcoding .jpeg, so PDFs serve correctly and non-JPEG image avatars keep working under FLAG_DISABLE_IMAGE_PROCESSING. Add coverLetterUrl/coverLetterName columns + Documents-section upload/remove. - Contacts editor in the detail sheet (add/edit/remove, keyed per app). - Board caps rendered cards per column (COLUMN_PAGE_SIZE=50 + "Show more"). - Extract new Lingui messages across locales. - Guard coverLetterUrl to http(s)/relative at the API boundary. Squash the five branch-only application-table migrations (create -> +tags -> +cover-letter -> drop -> re-add) into a single clean CREATE TABLE via drizzle-kit generate. Claude-Session: https://claude.ai/code/session_01TEeRHnEayw2MFCShFRyL5f
This commit is contained in:
@@ -24,6 +24,13 @@ const applicationSchema = createSelectSchema(schema.application, {
|
||||
aiMetadata: aiMetadataSchema.nullable(),
|
||||
campaign: z.string().trim().nullable(),
|
||||
notes: z.string().nullable(),
|
||||
// Rendered as an <a href>; only same-origin storage URLs are ever stored. Constrain to
|
||||
// http(s)/relative so a hand-crafted `update` can't smuggle a `javascript:` href.
|
||||
coverLetterUrl: z
|
||||
.string()
|
||||
.refine((value) => /^(https?:\/\/|\/)/.test(value), "Cover letter URL must be http(s) or a relative path.")
|
||||
.nullable(),
|
||||
coverLetterName: z.string().nullable(),
|
||||
followUpAt: z.date().nullable(),
|
||||
followUpNote: z.string().trim().nullable(),
|
||||
tags: z.array(z.string()),
|
||||
@@ -47,6 +54,8 @@ const editableSchema = applicationSchema.pick({
|
||||
jobDescription: true,
|
||||
campaign: true,
|
||||
notes: true,
|
||||
coverLetterUrl: true,
|
||||
coverLetterName: true,
|
||||
followUpAt: true,
|
||||
followUpNote: true,
|
||||
contacts: true,
|
||||
|
||||
@@ -26,6 +26,8 @@ type EditableFields = {
|
||||
jobDescription?: string | null | undefined;
|
||||
campaign?: string | null | undefined;
|
||||
notes?: string | null | undefined;
|
||||
coverLetterUrl?: string | null | undefined;
|
||||
coverLetterName?: string | null | undefined;
|
||||
followUpAt?: Date | null | undefined;
|
||||
followUpNote?: string | null | undefined;
|
||||
contacts?: Contact[] | undefined;
|
||||
|
||||
@@ -55,9 +55,21 @@ const DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
||||
|
||||
const IMAGE_MIME_TYPES = ["image/gif", "image/png", "image/jpeg", "image/webp"];
|
||||
|
||||
function buildPictureKey(userId: string): string {
|
||||
const timestamp = Date.now();
|
||||
return `uploads/${userId}/pictures/${timestamp}.jpeg`;
|
||||
const EXTENSION_BY_CONTENT_TYPE: Record<string, string> = {
|
||||
"image/jpeg": "jpeg",
|
||||
"image/png": "png",
|
||||
"image/webp": "webp",
|
||||
"image/gif": "gif",
|
||||
"application/pdf": "pdf",
|
||||
};
|
||||
|
||||
// Derive the stored key's extension from the content type so the static handler serves each
|
||||
// file correctly instead of mislabeling it. Images normally arrive as JPEG (sharp), but with
|
||||
// FLAG_DISABLE_IMAGE_PROCESSING they keep their original type — hence all image types are
|
||||
// mapped, not just JPEG. Non-image uploads (e.g. a cover-letter PDF) get their real extension.
|
||||
function buildFileKey(userId: string, contentType: string): string {
|
||||
const extension = EXTENSION_BY_CONTENT_TYPE[contentType] ?? "bin";
|
||||
return `uploads/${userId}/pictures/${Date.now()}.${extension}`;
|
||||
}
|
||||
|
||||
function buildPublicUrl(path: string): string {
|
||||
@@ -337,9 +349,8 @@ interface UploadFileResult {
|
||||
key: string;
|
||||
}
|
||||
|
||||
// ponytail: only "picture" uploads exist in production; screenshot/pdf types were speculative dead code
|
||||
export async function uploadFile(input: UploadFileInput): Promise<UploadFileResult> {
|
||||
const key = buildPictureKey(input.userId);
|
||||
const key = buildFileKey(input.userId, input.contentType);
|
||||
await getStorageService().write({ key, data: input.data, contentType: input.contentType });
|
||||
return { key, url: buildPublicUrl(key) };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user