From 8d439b114385df851f7c74dd02026d003e8293ef Mon Sep 17 00:00:00 2001 From: dev-minsoo Date: Fri, 24 Apr 2026 17:05:15 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20synced=20=EB=B0=B0=EC=A7=80=20=EC=84=B1?= =?UTF-8?q?=EA=B3=B5=20=EC=8B=9C=20=EC=BB=A8=ED=8E=98=ED=8B=B0=20=ED=9A=A8?= =?UTF-8?q?=EA=B3=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + src/adapters/confetti.ts | 103 ++++++++++++++++++++++++++++++ src/adapters/leetcode/index.ts | 6 ++ src/adapters/programmers/index.ts | 6 ++ 4 files changed, 116 insertions(+) create mode 100644 src/adapters/confetti.ts diff --git a/.gitignore b/.gitignore index a547bf3..7ffa07c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +AGENTS.md diff --git a/src/adapters/confetti.ts b/src/adapters/confetti.ts new file mode 100644 index 0000000..219f235 --- /dev/null +++ b/src/adapters/confetti.ts @@ -0,0 +1,103 @@ +const COLORS = ["#22c55e", "#14b8a6", "#3b82f6", "#f59e0b", "#facc15", "#fb7185"]; +const CONTAINER_ID = "algorithmhub-confetti-container"; + +type ConfettiPieceState = { + element: HTMLSpanElement; + angle: number; + distance: number; + rise: number; + drift: number; + rotationStart: number; + rotationVelocity: number; +}; + +function randomBetween(min: number, max: number) { + return Math.random() * (max - min) + min; +} + +function getContainer() { + let container = document.getElementById(CONTAINER_ID); + + if (container) { + return container; + } + + container = document.createElement("div"); + container.id = CONTAINER_ID; + container.style.position = "fixed"; + container.style.inset = "0"; + container.style.pointerEvents = "none"; + container.style.overflow = "hidden"; + container.style.zIndex = "2147483646"; + document.body.appendChild(container); + return container; +} + +export function burstConfetti(origin?: { x: number; y: number }) { + const container = getContainer(); + const width = window.innerWidth; + const startX = origin?.x ?? width / 2; + const startY = + origin?.y ?? Math.min(window.innerHeight * 0.24, 156); + const pieces: ConfettiPieceState[] = []; + const total = 20; + + for (let index = 0; index < total; index += 1) { + const piece = document.createElement("span"); + piece.style.position = "absolute"; + piece.style.left = `${startX}px`; + piece.style.top = `${startY}px`; + const shape = index % 3; + piece.style.width = `${shape === 1 ? randomBetween(7, 9) : randomBetween(5, 10)}px`; + piece.style.height = `${shape === 2 ? randomBetween(6, 8) : randomBetween(10, 16)}px`; + piece.style.borderRadius = shape === 2 ? "999px" : `${randomBetween(1, 3)}px`; + piece.style.background = COLORS[index % COLORS.length] ?? "#22c55e"; + piece.style.opacity = "1"; + piece.style.transform = `translate(0px, 0px) rotate(${randomBetween(0, 360)}deg)`; + piece.style.willChange = "transform, opacity"; + container.appendChild(piece); + pieces.push({ + element: piece, + angle: ((index / total) * Math.PI * 2) + randomBetween(-0.2, 0.2), + distance: randomBetween(80, 150), + rise: randomBetween(18, 42), + drift: randomBetween(110, 210), + rotationStart: randomBetween(0, 360), + rotationVelocity: randomBetween(260, 760), + }); + } + + const startAt = performance.now(); + const duration = 1100; + + const animate = (now: number) => { + const elapsed = now - startAt; + const progress = Math.min(elapsed / duration, 1); + const burstProgress = Math.min(progress / 0.45, 1); + const fallProgress = Math.max((progress - 0.2) / 0.8, 0); + + pieces.forEach((piece) => { + const spreadDistance = piece.distance * (1 - Math.pow(1 - burstProgress, 2)); + const horizontal = Math.cos(piece.angle) * spreadDistance; + const upward = Math.sin(piece.angle) * spreadDistance - piece.rise; + const gravityDrop = Math.pow(fallProgress, 2) * piece.drift; + const vertical = upward + gravityDrop; + const rotation = piece.rotationStart + progress * piece.rotationVelocity; + piece.element.style.transform = + `translate(${horizontal}px, ${vertical}px) rotate(${rotation}deg)`; + piece.element.style.opacity = String(1 - progress); + }); + + if (progress < 1) { + window.requestAnimationFrame(animate); + return; + } + + pieces.forEach((piece) => piece.element.remove()); + if (!container.childElementCount) { + container.remove(); + } + }; + + window.requestAnimationFrame(animate); +} diff --git a/src/adapters/leetcode/index.ts b/src/adapters/leetcode/index.ts index 41c192d..e418764 100644 --- a/src/adapters/leetcode/index.ts +++ b/src/adapters/leetcode/index.ts @@ -9,6 +9,7 @@ import type { RuntimeMessageResponse } from "../../core/types/messages"; import type { ExtensionSettings } from "../../core/types/domain"; import type { UploadJob } from "../../core/types/upload"; import type { PlatformAdapter } from "../types"; +import { burstConfetti } from "../confetti"; import { uploadThroughBackground } from "../upload"; const SUBMIT_BUTTON_SELECTOR = '[data-e2e-locator="console-submit-button"]'; @@ -243,6 +244,11 @@ function setInlineStatus( } if (tone === "success") { + const rect = marker.getBoundingClientRect(); + burstConfetti({ + x: rect.left + rect.width / 2, + y: Math.max(48, rect.top - 28), + }); marker.style.background = "#052e16"; marker.style.color = "#bbf7d0"; return; diff --git a/src/adapters/programmers/index.ts b/src/adapters/programmers/index.ts index 0b423f9..f76ab06 100644 --- a/src/adapters/programmers/index.ts +++ b/src/adapters/programmers/index.ts @@ -9,6 +9,7 @@ import type { ExtensionSettings } from "../../core/types/domain"; import type { RuntimeMessageResponse } from "../../core/types/messages"; import type { UploadJob } from "../../core/types/upload"; import type { PlatformAdapter } from "../types"; +import { burstConfetti } from "../confetti"; import { uploadThroughBackground } from "../upload"; const STATUS_MARKER_ID = "algorithmhub-programmers-status-marker"; @@ -241,6 +242,11 @@ function setInlineStatus( } if (tone === "success") { + const rect = marker.getBoundingClientRect(); + burstConfetti({ + x: rect.left + rect.width / 2, + y: Math.max(48, rect.top - 28), + }); marker.style.background = "#052e16"; marker.style.color = "#bbf7d0"; return;