diff --git a/apps/hackathon/src/app/certificate/winner-data.json b/apps/hackathon/src/app/certificate/winner-data.json deleted file mode 100644 index 70fa2f4..0000000 --- a/apps/hackathon/src/app/certificate/winner-data.json +++ /dev/null @@ -1,236 +0,0 @@ -{ - "message": "Success", - "data": [ - { - "id": "afbead4b-2ffa-435e-ae7b-91f26e111fc6", - "team_id": "ab436f6e-027f-4831-873b-474345273491", - "team": { - "id": "ab436f6e-027f-4831-873b-474345273491", - "name": "nama timnya apa ya @All", - "description": "pikri carry", - "city": "Kota Depok", - "visibility": "public", - "logo": null, - "banner": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/9a6dc902-c91b-4276-89cf-31e821531158-1764517227821.png", - "leader_id": "9a6dc902-c91b-4276-89cf-31e821531158", - "created_at": "2025-11-30T15:40:29.865564Z", - "updated_at": "2025-11-30T15:48:52.520850Z" - }, - "rank": 1, - "prize": "Rp6.000.000", - "announced_at": "2025-12-15T16:41:01.931757Z", - "created_at": "2025-12-15T16:41:01.931757Z", - "updated_at": "2025-12-15T16:41:01.931757Z" - }, - { - "id": "8e591dc6-3cff-4019-bdfc-5e42ea081208", - "team_id": "06cbf0f4-cdad-40d0-b0d0-1fa8a3d50968", - "team": { - "id": "06cbf0f4-cdad-40d0-b0d0-1fa8a3d50968", - "name": "TheLastOfUs", - "description": "We truly believe in Meaning First β designing innovation, powered by technology.", - "city": "Bandung", - "visibility": "private", - "logo": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/a32c0e81-1fbd-4bec-8e91-4cd6d10f553b-1764074851150.png", - "banner": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/a32c0e81-1fbd-4bec-8e91-4cd6d10f553b-1764074852181.png", - "leader_id": "a32c0e81-1fbd-4bec-8e91-4cd6d10f553b", - "created_at": "2025-11-25T12:47:33.040330Z", - "updated_at": "2025-11-25T12:47:33.040330Z" - }, - "rank": 2, - "prize": "Rp4.000.000", - "announced_at": "2025-12-15T16:41:32.534295Z", - "created_at": "2025-12-15T16:41:32.534295Z", - "updated_at": "2025-12-15T16:41:32.534295Z" - }, - { - "id": "5ca29971-5726-40dd-872c-853a6bfa03d9", - "team_id": "9d9ed9db-3523-47ae-a387-66c36dbec983", - "team": { - "id": "9d9ed9db-3523-47ae-a387-66c36dbec983", - "name": "Pecinta Capybara", - "description": "Ingin ikut meramaikan suasana.", - "city": "Tangerang", - "visibility": "public", - "logo": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/648f6d3d-a92e-49cc-805e-2f600de87074-1764205746897.jpg", - "banner": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/648f6d3d-a92e-49cc-805e-2f600de87074-1764205747479.png", - "leader_id": "648f6d3d-a92e-49cc-805e-2f600de87074", - "created_at": "2025-11-27T01:09:32.787150Z", - "updated_at": "2025-11-29T15:30:43.334609Z" - }, - "rank": 3, - "prize": "Rp2.500.000", - "announced_at": "2025-12-15T16:41:59.091294Z", - "created_at": "2025-12-15T16:41:59.091294Z", - "updated_at": "2025-12-15T16:41:59.091294Z" - }, - { - "id": "e9f82de6-f5d0-43c1-a0bd-4687f2e4ed5e", - "team_id": "95f782f4-433f-489d-a06d-eed89ca83beb", - "team": { - "id": "95f782f4-433f-489d-a06d-eed89ca83beb", - "name": "Savora", - "description": "a complete food ordering application for Indonesian SMEs (UMKM)", - "city": "Bandung", - "visibility": "private", - "logo": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/748de15d-374a-4756-89e2-75754e90542f-1764071952995.png", - "banner": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/748de15d-374a-4756-89e2-75754e90542f-1764071953753.png", - "leader_id": "748de15d-374a-4756-89e2-75754e90542f", - "created_at": "2025-11-25T11:57:52.098043Z", - "updated_at": "2025-11-25T11:59:14.508877Z" - }, - "rank": 4, - "prize": "Rp500.000", - "announced_at": "2025-12-15T16:42:39.361469Z", - "created_at": "2025-12-15T16:42:39.361469Z", - "updated_at": "2025-12-15T16:42:39.361469Z" - }, - { - "id": "ed93e080-0913-4c70-b69d-fce9bc0cdda7", - "team_id": "f9747574-8c01-4c3e-b20a-7ac1a70b947a", - "team": { - "id": "f9747574-8c01-4c3e-b20a-7ac1a70b947a", - "name": "AMBANET Corporation ππΈπ€", - "description": "Bersiaplah ambanet bakal mengguncang seluruh team hackathon ygy", - "city": "Kota Padang", - "visibility": "public", - "logo": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/a5ea628a-24d9-4754-b461-3eda53da53bf-1764351234491.png", - "banner": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/a5ea628a-24d9-4754-b461-3eda53da53bf-1764219109981.gif", - "leader_id": "a5ea628a-24d9-4754-b461-3eda53da53bf", - "created_at": "2025-11-27T04:48:53.080955Z", - "updated_at": "2025-11-29T13:31:51.276246Z" - }, - "rank": 5, - "prize": "Rp500.000", - "announced_at": "2025-12-15T16:42:57.065225Z", - "created_at": "2025-12-15T16:42:57.065225Z", - "updated_at": "2025-12-15T16:42:57.065225Z" - }, - { - "id": "7ed55b12-d585-419d-bae2-1a362a57798e", - "team_id": "4f3e2e85-3f3f-4880-9104-b3f66bb1c9d9", - "team": { - "id": "4f3e2e85-3f3f-4880-9104-b3f66bb1c9d9", - "name": "Pemuja Deadline Anti Refund", - "description": "Diusahakan banyak deadline tapi kg refund ~ \"Angga Kentod\"", - "city": "Kota Bandung", - "visibility": "public", - "logo": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/6cf045e2-d853-4480-91b8-244fb0b76b7f-1764332887921.jpg", - "banner": null, - "leader_id": "6cf045e2-d853-4480-91b8-244fb0b76b7f", - "created_at": "2025-11-27T05:02:35.128625Z", - "updated_at": "2025-11-28T12:28:09.697222Z" - }, - "rank": 6, - "prize": "Rp200.000", - "announced_at": "2025-12-15T16:43:09.471633Z", - "created_at": "2025-12-15T16:43:09.471633Z", - "updated_at": "2025-12-15T16:43:09.471633Z" - }, - { - "id": "0b3a0b5c-2038-4d09-85a7-ae4709702951", - "team_id": "9d8cd404-8e69-4efd-a125-fc0863d7afc7", - "team": { - "id": "9d8cd404-8e69-4efd-a125-fc0863d7afc7", - "name": "Aku nak meletup", - "description": "masih pemula bg", - "city": "Boyolali", - "visibility": "public", - "logo": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/c79d4b2b-e63f-4187-a8cd-520fde333ab3-1764211224866.jpg", - "banner": null, - "leader_id": "c79d4b2b-e63f-4187-a8cd-520fde333ab3", - "created_at": "2025-11-27T02:40:25.376372Z", - "updated_at": "2025-11-27T02:40:25.376372Z" - }, - "rank": 7, - "prize": "Rp200.000", - "announced_at": "2025-12-15T16:43:22.467439Z", - "created_at": "2025-12-15T16:43:22.467439Z", - "updated_at": "2025-12-15T16:43:22.467439Z" - }, - { - "id": "bb0706b9-b723-4a49-b0b0-3be9af70225b", - "team_id": "dd6c62dd-2632-4156-9da5-8ff9a3560f06", - "team": { - "id": "dd6c62dd-2632-4156-9da5-8ff9a3560f06", - "name": "Iseng Hackathon", - "description": "HME ITB Oke Champ", - "city": "Kota Bandung", - "visibility": "public", - "logo": null, - "banner": null, - "leader_id": "d1e4e3bf-098f-453e-bf00-612685d311d9", - "created_at": "2025-11-30T03:58:52.429673Z", - "updated_at": "2025-11-30T03:58:52.429673Z" - }, - "rank": 8, - "prize": "Rp200.000", - "announced_at": "2025-12-15T16:43:34.191207Z", - "created_at": "2025-12-15T16:43:34.191207Z", - "updated_at": "2025-12-15T16:43:34.191207Z" - }, - { - "id": "4a3e807c-2cf2-4557-a6af-99fcc726cebb", - "team_id": "52316c25-5697-4436-bfc2-02baef8ca956", - "team": { - "id": "52316c25-5697-4436-bfc2-02baef8ca956", - "name": "Gorengan 3 Ronde", - "description": "Jam 10 izin turun", - "city": "Kota Medan", - "visibility": "public", - "logo": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/e7bec056-7f63-4642-a763-afa39c3c340e-1764509780084.png", - "banner": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/e7bec056-7f63-4642-a763-afa39c3c340e-1764509810717.png", - "leader_id": "e7bec056-7f63-4642-a763-afa39c3c340e", - "created_at": "2025-11-30T13:36:52.344019Z", - "updated_at": "2025-11-30T13:36:52.344019Z" - }, - "rank": 9, - "prize": "Rp200.000", - "announced_at": "2025-12-15T16:43:51.988908Z", - "created_at": "2025-12-15T16:43:51.988908Z", - "updated_at": "2025-12-15T16:43:51.988908Z" - }, - { - "id": "e18827dd-839d-4e54-9126-c1faebce2c2c", - "team_id": "d2b2fb99-e934-405c-836e-711352e0f8a0", - "team": { - "id": "d2b2fb99-e934-405c-836e-711352e0f8a0", - "name": "Spark Developer", - "description": "Yang penting yakin untuk menang.", - "city": "Kota Batam", - "visibility": "public", - "logo": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/087921fb-2d17-423b-9628-73de2176360f-1764486044138.png", - "banner": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/087921fb-2d17-423b-9628-73de2176360f-1764486059581.jpg", - "leader_id": "087921fb-2d17-423b-9628-73de2176360f", - "created_at": "2025-11-29T04:49:54.266394Z", - "updated_at": "2025-11-30T07:01:02.316185Z" - }, - "rank": 10, - "prize": "Rp200.000", - "announced_at": "2025-12-15T16:44:02.680036Z", - "created_at": "2025-12-15T16:44:02.680036Z", - "updated_at": "2025-12-15T16:44:02.680036Z" - }, - { - "id": "a0ed8e58-f8e3-491c-8c03-20e82090d6f8", - "team_id": "d30d7d03-dce7-4220-9e7f-a537a45c54e0", - "team": { - "id": "d30d7d03-dce7-4220-9e7f-a537a45c54e0", - "name": "Eye of Horus", - "description": "Eye of horus", - "city": "Garut", - "visibility": "public", - "logo": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/96dc1c2b-a1ef-441a-93d5-da2ccad37d81-1764145807852.png", - "banner": "https://voymsjeqmvwrryetfldy.supabase.co/storage/v1/object/public/hackathon-uploads/teams/96dc1c2b-a1ef-441a-93d5-da2ccad37d81-1764145810329.jpeg", - "leader_id": "96dc1c2b-a1ef-441a-93d5-da2ccad37d81", - "created_at": "2025-11-26T08:30:10.385714Z", - "updated_at": "2025-11-26T09:35:12.386986Z" - }, - "rank": 11, - "prize": "hadiah", - "announced_at": "2025-12-16T04:19:09.502015Z", - "created_at": "2025-12-16T04:19:09.502015Z", - "updated_at": "2025-12-16T04:19:09.502015Z" - } - ] -} diff --git a/apps/hackathon/src/app/certificate/winner/[certId]/page.tsx b/apps/hackathon/src/app/certificate/winner/[certId]/page.tsx index 1df25b9..706a659 100644 --- a/apps/hackathon/src/app/certificate/winner/[certId]/page.tsx +++ b/apps/hackathon/src/app/certificate/winner/[certId]/page.tsx @@ -7,6 +7,7 @@ import { useAuthStore, useMyTeams, useTeamById, + useTeamSubmission, useWinners, } from '@imphnen-frontend-service/service'; import QRCode from 'qrcode'; @@ -37,8 +38,20 @@ const formatOrdinalRank = (rank: number): string => { } }; -const CERT_WIDTH = 1000; -const CERT_HEIGHT = (CERT_WIDTH * 595) / 842; // matches blank_winner_cert.svg aspect ratio +// Keep the template size aligned with the SVG native size (842x595) using an integer multiplier. +// This reduces sub-pixel scaling artifacts (blur) on thin lines when rasterizing with html2canvas. +const CERT_WIDTH = 842 * 2; +const CERT_HEIGHT = 595 * 2; + +// Balance between output sharpness and file size. +// Output resolution will be (CERT_WIDTH * EXPORT_SCALE) x (CERT_HEIGHT * EXPORT_SCALE). +const EXPORT_SCALE = 2; + +// The original layout was tuned around a ~1000px-wide template. +// We keep the same visual proportions by scaling fixed pixel values. +const LAYOUT_BASE_WIDTH = 1000; +const LAYOUT_SCALE = CERT_WIDTH / LAYOUT_BASE_WIDTH; +const s = (px: number) => Math.round(px * LAYOUT_SCALE); const CertificateWinnerPage: FC = (): ReactElement => { const { certId } = useParams<{ certId: string }>(); @@ -87,7 +100,12 @@ const CertificateWinnerPage: FC = (): ReactElement => { !!decodedTeamId ); + const { data: submissionData, isLoading: isLoadingSubmission } = + useTeamSubmission(decodedTeamId, !!decodedTeamId); + const team = teamData?.data; + const submission = submissionData?.data; + const submissionName = submission?.project_name || '(Submission unavailable)'; const memberNames = useMemo(() => { const members = team?.members || []; @@ -112,7 +130,7 @@ const CertificateWinnerPage: FC = (): ReactElement => { const certificateUrl = `${window.location.origin}/certificate/winner/${encodedCertId}`; QRCode.toDataURL(certificateUrl, { - width: 200, + width: s(200), margin: 1, color: { dark: '#000000', @@ -130,13 +148,15 @@ const CertificateWinnerPage: FC = (): ReactElement => { if (!team?.name) return; if (!qrCodeUrl) return; if (!winnerEntry?.rank) return; + if (isLoadingSubmission) return; setIsGenerating(true); try { + setShowTemplate(true); await new Promise((resolve) => setTimeout(resolve, 1500)); const canvas = await html2canvas(certificateRef.current, { - scale: 4, + scale: EXPORT_SCALE, useCORS: true, backgroundColor: '#ffffff', logging: false, @@ -158,7 +178,14 @@ const CertificateWinnerPage: FC = (): ReactElement => { }; generateCertificate(); - }, [team?.name, memberNames, qrCodeUrl, winnerEntry?.rank]); + }, [ + team?.name, + memberNames, + qrCodeUrl, + winnerEntry?.rank, + isLoadingSubmission, + submissionName, + ]); const handleDownloadCertificate = () => { if (!certificateImage) return; @@ -323,7 +350,8 @@ const CertificateWinnerPage: FC = (): ReactElement => { style={{ position: 'relative', backgroundImage: 'url(/images/blank_winner_cert.svg)', - backgroundSize: 'cover', + backgroundSize: '100% 100%', + backgroundRepeat: 'no-repeat', backgroundPosition: 'center', width: `${CERT_WIDTH}px`, height: `${CERT_HEIGHT}px`, @@ -339,7 +367,7 @@ const CertificateWinnerPage: FC = (): ReactElement => {