Compare commits

...
Author SHA1 Message Date
Hafid Nur 5f9a537c20 Merge branch 'develop' into hackathon/update 2025-12-16 13:56:05 +07:00
Hafid Nur 040f414e6d feat(hackathon): update certificate
- add submission project name to the certificate
2025-12-16 13:52:55 +07:00
Hafid Nur 3eb86c6ef4 feat(hackathon): refine winner certificate 2025-12-16 12:06:29 +07:00
Hafid Nur 48d97af044 feat(hackathon): draft winner certificate 2025-12-16 11:26:00 +07:00
Hafid Nur 36f7d10e7a update hackathon landing page 2025-12-15 14:40:49 +07:00
2 changed files with 54 additions and 254 deletions
@@ -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"
}
]
}
@@ -7,6 +7,7 @@ import {
useAuthStore, useAuthStore,
useMyTeams, useMyTeams,
useTeamById, useTeamById,
useTeamSubmission,
useWinners, useWinners,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service';
import QRCode from 'qrcode'; import QRCode from 'qrcode';
@@ -37,8 +38,20 @@ const formatOrdinalRank = (rank: number): string => {
} }
}; };
const CERT_WIDTH = 1000; // Keep the template size aligned with the SVG native size (842x595) using an integer multiplier.
const CERT_HEIGHT = (CERT_WIDTH * 595) / 842; // matches blank_winner_cert.svg aspect ratio // 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 CertificateWinnerPage: FC = (): ReactElement => {
const { certId } = useParams<{ certId: string }>(); const { certId } = useParams<{ certId: string }>();
@@ -87,7 +100,12 @@ const CertificateWinnerPage: FC = (): ReactElement => {
!!decodedTeamId !!decodedTeamId
); );
const { data: submissionData, isLoading: isLoadingSubmission } =
useTeamSubmission(decodedTeamId, !!decodedTeamId);
const team = teamData?.data; const team = teamData?.data;
const submission = submissionData?.data;
const submissionName = submission?.project_name || '(Submission unavailable)';
const memberNames = useMemo(() => { const memberNames = useMemo(() => {
const members = team?.members || []; const members = team?.members || [];
@@ -112,7 +130,7 @@ const CertificateWinnerPage: FC = (): ReactElement => {
const certificateUrl = `${window.location.origin}/certificate/winner/${encodedCertId}`; const certificateUrl = `${window.location.origin}/certificate/winner/${encodedCertId}`;
QRCode.toDataURL(certificateUrl, { QRCode.toDataURL(certificateUrl, {
width: 200, width: s(200),
margin: 1, margin: 1,
color: { color: {
dark: '#000000', dark: '#000000',
@@ -130,13 +148,15 @@ const CertificateWinnerPage: FC = (): ReactElement => {
if (!team?.name) return; if (!team?.name) return;
if (!qrCodeUrl) return; if (!qrCodeUrl) return;
if (!winnerEntry?.rank) return; if (!winnerEntry?.rank) return;
if (isLoadingSubmission) return;
setIsGenerating(true); setIsGenerating(true);
try { try {
setShowTemplate(true);
await new Promise((resolve) => setTimeout(resolve, 1500)); await new Promise((resolve) => setTimeout(resolve, 1500));
const canvas = await html2canvas(certificateRef.current, { const canvas = await html2canvas(certificateRef.current, {
scale: 4, scale: EXPORT_SCALE,
useCORS: true, useCORS: true,
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
logging: false, logging: false,
@@ -158,7 +178,14 @@ const CertificateWinnerPage: FC = (): ReactElement => {
}; };
generateCertificate(); generateCertificate();
}, [team?.name, memberNames, qrCodeUrl, winnerEntry?.rank]); }, [
team?.name,
memberNames,
qrCodeUrl,
winnerEntry?.rank,
isLoadingSubmission,
submissionName,
]);
const handleDownloadCertificate = () => { const handleDownloadCertificate = () => {
if (!certificateImage) return; if (!certificateImage) return;
@@ -323,7 +350,8 @@ const CertificateWinnerPage: FC = (): ReactElement => {
style={{ style={{
position: 'relative', position: 'relative',
backgroundImage: 'url(/images/blank_winner_cert.svg)', backgroundImage: 'url(/images/blank_winner_cert.svg)',
backgroundSize: 'cover', backgroundSize: '100% 100%',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center', backgroundPosition: 'center',
width: `${CERT_WIDTH}px`, width: `${CERT_WIDTH}px`,
height: `${CERT_HEIGHT}px`, height: `${CERT_HEIGHT}px`,
@@ -339,7 +367,7 @@ const CertificateWinnerPage: FC = (): ReactElement => {
<div <div
style={{ style={{
position: 'absolute', position: 'absolute',
top: '37%', top: '35%',
left: '3.5%', left: '3.5%',
width: '55%', width: '55%',
}} }}
@@ -350,7 +378,7 @@ const CertificateWinnerPage: FC = (): ReactElement => {
fontWeight: 'bold', fontWeight: 'bold',
color: '#59bef5', color: '#59bef5',
textAlign: 'left', textAlign: 'left',
fontSize: '28px', fontSize: `${s(28)}px`,
lineHeight: '1.2', lineHeight: '1.2',
wordBreak: 'break-word', wordBreak: 'break-word',
margin: 0, margin: 0,
@@ -364,7 +392,7 @@ const CertificateWinnerPage: FC = (): ReactElement => {
<div <div
style={{ style={{
position: 'absolute', position: 'absolute',
top: '42.5%', top: '40.5%',
left: '3.5%', left: '3.5%',
width: '55%', width: '55%',
}} }}
@@ -374,7 +402,7 @@ const CertificateWinnerPage: FC = (): ReactElement => {
style={{ style={{
margin: 0, margin: 0,
fontFamily: 'Poppins, sans-serif', fontFamily: 'Poppins, sans-serif',
fontSize: '18px', fontSize: `${s(18)}px`,
lineHeight: '1.35', lineHeight: '1.35',
color: '#59bef5', color: '#59bef5',
}} }}
@@ -392,7 +420,7 @@ const CertificateWinnerPage: FC = (): ReactElement => {
<div <div
style={{ style={{
position: 'absolute', position: 'absolute',
top: '62%', top: '60%',
left: '3.5%', left: '3.5%',
width: '60%', width: '60%',
}} }}
@@ -401,7 +429,7 @@ const CertificateWinnerPage: FC = (): ReactElement => {
style={{ style={{
margin: 0, margin: 0,
fontFamily: 'Poppins, sans-serif', fontFamily: 'Poppins, sans-serif',
fontSize: '18px', fontSize: `${s(18)}px`,
lineHeight: '1.35', lineHeight: '1.35',
color: '#6B6B6B', color: '#6B6B6B',
}} }}
@@ -410,6 +438,10 @@ const CertificateWinnerPage: FC = (): ReactElement => {
<br /> <br />
<b>JUARA {winnerEntry.rank}</b> pada Hackathon IMPHNEN x <b>JUARA {winnerEntry.rank}</b> pada Hackathon IMPHNEN x
Kolosal.ai Kolosal.ai
<br />
<span>
dengan nama project: <b>{submissionName}</b>
</span>
</p> </p>
</div> </div>
@@ -420,7 +452,7 @@ const CertificateWinnerPage: FC = (): ReactElement => {
position: 'absolute', position: 'absolute',
top: '8%', top: '8%',
right: '8.5%', right: '8.5%',
width: '190px', width: `${s(190)}px`,
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
}} }}
@@ -430,12 +462,12 @@ const CertificateWinnerPage: FC = (): ReactElement => {
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
padding: '6px 16px', padding: `${s(6)}px ${s(16)}px`,
textAlign: 'center', textAlign: 'center',
fontFamily: 'Poppins, sans-serif', fontFamily: 'Poppins, sans-serif',
fontWeight: 700, fontWeight: 700,
color: '#78350F', color: '#78350F',
fontSize: '24px', fontSize: `${s(24)}px`,
lineHeight: '1', lineHeight: '1',
}} }}
> >
@@ -450,8 +482,8 @@ const CertificateWinnerPage: FC = (): ReactElement => {
position: 'absolute', position: 'absolute',
top: '33%', top: '33%',
right: '9.3%', right: '9.3%',
width: '190px', width: `${s(190)}px`,
height: '190px', height: `${s(190)}px`,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@@ -461,7 +493,11 @@ const CertificateWinnerPage: FC = (): ReactElement => {
<img <img
src={qrCodeUrl} src={qrCodeUrl}
alt="Certificate QR Code" alt="Certificate QR Code"
style={{ width: '190px', height: '190px', display: 'block' }} style={{
width: `${s(190)}px`,
height: `${s(190)}px`,
display: 'block',
}}
/> />
)} )}
</div> </div>