Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 42 additions & 17 deletions src/routers/web/associations.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { asc, eq } from "drizzle-orm"
import z from "zod"
import { uploadBlob } from "@/azure/blob"
import { DB, SCHEMA } from "@/db"
import { createTRPCRouter, publicProcedure } from "@/trpc"
import { getImageExtension } from "@/utils/web"

const ASSOCIATIONS = SCHEMA.WEB.associations

const logoFileSchema = z
.file()
.mime(["image/png", "image/jpeg", "image/svg+xml"])
.min(1)
.max(1024 * 1024)

const associationLinksSchema = z.object({
email: z.email().nullable(),
website: z.url().nullable(),
Expand All @@ -18,6 +26,13 @@ const associationLinksSchema = z.object({
spotify: z.url().nullable(),
})

const associationFormSchema = z.object({
name: z.string(),
descriptionIt: z.string(),
descriptionEn: z.string(),
logo: logoFileSchema.optional(),
})
Comment thread
BIA3IA marked this conversation as resolved.

const associationSchema = z.object({
id: z.number(),
name: z.string(),
Expand Down Expand Up @@ -58,23 +73,28 @@ export default createTRPCRouter({

addAssociation: publicProcedure
.input(
z.object({
name: z.string(),
descriptionIt: z.string(),
descriptionEn: z.string(),
logo: z.string().nullable(),
createdBy: z.number(),
})
z
.instanceof(FormData)
.transform((fd): Record<string, string | File> => Object.fromEntries(fd.entries()))
.pipe(
associationFormSchema.extend({
createdBy: z.coerce.number<string>(),
})
)
)
.mutation(async ({ input }) => {
const { name, descriptionIt, descriptionEn, logo, createdBy } = input

const uploadedLogo = logo
? await uploadBlob(Buffer.from(await logo.arrayBuffer()), getImageExtension(logo), logo.type)
Comment thread
BIA3IA marked this conversation as resolved.
: null

const [res] = await DB.insert(ASSOCIATIONS)
.values({
name,
descriptionIt,
descriptionEn,
logo,
logo: uploadedLogo?.url ?? null,
createdBy,
})
.returning()
Expand All @@ -84,24 +104,29 @@ export default createTRPCRouter({

editAssociation: publicProcedure
.input(
z.object({
id: z.number(),
name: z.string(),
descriptionIt: z.string(),
descriptionEn: z.string(),
logo: z.string().nullable(),
modifiedBy: z.number(),
})
z
.instanceof(FormData)
.transform((fd): Record<string, string | File> => Object.fromEntries(fd.entries()))
.pipe(
associationFormSchema.extend({
id: z.coerce.number<string>(),
modifiedBy: z.coerce.number<string>(),
})
)
)
.mutation(async ({ input }) => {
const { id, name, descriptionIt, descriptionEn, logo, modifiedBy } = input

const uploadedLogo = logo
? await uploadBlob(Buffer.from(await logo.arrayBuffer()), getImageExtension(logo), logo.type)
: null

const [res] = await DB.update(ASSOCIATIONS)
.set({
name,
descriptionIt,
descriptionEn,
logo,
logo: uploadedLogo?.url ?? null,
Comment on lines +120 to +129

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve the existing logo on edit when no new file is uploaded.

logo is optional, but the edit path writes null when omitted, so text-only edits delete the current logo. Only set logo when replacing it, or add an explicit removal flag.

Proposed fix
-      const uploadedLogo = logo
-        ? await uploadBlob(Buffer.from(await logo.arrayBuffer()), getImageExtension(logo), logo.type)
-        : null
+      const uploadedLogoUrl = logo
+        ? (await uploadBlob(Buffer.from(await logo.arrayBuffer()), getImageExtension(logo), logo.type)).url
+        : undefined
 
       const [res] = await DB.update(ASSOCIATIONS)
         .set({
           name,
           descriptionIt,
           descriptionEn,
-          logo: uploadedLogo?.url ?? null,
+          ...(uploadedLogoUrl !== undefined ? { logo: uploadedLogoUrl } : {}),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const uploadedLogo = logo
? await uploadBlob(Buffer.from(await logo.arrayBuffer()), getImageExtension(logo), logo.type)
: null
const [res] = await DB.update(ASSOCIATIONS)
.set({
name,
descriptionIt,
descriptionEn,
logo,
logo: uploadedLogo?.url ?? null,
const uploadedLogoUrl = logo
? (await uploadBlob(Buffer.from(await logo.arrayBuffer()), getImageExtension(logo), logo.type)).url
: undefined
const [res] = await DB.update(ASSOCIATIONS)
.set({
name,
descriptionIt,
descriptionEn,
...(uploadedLogoUrl !== undefined ? { logo: uploadedLogoUrl } : {}),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routers/web/associations.ts` around lines 120 - 129, The edit flow in
associations.ts currently clears the existing logo when no new file is uploaded.
Update the update path around the uploadBlob handling and
DB.update(ASSOCIATIONS).set(...) so logo is only changed when a new file is
provided, and otherwise left untouched; if intentional removal is needed, add an
explicit remove flag in the same update logic.

modifiedBy,
})
.where(eq(ASSOCIATIONS.id, id))
Expand Down
85 changes: 65 additions & 20 deletions src/routers/web/projects.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
import { asc, eq } from "drizzle-orm"
import z from "zod"
import { uploadBlob } from "@/azure/blob"
import { DB, SCHEMA } from "@/db"
import { projectsCategories } from "@/db/schema/web/projects"
import { createTRPCRouter, publicProcedure } from "@/trpc"
import { getImageExtension } from "@/utils/web"

const PROJECTS = SCHEMA.WEB.projects

const logoFileSchema = z
.file()
.mime(["image/png", "image/jpeg", "image/svg+xml"])
.min(1)
.max(1024 * 1024)

const projectFormSchema = z.object({
title: z.string(),
descriptionIt: z.string(),
descriptionEn: z.string(),
logo: logoFileSchema.optional(),
link: z.string().transform((value) => value || null),
category: z.enum(projectsCategories),
})
Comment thread
BIA3IA marked this conversation as resolved.

const projectSchema = z.object({
id: z.number(),
title: z.string(),
Expand All @@ -22,16 +39,29 @@ export default createTRPCRouter({
}),

addProject: publicProcedure
.input(projectSchema.omit({ id: true }).extend({ createdBy: z.number() }))
.input(
z
.instanceof(FormData)
.transform((fd): Record<string, string | File> => Object.fromEntries(fd.entries()))
.pipe(
projectFormSchema.extend({
createdBy: z.coerce.number<string>(),
})
)
)
.mutation(async ({ input }) => {
const { title, descriptionIt, descriptionEn, logo, link, category, createdBy } = input

const uploadedLogo = logo
? await uploadBlob(Buffer.from(await logo.arrayBuffer()), getImageExtension(logo), logo.type)
Comment thread
BIA3IA marked this conversation as resolved.
: null

const [res] = await DB.insert(PROJECTS)
.values({
title,
descriptionIt,
descriptionEn,
logo,
logo: uploadedLogo?.url ?? null,
link,
category,
createdBy,
Expand All @@ -41,25 +71,40 @@ export default createTRPCRouter({
return res
}),

editProject: publicProcedure.input(projectSchema.extend({ modifiedBy: z.number() })).mutation(async ({ input }) => {
const { id, title, descriptionIt, descriptionEn, logo, link, category, modifiedBy } = input

const [res] = await DB.update(PROJECTS)
.set({
title,
descriptionIt,
descriptionEn,
logo,
link,
category,
modifiedBy,
})
.where(eq(PROJECTS.id, id))
.returning()
editProject: publicProcedure
.input(
z
.instanceof(FormData)
.transform((fd): Record<string, string | File> => Object.fromEntries(fd.entries()))
.pipe(
projectFormSchema.extend({
id: z.coerce.number<string>(),
modifiedBy: z.coerce.number<string>(),
})
)
)
.mutation(async ({ input }) => {
const { title, descriptionIt, descriptionEn, logo, link, category, modifiedBy } = input
const uploadedLogo = logo
? await uploadBlob(Buffer.from(await logo.arrayBuffer()), getImageExtension(logo), logo.type)
: null

if (!res) return { error: "NOT_FOUND" }
return res
}),
const [res] = await DB.update(PROJECTS)
.set({
title,
descriptionIt,
descriptionEn,
logo: uploadedLogo?.url ?? null,
Comment thread
BIA3IA marked this conversation as resolved.
link,
category,
modifiedBy,
})
.where(eq(PROJECTS.id, input.id))
.returning()

if (!res) return { error: "NOT_FOUND" }
return res
}),

reorderProjects: publicProcedure
.input(
Expand Down
13 changes: 13 additions & 0 deletions src/utils/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,16 @@ export async function verifyTgLink(link: string): Promise<boolean> {
root.querySelector(".tgme_page_action")?.querySelector("a")?.classList.contains("tgme_action_button_new") ?? false
)
}

export function getImageExtension(file: File) {
switch (file.type) {
case "image/png":
return "png"
case "image/jpeg":
return "jpeg"
case "image/svg+xml":
return "svg"
default:
throw new Error("Unsupported image type")
}
}