From 142667963d90a0752c2a74d50391e836bee15f7a Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 9 Aug 2026 09:30:32 +0700 Subject: [PATCH 01/14] Hide cookie notice on embed widget route --- components/privacy/cookie-consent.tsx | 241 ++------------------------ 1 file changed, 19 insertions(+), 222 deletions(-) diff --git a/components/privacy/cookie-consent.tsx b/components/privacy/cookie-consent.tsx index 633ea04aa..c8dffc264 100644 --- a/components/privacy/cookie-consent.tsx +++ b/components/privacy/cookie-consent.tsx @@ -12,228 +12,25 @@ const COOKIE_NOTICE_STORAGE_KEY = "arcade-cookie-notice-v1" const COOKIE_NOTICE_PREVIEW_STORAGE_KEY = "arcade-cookie-notice-preview-v1" const COPY = { - en: { - eyebrow: "Cookie information", - title: "We use cookies and analytics", - description: - "Essential storage supports core features. Google Analytics measures anonymous usage so we can improve the service.", - usageTitle: "Cookie usage details", - essentialTitle: "Essential storage", - essentialStatus: "Always active", - essentialDescription: - "Stores interface preferences such as theme, language, and recent calculator results on your device.", - analyticsTitle: "Google Analytics", - analyticsStatus: "Enabled", - analyticsPreviewStatus: "Disabled in preview", - analyticsDescription: - "Collects aggregated information such as page visits and browser or device details. It does not provide access to your Google account or private profile data.", - preview: - "Preview mode: this notice is displayed for review, but Google Analytics is not loaded on the preview URL.", - moreTitle: "More information", - moreDescription: - "Read the Privacy Policy for details about stored information, third-party services, and contact options.", - privacy: "Privacy Policy", - showDetails: "View details", - hideDetails: "Hide details", - acknowledge: "I understand", - close: "Close cookie information", - }, - vi: { - eyebrow: "Thông tin cookie", - title: "Chúng tôi sử dụng cookie và analytics", - description: - "Bộ nhớ thiết yếu hỗ trợ chức năng chính. Google Analytics đo lường dữ liệu sử dụng ẩn danh để cải thiện dịch vụ.", - usageTitle: "Chi tiết sử dụng cookie", - essentialTitle: "Bộ nhớ thiết yếu", - essentialStatus: "Luôn hoạt động", - essentialDescription: - "Lưu tùy chọn giao diện như chủ đề, ngôn ngữ và kết quả tính gần đây trên thiết bị của bạn.", - analyticsTitle: "Google Analytics", - analyticsStatus: "Đang bật", - analyticsPreviewStatus: "Tắt trong preview", - analyticsDescription: - "Thu thập dữ liệu tổng hợp như lượt xem trang và thông tin trình duyệt hoặc thiết bị. Dịch vụ này không truy cập tài khoản Google hay dữ liệu hồ sơ riêng tư của bạn.", - preview: - "Chế độ xem trước: banner được hiển thị để kiểm tra giao diện nhưng Google Analytics không được tải tại URL preview.", - moreTitle: "Thông tin thêm", - moreDescription: - "Xem Chính sách quyền riêng tư để biết chi tiết về dữ liệu được lưu, dịch vụ bên thứ ba và phương thức liên hệ.", - privacy: "Chính sách quyền riêng tư", - showDetails: "Xem chi tiết", - hideDetails: "Thu gọn", - acknowledge: "Tôi đã hiểu", - close: "Đóng thông tin cookie", - }, + en: { eyebrow:"Cookie information", title:"We use cookies and analytics", description:"Essential storage supports core features. Google Analytics measures anonymous usage so we can improve the service.", usageTitle:"Cookie usage details", essentialTitle:"Essential storage", essentialStatus:"Always active", essentialDescription:"Stores interface preferences such as theme, language, and recent calculator results on your device.", analyticsTitle:"Google Analytics", analyticsStatus:"Enabled", analyticsPreviewStatus:"Disabled in preview", analyticsDescription:"Collects aggregated information such as page visits and browser or device details. It does not provide access to your Google account or private profile data.", preview:"Preview mode: this notice is displayed for review, but Google Analytics is not loaded on the preview URL.", moreTitle:"More information", moreDescription:"Read the Privacy Policy for details about stored information, third-party services, and contact options.", privacy:"Privacy Policy", showDetails:"View details", hideDetails:"Hide details", acknowledge:"I understand", close:"Close cookie information" }, + vi: { eyebrow:"Thông tin cookie", title:"Chúng tôi sử dụng cookie và analytics", description:"Bộ nhớ thiết yếu hỗ trợ chức năng chính. Google Analytics đo lường dữ liệu sử dụng ẩn danh để cải thiện dịch vụ.", usageTitle:"Chi tiết sử dụng cookie", essentialTitle:"Bộ nhớ thiết yếu", essentialStatus:"Luôn hoạt động", essentialDescription:"Lưu tùy chọn giao diện như chủ đề, ngôn ngữ và kết quả tính gần đây trên thiết bị của bạn.", analyticsTitle:"Google Analytics", analyticsStatus:"Đang bật", analyticsPreviewStatus:"Tắt trong preview", analyticsDescription:"Thu thập dữ liệu tổng hợp như lượt xem trang và thông tin trình duyệt hoặc thiết bị. Dịch vụ này không truy cập tài khoản Google hay dữ liệu hồ sơ riêng tư của bạn.", preview:"Chế độ xem trước: banner được hiển thị để kiểm tra giao diện nhưng Google Analytics không được tải tại URL preview.", moreTitle:"Thông tin thêm", moreDescription:"Xem Chính sách quyền riêng tư để biết chi tiết về dữ liệu được lưu, dịch vụ bên thứ ba và phương thức liên hệ.", privacy:"Chính sách quyền riêng tư", showDetails:"Xem chi tiết", hideDetails:"Thu gọn", acknowledge:"Tôi đã hiểu", close:"Đóng thông tin cookie" }, } as const -function readAcknowledgement(storageKey: string): boolean { - try { - return window.localStorage.getItem(storageKey) === "acknowledged" - } catch { - return false - } -} - -function storeAcknowledgement(storageKey: string): void { - try { - window.localStorage.setItem(storageKey, "acknowledged") - } catch { - // The notice can still be closed for the current page if storage is blocked. - } -} - -type CookieConsentProps = { - analyticsEnabled: boolean - previewMode?: boolean -} - -export default function CookieConsent({ - analyticsEnabled, - previewMode = false, -}: CookieConsentProps) { - const pathname = usePathname() - const [isOpen, setIsOpen] = useState(false) - const [showDetails, setShowDetails] = useState(false) - const copy = useMemo( - () => - getWebsiteLocaleFromPathname(pathname ?? "/") === "vi" - ? COPY.vi - : COPY.en, - [pathname], - ) - const storageKey = previewMode - ? COOKIE_NOTICE_PREVIEW_STORAGE_KEY - : COOKIE_NOTICE_STORAGE_KEY - const noticeEnabled = analyticsEnabled || previewMode - - useEffect(() => { - if (!noticeEnabled) return - - setShowDetails(false) - setIsOpen(previewMode || !readAcknowledgement(storageKey)) - - const openNotice = () => { - setShowDetails(false) - setIsOpen(true) - } - window.addEventListener(COOKIE_PREFERENCES_EVENT, openNotice) - - return () => { - window.removeEventListener(COOKIE_PREFERENCES_EVENT, openNotice) - } - }, [noticeEnabled, previewMode, storageKey]) - - function acknowledgeNotice() { - storeAcknowledgement(storageKey) - setShowDetails(false) - setIsOpen(false) - } - - if (!noticeEnabled || !isOpen) return null - - return ( -
-
-
- - -
- {copy.eyebrow} - - -
- - - -
- - -
-
- - {showDetails ? ( - - ) : null} -
-
- ) +function readAcknowledgement(storageKey:string){try{return window.localStorage.getItem(storageKey)==="acknowledged"}catch{return false}} +function storeAcknowledgement(storageKey:string){try{window.localStorage.setItem(storageKey,"acknowledged")}catch{}} + +type CookieConsentProps={analyticsEnabled:boolean;previewMode?:boolean} + +export default function CookieConsent({analyticsEnabled,previewMode=false}:CookieConsentProps){ + const pathname=usePathname() + const [isOpen,setIsOpen]=useState(false) + const [showDetails,setShowDetails]=useState(false) + const isEmbedWidget=pathname === "/widget" || pathname?.endsWith("/widget") || pathname?.endsWith("/widget/") + const copy=useMemo(()=>getWebsiteLocaleFromPathname(pathname??"/")==="vi"?COPY.vi:COPY.en,[pathname]) + const storageKey=previewMode?COOKIE_NOTICE_PREVIEW_STORAGE_KEY:COOKIE_NOTICE_STORAGE_KEY + const noticeEnabled=(analyticsEnabled||previewMode)&&!isEmbedWidget + useEffect(()=>{if(!noticeEnabled){setIsOpen(false);return}setShowDetails(false);setIsOpen(previewMode||!readAcknowledgement(storageKey));const openNotice=()=>{setShowDetails(false);setIsOpen(true)};window.addEventListener(COOKIE_PREFERENCES_EVENT,openNotice);return()=>window.removeEventListener(COOKIE_PREFERENCES_EVENT,openNotice)},[noticeEnabled,previewMode,storageKey]) + function acknowledgeNotice(){storeAcknowledgement(storageKey);setShowDetails(false);setIsOpen(false)} + if(!noticeEnabled||!isOpen)return null + return
{copy.eyebrow}
{showDetails?:null}
} From f9dca23cebae369446008bf58e922fb82a9f891f Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 9 Aug 2026 09:30:45 +0700 Subject: [PATCH 02/14] Redesign compact embed widget --- components/arcade/arcade-embed-widget.tsx | 64 ++++++----------------- 1 file changed, 17 insertions(+), 47 deletions(-) diff --git a/components/arcade/arcade-embed-widget.tsx b/components/arcade/arcade-embed-widget.tsx index 4866320b6..4b6af13ed 100644 --- a/components/arcade/arcade-embed-widget.tsx +++ b/components/arcade/arcade-embed-widget.tsx @@ -1,6 +1,6 @@ "use client" -import { Calendar, ExternalLink, Gamepad2, GraduationCap, Trophy } from "lucide-react" +import { ExternalLink, Gamepad2, GraduationCap, Sparkles, Trophy } from "lucide-react" import { useMemo } from "react" function sanitizeUtm(value: string | null, fallback: string): string { @@ -10,61 +10,31 @@ function sanitizeUtm(value: string | null, fallback: string): string { export default function ArcadeEmbedWidget() { const ctaUrl = useMemo(() => { - if (typeof window === "undefined") { - return "https://arcade.eplus.dev/?utm_source=hashnode&utm_medium=widget" - } - + if (typeof window === "undefined") return "https://arcade.eplus.dev/?utm_source=hashnode&utm_medium=widget" const params = new URLSearchParams(window.location.search) - const source = sanitizeUtm(params.get("utm_source"), "hashnode") - const medium = sanitizeUtm(params.get("utm_medium"), "widget") - const campaign = sanitizeUtm(params.get("utm_campaign"), "arcade-widget") - const target = new URL("https://arcade.eplus.dev/") - target.searchParams.set("utm_source", source) - target.searchParams.set("utm_medium", medium) - target.searchParams.set("utm_campaign", campaign) - + target.searchParams.set("utm_source", sanitizeUtm(params.get("utm_source"), "hashnode")) + target.searchParams.set("utm_medium", sanitizeUtm(params.get("utm_medium"), "widget")) + target.searchParams.set("utm_campaign", sanitizeUtm(params.get("utm_campaign"), "arcade-widget")) return target.toString() }, []) return (
-
-
-
- -
- ARCADE POINTS - by ePlus.DEV -
-
- FREE -
- -
-
-

Google Cloud Arcade Tracker

-

Track your Arcade progress in one place.

-

- Calculate points, follow milestones, discover monthly games and keep up with Facilitator bonuses. -

-
- -
+
Check your score
+
) } From 9a552d5b6acc175f5a51a1a6f02d70490083f816 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 9 Aug 2026 09:31:03 +0700 Subject: [PATCH 03/14] Polish responsive widget presentation --- app/widget/widget.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/widget/widget.css b/app/widget/widget.css index dab0eb3aa..80ed3c266 100644 --- a/app/widget/widget.css +++ b/app/widget/widget.css @@ -1 +1 @@ -html,body{margin:0!important;min-height:100%;background:transparent!important}.arcade-embed-shell{box-sizing:border-box;min-height:100vh;padding:10px;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#eef4ff}.arcade-embed-card{position:relative;overflow:hidden;display:grid;gap:16px;max-width:980px;margin:0 auto;padding:20px;border:1px solid rgba(139,92,246,.32);border-radius:20px;background:radial-gradient(circle at 92% 8%,rgba(34,211,238,.16),transparent 34%),radial-gradient(circle at 8% 100%,rgba(168,85,247,.18),transparent 36%),linear-gradient(145deg,#081225,#0d1730 55%,#11102b);box-shadow:0 18px 44px rgba(3,8,20,.34)}.arcade-embed-card::after{content:"";position:absolute;inset:auto -18% -65% 35%;height:190px;border-radius:50%;background:rgba(124,58,237,.22);filter:blur(46px);pointer-events:none}.arcade-embed-brand-row,.arcade-embed-footer{position:relative;z-index:1;display:flex;align-items:center;justify-content:space-between;gap:14px}.arcade-embed-brand{display:flex;align-items:center;gap:10px}.arcade-embed-logo{display:grid;place-items:center;width:42px;height:42px;border:1px solid rgba(255,213,72,.34);border-radius:13px;background:linear-gradient(145deg,#3d2b75,#1f5ca8);color:#ffd548}.arcade-embed-logo svg{width:24px;height:24px}.arcade-embed-brand div{display:grid;gap:1px}.arcade-embed-brand strong{font-size:.9rem;letter-spacing:.06em}.arcade-embed-brand span{color:#8fa2bf;font-size:.67rem}.arcade-embed-badge{padding:5px 8px;border:1px solid rgba(168,85,247,.44);border-radius:999px;background:rgba(124,58,237,.16);color:#c9a7ff;font-size:.62rem;font-weight:800;letter-spacing:.08em}.arcade-embed-copy{position:relative;z-index:1;display:grid;grid-template-columns:minmax(0,1.3fr) minmax(240px,.7fr);gap:24px;align-items:end}.arcade-embed-eyebrow{margin:0 0 6px;color:#8ab4ff!important;font-size:.68rem!important;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.arcade-embed-copy h1{max-width:680px;margin:0;color:#fff;font-size:clamp(1.25rem,3vw,2rem);line-height:1.08}.arcade-embed-copy p{max-width:680px;margin:8px 0 0;color:#9eb0c9;font-size:.78rem;line-height:1.55}.arcade-embed-features{display:grid;grid-template-columns:1fr;gap:8px}.arcade-embed-features span{display:flex;align-items:center;gap:8px;padding:8px 10px;border:1px solid rgba(148,163,184,.14);border-radius:10px;background:rgba(7,17,36,.52);color:#dbe7f8;font-size:.72rem}.arcade-embed-features svg{width:15px;height:15px;color:#b88cff;flex:0 0 auto}.arcade-embed-footer{padding-top:13px;border-top:1px solid rgba(148,163,184,.13)}.arcade-embed-footer>span{color:#71839f;font-size:.68rem}.arcade-embed-footer a{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:10px 14px;border:1px solid rgba(139,92,246,.48);border-radius:11px;background:linear-gradient(90deg,#6d28d9,#7c3aed 54%,#2563eb);color:white;text-decoration:none;font-size:.74rem;font-weight:800;box-shadow:0 8px 24px rgba(91,33,182,.24);transition:transform .16s ease,filter .16s ease}.arcade-embed-footer a:hover{transform:translateY(-1px);filter:brightness(1.08)}.arcade-embed-footer a svg{width:14px;height:14px}@media(max-width:640px){.arcade-embed-shell{padding:6px}.arcade-embed-card{gap:13px;padding:16px;border-radius:16px}.arcade-embed-copy{grid-template-columns:1fr;gap:14px}.arcade-embed-features{grid-template-columns:repeat(3,minmax(0,1fr));gap:6px}.arcade-embed-features span{justify-content:center;padding:8px 6px;text-align:center;font-size:.63rem}.arcade-embed-footer{align-items:stretch;flex-direction:column}.arcade-embed-footer>span{display:none}.arcade-embed-footer a{width:auto}.arcade-embed-copy h1{font-size:1.28rem}.arcade-embed-copy p{font-size:.74rem}}@media(max-width:390px){.arcade-embed-features{grid-template-columns:1fr}.arcade-embed-features span{justify-content:flex-start}.arcade-embed-brand strong{font-size:.82rem}.arcade-embed-badge{font-size:.58rem}}@media(prefers-color-scheme:light){.arcade-embed-shell{color:#14213a}.arcade-embed-card{border-color:rgba(99,102,241,.22);background:radial-gradient(circle at 92% 8%,rgba(34,211,238,.13),transparent 34%),radial-gradient(circle at 8% 100%,rgba(168,85,247,.13),transparent 36%),linear-gradient(145deg,#fff,#f7f8ff 58%,#f5f3ff);box-shadow:0 18px 42px rgba(73,85,120,.16)}.arcade-embed-brand strong,.arcade-embed-copy h1{color:#111827}.arcade-embed-brand span,.arcade-embed-copy p{color:#64748b}.arcade-embed-features span{border-color:rgba(99,102,241,.12);background:rgba(255,255,255,.74);color:#334155}.arcade-embed-footer{border-color:rgba(99,102,241,.12)}.arcade-embed-footer>span{color:#7c8ca5}} \ No newline at end of file +html,body{margin:0!important;min-height:100%;background:transparent!important}.arcade-embed-shell{box-sizing:border-box;width:100%;padding:8px;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.arcade-embed-card{box-sizing:border-box;position:relative;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:18px;width:100%;max-width:980px;margin:0 auto;padding:20px 22px;overflow:hidden;border:1px solid rgba(139,92,246,.28);border-radius:18px;background:radial-gradient(circle at 0 0,rgba(124,58,237,.18),transparent 32%),linear-gradient(135deg,#091327,#0b1730 62%,#11102b);box-shadow:0 12px 32px rgba(2,8,23,.22);color:#eef4ff;text-decoration:none}.arcade-embed-card:before{content:"";position:absolute;right:-55px;bottom:-85px;width:210px;height:160px;border-radius:50%;background:rgba(37,99,235,.18);filter:blur(42px);pointer-events:none}.arcade-embed-icon{position:relative;z-index:1;display:grid;place-items:center;width:58px;height:58px;border-radius:16px;background:linear-gradient(145deg,#7c3aed,#2563eb);box-shadow:0 8px 24px rgba(79,70,229,.28);color:#fff}.arcade-embed-icon svg{width:30px;height:30px}.arcade-embed-content{position:relative;z-index:1;min-width:0}.arcade-embed-brand{display:flex;align-items:baseline;gap:7px;margin-bottom:4px}.arcade-embed-brand strong{font-size:.72rem;letter-spacing:.09em;color:#c4b5fd}.arcade-embed-brand span{font-size:.64rem;color:#71839f}.arcade-embed-content h2{margin:0;color:#fff;font-size:1.22rem;line-height:1.2;letter-spacing:-.02em}.arcade-embed-content p{margin:5px 0 10px;color:#9fb0c8;font-size:.75rem;line-height:1.45}.arcade-embed-features{display:flex;align-items:center;gap:14px;flex-wrap:wrap}.arcade-embed-features span{display:inline-flex;align-items:center;gap:5px;color:#c9d5e6;font-size:.67rem;font-weight:600}.arcade-embed-features svg{width:13px;height:13px;color:#a78bfa}.arcade-embed-cta{position:relative;z-index:1;display:flex;align-items:center;gap:8px;white-space:nowrap;padding:11px 14px;border-radius:11px;background:rgba(124,58,237,.15);border:1px solid rgba(167,139,250,.3);color:#ddd6fe;font-size:.72rem;font-weight:800;transition:background .16s ease,transform .16s ease}.arcade-embed-cta svg{width:14px;height:14px}.arcade-embed-card:hover .arcade-embed-cta{background:rgba(124,58,237,.26);transform:translateY(-1px)}@media(max-width:640px){.arcade-embed-shell{padding:6px}.arcade-embed-card{grid-template-columns:46px minmax(0,1fr);gap:12px;padding:15px;border-radius:15px}.arcade-embed-icon{width:46px;height:46px;border-radius:13px;align-self:start}.arcade-embed-icon svg{width:24px;height:24px}.arcade-embed-content h2{font-size:1rem}.arcade-embed-content p{font-size:.69rem;margin:4px 0 9px}.arcade-embed-brand strong{font-size:.65rem}.arcade-embed-brand span{font-size:.58rem}.arcade-embed-features{gap:8px 11px}.arcade-embed-features span{font-size:.61rem}.arcade-embed-cta{grid-column:1/-1;justify-content:center;padding:10px 12px;font-size:.69rem}}@media(max-width:390px){.arcade-embed-card{grid-template-columns:40px minmax(0,1fr);padding:13px 12px}.arcade-embed-icon{width:40px;height:40px}.arcade-embed-content p{display:none}.arcade-embed-features{margin-top:8px}.arcade-embed-features span{font-size:.58rem}}@media(prefers-color-scheme:light){.arcade-embed-card{border-color:rgba(99,102,241,.18);background:radial-gradient(circle at 0 0,rgba(124,58,237,.1),transparent 34%),linear-gradient(135deg,#fff,#f8faff 70%,#f5f3ff);box-shadow:0 10px 28px rgba(71,85,105,.12);color:#172033}.arcade-embed-content h2{color:#172033}.arcade-embed-content p{color:#64748b}.arcade-embed-brand span{color:#94a3b8}.arcade-embed-features span{color:#475569}.arcade-embed-cta{background:#f5f3ff;border-color:#ddd6fe;color:#6d28d9}.arcade-embed-card:hover .arcade-embed-cta{background:#ede9fe}} From aac65f01ff439052decf27fed8ec980071c207ef Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 9 Aug 2026 09:50:08 +0700 Subject: [PATCH 04/14] Allow widget CTA target URL from query string --- components/arcade/arcade-embed-widget.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/components/arcade/arcade-embed-widget.tsx b/components/arcade/arcade-embed-widget.tsx index 4b6af13ed..4f671bba5 100644 --- a/components/arcade/arcade-embed-widget.tsx +++ b/components/arcade/arcade-embed-widget.tsx @@ -8,11 +8,23 @@ function sanitizeUtm(value: string | null, fallback: string): string { return /^[a-zA-Z0-9._-]{1,80}$/.test(value) ? value : fallback } +function parseTargetUrl(value: string | null): URL { + const fallback = new URL("https://arcade.eplus.dev/") + if (!value) return fallback + + try { + const target = new URL(value) + return target.protocol === "http:" || target.protocol === "https:" ? target : fallback + } catch { + return fallback + } +} + export default function ArcadeEmbedWidget() { const ctaUrl = useMemo(() => { if (typeof window === "undefined") return "https://arcade.eplus.dev/?utm_source=hashnode&utm_medium=widget" const params = new URLSearchParams(window.location.search) - const target = new URL("https://arcade.eplus.dev/") + const target = parseTargetUrl(params.get("url")) target.searchParams.set("utm_source", sanitizeUtm(params.get("utm_source"), "hashnode")) target.searchParams.set("utm_medium", sanitizeUtm(params.get("utm_medium"), "widget")) target.searchParams.set("utm_campaign", sanitizeUtm(params.get("utm_campaign"), "arcade-widget")) From f83ec84c76b33c857027d4b2adcc6a256995e6c9 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 9 Aug 2026 10:14:40 +0700 Subject: [PATCH 05/14] Hide theme toggle on widget embed --- components/theme-toggle.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx index 7a7bf9638..c3915e4cb 100644 --- a/components/theme-toggle.tsx +++ b/components/theme-toggle.tsx @@ -1,6 +1,7 @@ "use client" import { useTheme } from "next-themes" +import { usePathname } from "next/navigation" import { useEffect, useState } from "react" import { createPortal } from "react-dom" import { usePortalTarget } from "@/components/use-portal-target" @@ -47,15 +48,19 @@ function MoonIcon() { /** Toggles between the persisted light and dark website themes. */ export default function ThemeToggle() { + const pathname = usePathname() const { resolvedTheme, setTheme } = useTheme() const [mounted, setMounted] = useState(false) const portalTarget = usePortalTarget(".arcade-header-actions") + const isWidgetRoute = pathname === "/widget" || pathname?.endsWith("/widget/") useEffect(() => { setMounted(true) }, []) useEffect(() => { + if (isWidgetRoute) return + const activeTheme = resolvedTheme === "light" ? "light" : "dark" let themeColor = document.querySelector( 'meta[name="theme-color"]', @@ -68,9 +73,9 @@ export default function ThemeToggle() { } themeColor.content = THEME_COLORS[activeTheme] - }, [resolvedTheme]) + }, [isWidgetRoute, resolvedTheme]) - if (!mounted) return null + if (!mounted || isWidgetRoute) return null const isDark = resolvedTheme === "dark" const nextTheme = isDark ? "light" : "dark" From 1797bc575383c2f08f6d3e73e3e9e450c09d7e1b Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 9 Aug 2026 10:15:04 +0700 Subject: [PATCH 06/14] Use widget URL as tracking source --- components/arcade/arcade-embed-widget.tsx | 26 ++++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/components/arcade/arcade-embed-widget.tsx b/components/arcade/arcade-embed-widget.tsx index 4f671bba5..c9dde1aa8 100644 --- a/components/arcade/arcade-embed-widget.tsx +++ b/components/arcade/arcade-embed-widget.tsx @@ -8,26 +8,36 @@ function sanitizeUtm(value: string | null, fallback: string): string { return /^[a-zA-Z0-9._-]{1,80}$/.test(value) ? value : fallback } -function parseTargetUrl(value: string | null): URL { - const fallback = new URL("https://arcade.eplus.dev/") - if (!value) return fallback +function parseTrackingUrl(value: string | null): string | null { + if (!value) return null try { - const target = new URL(value) - return target.protocol === "http:" || target.protocol === "https:" ? target : fallback + const url = new URL(value) + return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : null } catch { - return fallback + return null } } export default function ArcadeEmbedWidget() { const ctaUrl = useMemo(() => { - if (typeof window === "undefined") return "https://arcade.eplus.dev/?utm_source=hashnode&utm_medium=widget" + if (typeof window === "undefined") { + return "https://arcade.eplus.dev/?utm_source=hashnode&utm_medium=widget" + } + const params = new URLSearchParams(window.location.search) - const target = parseTargetUrl(params.get("url")) + const target = new URL("https://arcade.eplus.dev/") + const trackingUrl = + parseTrackingUrl(params.get("url")) ?? parseTrackingUrl(document.referrer) + target.searchParams.set("utm_source", sanitizeUtm(params.get("utm_source"), "hashnode")) target.searchParams.set("utm_medium", sanitizeUtm(params.get("utm_medium"), "widget")) target.searchParams.set("utm_campaign", sanitizeUtm(params.get("utm_campaign"), "arcade-widget")) + + if (trackingUrl) { + target.searchParams.set("source_url", trackingUrl) + } + return target.toString() }, []) From 125b39366f0c52350fb78411559060e43deb0adc Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 9 Aug 2026 10:34:53 +0700 Subject: [PATCH 07/14] Turn embed widget into mini score checker --- components/arcade/arcade-embed-widget.tsx | 183 ++++++++++++++++++---- 1 file changed, 149 insertions(+), 34 deletions(-) diff --git a/components/arcade/arcade-embed-widget.tsx b/components/arcade/arcade-embed-widget.tsx index c9dde1aa8..d9565cef6 100644 --- a/components/arcade/arcade-embed-widget.tsx +++ b/components/arcade/arcade-embed-widget.tsx @@ -1,62 +1,177 @@ "use client" -import { ExternalLink, Gamepad2, GraduationCap, Sparkles, Trophy } from "lucide-react" -import { useMemo } from "react" +import { ExternalLink, Gamepad2, LoaderCircle, Search, Trophy } from "lucide-react" +import type { FormEvent } from "react" +import { useMemo, useState } from "react" +import { getFacilitatorAdjustedPoints } from "@/components/arcade/facilitator-points" +import { readFacilitatorParticipation } from "@/components/arcade/facilitator-participation" +import { + API_URL, + DASHBOARD_STORAGE_KEY, + PROFILE_URL_PATTERN, + getTier, + numeric, +} from "@/components/arcade/model" +import type { ArcadeApiResponse } from "@/components/arcade/model" function sanitizeUtm(value: string | null, fallback: string): string { if (!value) return fallback return /^[a-zA-Z0-9._-]{1,80}$/.test(value) ? value : fallback } -function parseTrackingUrl(value: string | null): string | null { - if (!value) return null - - try { - const url = new URL(value) - return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : null - } catch { - return null - } +function readInitialProfileUrl(): string { + if (typeof window === "undefined") return "" + const value = new URLSearchParams(window.location.search).get("url")?.trim() ?? "" + return PROFILE_URL_PATTERN.test(value.replace(/\/$/, "")) ? value : "" } export default function ArcadeEmbedWidget() { - const ctaUrl = useMemo(() => { - if (typeof window === "undefined") { - return "https://arcade.eplus.dev/?utm_source=hashnode&utm_medium=widget" - } + const [profileUrl, setProfileUrl] = useState(readInitialProfileUrl) + const [loading, setLoading] = useState(false) + const [error, setError] = useState("") + const [result, setResult] = useState(null) - const params = new URLSearchParams(window.location.search) + const fullResultUrl = useMemo(() => { const target = new URL("https://arcade.eplus.dev/") - const trackingUrl = - parseTrackingUrl(params.get("url")) ?? parseTrackingUrl(document.referrer) + if (typeof window === "undefined") return target.toString() + const params = new URLSearchParams(window.location.search) target.searchParams.set("utm_source", sanitizeUtm(params.get("utm_source"), "hashnode")) target.searchParams.set("utm_medium", sanitizeUtm(params.get("utm_medium"), "widget")) target.searchParams.set("utm_campaign", sanitizeUtm(params.get("utm_campaign"), "arcade-widget")) + return target.toString() + }, []) + + const normalizedProfileUrl = profileUrl.trim().replace(/\/$/, "") + const basePoints = numeric(result?.arcadePoints?.totalPoints) + const facilitatorScore = result + ? getFacilitatorAdjustedPoints( + basePoints, + { + games: numeric(result.faciCounts?.faciGame), + skills: numeric(result.faciCounts?.faciSkill), + }, + readFacilitatorParticipation(normalizedProfileUrl), + ) + : null + const totalPoints = facilitatorScore?.totalPoints ?? basePoints + const tier = getTier(totalPoints) + const badgeCount = numeric(result?.beta?.profileBadgeCount) || (result?.badges?.length ?? 0) + const userName = result?.userDetails?.[0]?.userName || "Google Skills learner" + + async function checkScore(event: FormEvent) { + event.preventDefault() + const normalized = profileUrl.trim().replace(/\/$/, "") - if (trackingUrl) { - target.searchParams.set("source_url", trackingUrl) + if (!PROFILE_URL_PATTERN.test(normalized)) { + setError("Paste a valid public profile URL from skills.google.") + setResult(null) + return } - return target.toString() - }, []) + setLoading(true) + setError("") + + try { + const response = await fetch(API_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url: normalized, season: "2026" }), + }) + + let payload: ArcadeApiResponse | null = null + try { + payload = (await response.json()) as ArcadeApiResponse + } catch { + // Keep the stable error below when the gateway returns invalid JSON. + } + + if (!response.ok || !payload?.success) { + throw new Error(payload?.message || "The profile could not be analyzed right now.") + } + + setProfileUrl(normalized) + setResult(payload) + + try { + window.localStorage.setItem( + DASHBOARD_STORAGE_KEY, + JSON.stringify({ profileUrl: normalized, result: payload }), + ) + } catch { + // The mini checker still works when storage is unavailable. + } + } catch (caught) { + setResult(null) + setError(caught instanceof Error ? caught.message : "The profile could not be analyzed.") + } finally { + setLoading(false) + } + } return (
- - -
-
ARCADE POINTSby ePlus.DEV
-

Track your Google Cloud Arcade progress.

-

Calculate points, follow milestones and keep up with monthly Arcade activities.

-
- Points & tiers - Monthly games - Facilitator bonus +
+
+ +
+ ARCADE POINTS + by ePlus.DEV
-
Check your score
-
+ + {!result ? ( + <> +
+

Check your Google Cloud Arcade score

+

Paste your public Google Skills profile URL to see your points and tier.

+
+ +
+ + +
+ {error ?

{error}

: null} + + ) : ( +
+
+ {userName} + {totalPoints} + Arcade points +
+
+ {tier.name} tier + {badgeCount} badges + {facilitatorScore && facilitatorScore.bonus > 0 ? ( + +{facilitatorScore.bonus} Facilitator bonus + ) : null} +
+
+ + + View full result + +
+
+ )} +
) } From a77ed5ff1127c043cd44146b982b8e9c9684559d Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 9 Aug 2026 10:35:34 +0700 Subject: [PATCH 08/14] Style widget as compact embedded score checker --- app/widget/widget.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/widget/widget.css b/app/widget/widget.css index 80ed3c266..f4662a410 100644 --- a/app/widget/widget.css +++ b/app/widget/widget.css @@ -1 +1 @@ -html,body{margin:0!important;min-height:100%;background:transparent!important}.arcade-embed-shell{box-sizing:border-box;width:100%;padding:8px;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.arcade-embed-card{box-sizing:border-box;position:relative;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:18px;width:100%;max-width:980px;margin:0 auto;padding:20px 22px;overflow:hidden;border:1px solid rgba(139,92,246,.28);border-radius:18px;background:radial-gradient(circle at 0 0,rgba(124,58,237,.18),transparent 32%),linear-gradient(135deg,#091327,#0b1730 62%,#11102b);box-shadow:0 12px 32px rgba(2,8,23,.22);color:#eef4ff;text-decoration:none}.arcade-embed-card:before{content:"";position:absolute;right:-55px;bottom:-85px;width:210px;height:160px;border-radius:50%;background:rgba(37,99,235,.18);filter:blur(42px);pointer-events:none}.arcade-embed-icon{position:relative;z-index:1;display:grid;place-items:center;width:58px;height:58px;border-radius:16px;background:linear-gradient(145deg,#7c3aed,#2563eb);box-shadow:0 8px 24px rgba(79,70,229,.28);color:#fff}.arcade-embed-icon svg{width:30px;height:30px}.arcade-embed-content{position:relative;z-index:1;min-width:0}.arcade-embed-brand{display:flex;align-items:baseline;gap:7px;margin-bottom:4px}.arcade-embed-brand strong{font-size:.72rem;letter-spacing:.09em;color:#c4b5fd}.arcade-embed-brand span{font-size:.64rem;color:#71839f}.arcade-embed-content h2{margin:0;color:#fff;font-size:1.22rem;line-height:1.2;letter-spacing:-.02em}.arcade-embed-content p{margin:5px 0 10px;color:#9fb0c8;font-size:.75rem;line-height:1.45}.arcade-embed-features{display:flex;align-items:center;gap:14px;flex-wrap:wrap}.arcade-embed-features span{display:inline-flex;align-items:center;gap:5px;color:#c9d5e6;font-size:.67rem;font-weight:600}.arcade-embed-features svg{width:13px;height:13px;color:#a78bfa}.arcade-embed-cta{position:relative;z-index:1;display:flex;align-items:center;gap:8px;white-space:nowrap;padding:11px 14px;border-radius:11px;background:rgba(124,58,237,.15);border:1px solid rgba(167,139,250,.3);color:#ddd6fe;font-size:.72rem;font-weight:800;transition:background .16s ease,transform .16s ease}.arcade-embed-cta svg{width:14px;height:14px}.arcade-embed-card:hover .arcade-embed-cta{background:rgba(124,58,237,.26);transform:translateY(-1px)}@media(max-width:640px){.arcade-embed-shell{padding:6px}.arcade-embed-card{grid-template-columns:46px minmax(0,1fr);gap:12px;padding:15px;border-radius:15px}.arcade-embed-icon{width:46px;height:46px;border-radius:13px;align-self:start}.arcade-embed-icon svg{width:24px;height:24px}.arcade-embed-content h2{font-size:1rem}.arcade-embed-content p{font-size:.69rem;margin:4px 0 9px}.arcade-embed-brand strong{font-size:.65rem}.arcade-embed-brand span{font-size:.58rem}.arcade-embed-features{gap:8px 11px}.arcade-embed-features span{font-size:.61rem}.arcade-embed-cta{grid-column:1/-1;justify-content:center;padding:10px 12px;font-size:.69rem}}@media(max-width:390px){.arcade-embed-card{grid-template-columns:40px minmax(0,1fr);padding:13px 12px}.arcade-embed-icon{width:40px;height:40px}.arcade-embed-content p{display:none}.arcade-embed-features{margin-top:8px}.arcade-embed-features span{font-size:.58rem}}@media(prefers-color-scheme:light){.arcade-embed-card{border-color:rgba(99,102,241,.18);background:radial-gradient(circle at 0 0,rgba(124,58,237,.1),transparent 34%),linear-gradient(135deg,#fff,#f8faff 70%,#f5f3ff);box-shadow:0 10px 28px rgba(71,85,105,.12);color:#172033}.arcade-embed-content h2{color:#172033}.arcade-embed-content p{color:#64748b}.arcade-embed-brand span{color:#94a3b8}.arcade-embed-features span{color:#475569}.arcade-embed-cta{background:#f5f3ff;border-color:#ddd6fe;color:#6d28d9}.arcade-embed-card:hover .arcade-embed-cta{background:#ede9fe}} +html,body{margin:0!important;min-height:100%;background:transparent!important}.arcade-embed-shell{box-sizing:border-box;width:100%;padding:8px;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.arcade-widget-card{box-sizing:border-box;position:relative;width:100%;max-width:760px;margin:0 auto;padding:16px;border:1px solid rgba(139,92,246,.28);border-radius:18px;background:radial-gradient(circle at 100% 0,rgba(37,99,235,.16),transparent 34%),linear-gradient(135deg,#091327,#0b1730 62%,#11102b);box-shadow:0 12px 32px rgba(2,8,23,.22);color:#eef4ff;overflow:hidden}.arcade-widget-card:before{content:"";position:absolute;right:-60px;bottom:-100px;width:220px;height:180px;border-radius:50%;background:rgba(124,58,237,.2);filter:blur(44px);pointer-events:none}.arcade-widget-head{position:relative;z-index:1;display:flex;align-items:center;gap:9px;margin-bottom:10px}.arcade-widget-icon{display:grid;place-items:center;width:38px;height:38px;border-radius:12px;background:linear-gradient(145deg,#7c3aed,#2563eb);color:#fff;box-shadow:0 7px 20px rgba(79,70,229,.25)}.arcade-widget-icon svg{width:21px;height:21px}.arcade-widget-head>div{display:grid;gap:1px}.arcade-widget-head strong{font-size:.72rem;letter-spacing:.09em;color:#c4b5fd}.arcade-widget-head span{font-size:.6rem;color:#71839f}.arcade-widget-copy{position:relative;z-index:1}.arcade-widget-copy h2{margin:0;color:#fff;font-size:1.08rem;line-height:1.22;letter-spacing:-.02em}.arcade-widget-copy p{margin:4px 0 12px;color:#9fb0c8;font-size:.72rem;line-height:1.45}.arcade-widget-form{position:relative;z-index:1;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px}.arcade-widget-input{display:flex;align-items:center;gap:8px;min-width:0;padding:0 11px;border:1px solid rgba(148,163,184,.2);border-radius:11px;background:rgba(6,15,32,.62);transition:border-color .16s ease,box-shadow .16s ease}.arcade-widget-input:focus-within{border-color:rgba(167,139,250,.7);box-shadow:0 0 0 3px rgba(124,58,237,.12)}.arcade-widget-input.has-error{border-color:rgba(248,113,113,.72)}.arcade-widget-input svg{width:15px;height:15px;color:#8fa2bf;flex:0 0 auto}.arcade-widget-input input{box-sizing:border-box;width:100%;min-width:0;height:42px;border:0;outline:0;background:transparent;color:#eef4ff;font:inherit;font-size:.7rem}.arcade-widget-input input::placeholder{color:#64748b}.arcade-widget-form>button{display:inline-flex;align-items:center;justify-content:center;gap:7px;height:42px;padding:0 14px;border:1px solid rgba(167,139,250,.35);border-radius:11px;background:linear-gradient(90deg,#6d28d9,#7c3aed 55%,#2563eb);color:#fff;font-size:.7rem;font-weight:800;cursor:pointer;white-space:nowrap}.arcade-widget-form>button:disabled{opacity:.72;cursor:wait}.arcade-widget-form>button svg{width:15px;height:15px}.arcade-widget-error{position:relative;z-index:1;margin:7px 2px 0;color:#fca5a5;font-size:.66rem}.spin{animation:arcade-widget-spin .8s linear infinite}@keyframes arcade-widget-spin{to{transform:rotate(360deg)}}.arcade-widget-result{position:relative;z-index:1;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:18px}.arcade-widget-result-copy{display:grid;min-width:112px}.arcade-widget-result-copy>span{max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#aebdd1;font-size:.63rem}.arcade-widget-result-copy>strong{margin-top:1px;color:#fff;font-size:2rem;line-height:1}.arcade-widget-result-copy>small{margin-top:3px;color:#9fb0c8;font-size:.64rem}.arcade-widget-result-meta{display:flex;align-items:center;gap:7px 16px;flex-wrap:wrap}.arcade-widget-result-meta span{color:#b9c7da;font-size:.68rem}.arcade-widget-result-meta b{color:#ddd6fe}.arcade-widget-result-actions{display:flex;align-items:center;gap:7px;white-space:nowrap}.arcade-widget-result-actions button,.arcade-widget-result-actions a{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:38px;padding:0 11px;border-radius:10px;font-size:.66rem;font-weight:800;text-decoration:none;cursor:pointer}.arcade-widget-result-actions button{border:1px solid rgba(148,163,184,.2);background:rgba(6,15,32,.45);color:#b9c7da}.arcade-widget-result-actions a{border:1px solid rgba(167,139,250,.34);background:rgba(124,58,237,.18);color:#ddd6fe}.arcade-widget-result-actions svg{width:13px;height:13px}@media(max-width:640px){.arcade-embed-shell{padding:6px}.arcade-widget-card{padding:14px;border-radius:15px}.arcade-widget-copy h2{font-size:.98rem}.arcade-widget-copy p{font-size:.67rem}.arcade-widget-form{grid-template-columns:1fr}.arcade-widget-form>button{width:100%}.arcade-widget-result{grid-template-columns:auto 1fr;gap:12px}.arcade-widget-result-actions{grid-column:1/-1;display:grid;grid-template-columns:1fr 1fr}.arcade-widget-result-actions button,.arcade-widget-result-actions a{width:auto}.arcade-widget-result-meta{gap:6px 11px}.arcade-widget-result-copy>strong{font-size:1.7rem}}@media(max-width:390px){.arcade-widget-copy p{display:none}.arcade-widget-form{margin-top:10px}.arcade-widget-result{grid-template-columns:1fr}.arcade-widget-result-copy{grid-template-columns:auto auto;align-items:end;column-gap:6px}.arcade-widget-result-copy>span{grid-column:1/-1}.arcade-widget-result-copy>small{margin-bottom:2px}.arcade-widget-result-actions{grid-column:auto}.arcade-widget-result-meta{margin-top:-3px}}@media(prefers-color-scheme:light){.arcade-widget-card{border-color:rgba(99,102,241,.18);background:radial-gradient(circle at 100% 0,rgba(37,99,235,.09),transparent 34%),linear-gradient(135deg,#fff,#f8faff 70%,#f5f3ff);box-shadow:0 10px 28px rgba(71,85,105,.12);color:#172033}.arcade-widget-copy h2,.arcade-widget-result-copy>strong{color:#172033}.arcade-widget-copy p,.arcade-widget-result-copy>small,.arcade-widget-result-copy>span{color:#64748b}.arcade-widget-head span{color:#94a3b8}.arcade-widget-input{border-color:rgba(99,102,241,.14);background:rgba(255,255,255,.78)}.arcade-widget-input input{color:#172033}.arcade-widget-input input::placeholder{color:#94a3b8}.arcade-widget-result-meta span{color:#64748b}.arcade-widget-result-meta b{color:#6d28d9}.arcade-widget-result-actions button{background:#fff;color:#64748b}.arcade-widget-result-actions a{background:#f5f3ff;color:#6d28d9}} From b92d42d3bfcd2a070fab86cba32d72c61d537c5c Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 9 Aug 2026 10:42:02 +0700 Subject: [PATCH 09/14] Add always-available Arcade CTA to widget --- components/arcade/arcade-embed-widget.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/arcade/arcade-embed-widget.tsx b/components/arcade/arcade-embed-widget.tsx index d9565cef6..499923616 100644 --- a/components/arcade/arcade-embed-widget.tsx +++ b/components/arcade/arcade-embed-widget.tsx @@ -118,6 +118,9 @@ export default function ArcadeEmbedWidget() { ARCADE POINTS by ePlus.DEV + + Open Arcade + {!result ? ( From 39372051e49bda7ab6c5a9eef40c0c20b36e6c8c Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 9 Aug 2026 10:42:42 +0700 Subject: [PATCH 10/14] Style always-available Arcade widget link --- app/widget/widget.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/widget/widget.css b/app/widget/widget.css index f4662a410..1c0a73c1c 100644 --- a/app/widget/widget.css +++ b/app/widget/widget.css @@ -1 +1 @@ -html,body{margin:0!important;min-height:100%;background:transparent!important}.arcade-embed-shell{box-sizing:border-box;width:100%;padding:8px;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.arcade-widget-card{box-sizing:border-box;position:relative;width:100%;max-width:760px;margin:0 auto;padding:16px;border:1px solid rgba(139,92,246,.28);border-radius:18px;background:radial-gradient(circle at 100% 0,rgba(37,99,235,.16),transparent 34%),linear-gradient(135deg,#091327,#0b1730 62%,#11102b);box-shadow:0 12px 32px rgba(2,8,23,.22);color:#eef4ff;overflow:hidden}.arcade-widget-card:before{content:"";position:absolute;right:-60px;bottom:-100px;width:220px;height:180px;border-radius:50%;background:rgba(124,58,237,.2);filter:blur(44px);pointer-events:none}.arcade-widget-head{position:relative;z-index:1;display:flex;align-items:center;gap:9px;margin-bottom:10px}.arcade-widget-icon{display:grid;place-items:center;width:38px;height:38px;border-radius:12px;background:linear-gradient(145deg,#7c3aed,#2563eb);color:#fff;box-shadow:0 7px 20px rgba(79,70,229,.25)}.arcade-widget-icon svg{width:21px;height:21px}.arcade-widget-head>div{display:grid;gap:1px}.arcade-widget-head strong{font-size:.72rem;letter-spacing:.09em;color:#c4b5fd}.arcade-widget-head span{font-size:.6rem;color:#71839f}.arcade-widget-copy{position:relative;z-index:1}.arcade-widget-copy h2{margin:0;color:#fff;font-size:1.08rem;line-height:1.22;letter-spacing:-.02em}.arcade-widget-copy p{margin:4px 0 12px;color:#9fb0c8;font-size:.72rem;line-height:1.45}.arcade-widget-form{position:relative;z-index:1;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px}.arcade-widget-input{display:flex;align-items:center;gap:8px;min-width:0;padding:0 11px;border:1px solid rgba(148,163,184,.2);border-radius:11px;background:rgba(6,15,32,.62);transition:border-color .16s ease,box-shadow .16s ease}.arcade-widget-input:focus-within{border-color:rgba(167,139,250,.7);box-shadow:0 0 0 3px rgba(124,58,237,.12)}.arcade-widget-input.has-error{border-color:rgba(248,113,113,.72)}.arcade-widget-input svg{width:15px;height:15px;color:#8fa2bf;flex:0 0 auto}.arcade-widget-input input{box-sizing:border-box;width:100%;min-width:0;height:42px;border:0;outline:0;background:transparent;color:#eef4ff;font:inherit;font-size:.7rem}.arcade-widget-input input::placeholder{color:#64748b}.arcade-widget-form>button{display:inline-flex;align-items:center;justify-content:center;gap:7px;height:42px;padding:0 14px;border:1px solid rgba(167,139,250,.35);border-radius:11px;background:linear-gradient(90deg,#6d28d9,#7c3aed 55%,#2563eb);color:#fff;font-size:.7rem;font-weight:800;cursor:pointer;white-space:nowrap}.arcade-widget-form>button:disabled{opacity:.72;cursor:wait}.arcade-widget-form>button svg{width:15px;height:15px}.arcade-widget-error{position:relative;z-index:1;margin:7px 2px 0;color:#fca5a5;font-size:.66rem}.spin{animation:arcade-widget-spin .8s linear infinite}@keyframes arcade-widget-spin{to{transform:rotate(360deg)}}.arcade-widget-result{position:relative;z-index:1;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:18px}.arcade-widget-result-copy{display:grid;min-width:112px}.arcade-widget-result-copy>span{max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#aebdd1;font-size:.63rem}.arcade-widget-result-copy>strong{margin-top:1px;color:#fff;font-size:2rem;line-height:1}.arcade-widget-result-copy>small{margin-top:3px;color:#9fb0c8;font-size:.64rem}.arcade-widget-result-meta{display:flex;align-items:center;gap:7px 16px;flex-wrap:wrap}.arcade-widget-result-meta span{color:#b9c7da;font-size:.68rem}.arcade-widget-result-meta b{color:#ddd6fe}.arcade-widget-result-actions{display:flex;align-items:center;gap:7px;white-space:nowrap}.arcade-widget-result-actions button,.arcade-widget-result-actions a{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:38px;padding:0 11px;border-radius:10px;font-size:.66rem;font-weight:800;text-decoration:none;cursor:pointer}.arcade-widget-result-actions button{border:1px solid rgba(148,163,184,.2);background:rgba(6,15,32,.45);color:#b9c7da}.arcade-widget-result-actions a{border:1px solid rgba(167,139,250,.34);background:rgba(124,58,237,.18);color:#ddd6fe}.arcade-widget-result-actions svg{width:13px;height:13px}@media(max-width:640px){.arcade-embed-shell{padding:6px}.arcade-widget-card{padding:14px;border-radius:15px}.arcade-widget-copy h2{font-size:.98rem}.arcade-widget-copy p{font-size:.67rem}.arcade-widget-form{grid-template-columns:1fr}.arcade-widget-form>button{width:100%}.arcade-widget-result{grid-template-columns:auto 1fr;gap:12px}.arcade-widget-result-actions{grid-column:1/-1;display:grid;grid-template-columns:1fr 1fr}.arcade-widget-result-actions button,.arcade-widget-result-actions a{width:auto}.arcade-widget-result-meta{gap:6px 11px}.arcade-widget-result-copy>strong{font-size:1.7rem}}@media(max-width:390px){.arcade-widget-copy p{display:none}.arcade-widget-form{margin-top:10px}.arcade-widget-result{grid-template-columns:1fr}.arcade-widget-result-copy{grid-template-columns:auto auto;align-items:end;column-gap:6px}.arcade-widget-result-copy>span{grid-column:1/-1}.arcade-widget-result-copy>small{margin-bottom:2px}.arcade-widget-result-actions{grid-column:auto}.arcade-widget-result-meta{margin-top:-3px}}@media(prefers-color-scheme:light){.arcade-widget-card{border-color:rgba(99,102,241,.18);background:radial-gradient(circle at 100% 0,rgba(37,99,235,.09),transparent 34%),linear-gradient(135deg,#fff,#f8faff 70%,#f5f3ff);box-shadow:0 10px 28px rgba(71,85,105,.12);color:#172033}.arcade-widget-copy h2,.arcade-widget-result-copy>strong{color:#172033}.arcade-widget-copy p,.arcade-widget-result-copy>small,.arcade-widget-result-copy>span{color:#64748b}.arcade-widget-head span{color:#94a3b8}.arcade-widget-input{border-color:rgba(99,102,241,.14);background:rgba(255,255,255,.78)}.arcade-widget-input input{color:#172033}.arcade-widget-input input::placeholder{color:#94a3b8}.arcade-widget-result-meta span{color:#64748b}.arcade-widget-result-meta b{color:#6d28d9}.arcade-widget-result-actions button{background:#fff;color:#64748b}.arcade-widget-result-actions a{background:#f5f3ff;color:#6d28d9}} +html,body{margin:0!important;min-height:100%;background:transparent!important}.arcade-embed-shell{box-sizing:border-box;width:100%;padding:8px;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.arcade-widget-card{box-sizing:border-box;position:relative;width:100%;max-width:760px;margin:0 auto;padding:16px;border:1px solid rgba(139,92,246,.28);border-radius:18px;background:radial-gradient(circle at 100% 0,rgba(37,99,235,.16),transparent 34%),linear-gradient(135deg,#091327,#0b1730 62%,#11102b);box-shadow:0 12px 32px rgba(2,8,23,.22);color:#eef4ff;overflow:hidden}.arcade-widget-card:before{content:"";position:absolute;right:-60px;bottom:-100px;width:220px;height:180px;border-radius:50%;background:rgba(124,58,237,.2);filter:blur(44px);pointer-events:none}.arcade-widget-head{position:relative;z-index:1;display:flex;align-items:center;gap:9px;margin-bottom:10px}.arcade-widget-icon{display:grid;place-items:center;width:38px;height:38px;border-radius:12px;background:linear-gradient(145deg,#7c3aed,#2563eb);color:#fff;box-shadow:0 7px 20px rgba(79,70,229,.25)}.arcade-widget-icon svg{width:21px;height:21px}.arcade-widget-head>div{display:grid;gap:1px}.arcade-widget-head strong{font-size:.72rem;letter-spacing:.09em;color:#c4b5fd}.arcade-widget-head span{font-size:.6rem;color:#71839f}.arcade-widget-open-link{margin-left:auto;display:inline-flex;align-items:center;gap:6px;padding:7px 9px;border:1px solid rgba(148,163,184,.17);border-radius:9px;background:rgba(6,15,32,.32);color:#b9c7da;font-size:.63rem;font-weight:750;text-decoration:none;white-space:nowrap}.arcade-widget-open-link:hover{border-color:rgba(167,139,250,.4);color:#ddd6fe}.arcade-widget-open-link svg{width:12px;height:12px}.arcade-widget-copy{position:relative;z-index:1}.arcade-widget-copy h2{margin:0;color:#fff;font-size:1.08rem;line-height:1.22;letter-spacing:-.02em}.arcade-widget-copy p{margin:4px 0 12px;color:#9fb0c8;font-size:.72rem;line-height:1.45}.arcade-widget-form{position:relative;z-index:1;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px}.arcade-widget-input{display:flex;align-items:center;gap:8px;min-width:0;padding:0 11px;border:1px solid rgba(148,163,184,.2);border-radius:11px;background:rgba(6,15,32,.62);transition:border-color .16s ease,box-shadow .16s ease}.arcade-widget-input:focus-within{border-color:rgba(167,139,250,.7);box-shadow:0 0 0 3px rgba(124,58,237,.12)}.arcade-widget-input.has-error{border-color:rgba(248,113,113,.72)}.arcade-widget-input svg{width:15px;height:15px;color:#8fa2bf;flex:0 0 auto}.arcade-widget-input input{box-sizing:border-box;width:100%;min-width:0;height:42px;border:0;outline:0;background:transparent;color:#eef4ff;font:inherit;font-size:.7rem}.arcade-widget-input input::placeholder{color:#64748b}.arcade-widget-form>button{display:inline-flex;align-items:center;justify-content:center;gap:7px;height:42px;padding:0 14px;border:1px solid rgba(167,139,250,.35);border-radius:11px;background:linear-gradient(90deg,#6d28d9,#7c3aed 55%,#2563eb);color:#fff;font-size:.7rem;font-weight:800;cursor:pointer;white-space:nowrap}.arcade-widget-form>button:disabled{opacity:.72;cursor:wait}.arcade-widget-form>button svg{width:15px;height:15px}.arcade-widget-error{position:relative;z-index:1;margin:7px 2px 0;color:#fca5a5;font-size:.66rem}.spin{animation:arcade-widget-spin .8s linear infinite}@keyframes arcade-widget-spin{to{transform:rotate(360deg)}}.arcade-widget-result{position:relative;z-index:1;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:18px}.arcade-widget-result-copy{display:grid;min-width:112px}.arcade-widget-result-copy>span{max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#aebdd1;font-size:.63rem}.arcade-widget-result-copy>strong{margin-top:1px;color:#fff;font-size:2rem;line-height:1}.arcade-widget-result-copy>small{margin-top:3px;color:#9fb0c8;font-size:.64rem}.arcade-widget-result-meta{display:flex;align-items:center;gap:7px 16px;flex-wrap:wrap}.arcade-widget-result-meta span{color:#b9c7da;font-size:.68rem}.arcade-widget-result-meta b{color:#ddd6fe}.arcade-widget-result-actions{display:flex;align-items:center;gap:7px;white-space:nowrap}.arcade-widget-result-actions button,.arcade-widget-result-actions a{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:38px;padding:0 11px;border-radius:10px;font-size:.66rem;font-weight:800;text-decoration:none;cursor:pointer}.arcade-widget-result-actions button{border:1px solid rgba(148,163,184,.2);background:rgba(6,15,32,.45);color:#b9c7da}.arcade-widget-result-actions a{border:1px solid rgba(167,139,250,.34);background:rgba(124,58,237,.18);color:#ddd6fe}.arcade-widget-result-actions svg{width:13px;height:13px}@media(max-width:640px){.arcade-embed-shell{padding:6px}.arcade-widget-card{padding:14px;border-radius:15px}.arcade-widget-copy h2{font-size:.98rem}.arcade-widget-copy p{font-size:.67rem}.arcade-widget-open-link{padding:6px 8px;font-size:.6rem}.arcade-widget-form{grid-template-columns:1fr}.arcade-widget-form>button{width:100%}.arcade-widget-result{grid-template-columns:auto 1fr;gap:12px}.arcade-widget-result-actions{grid-column:1/-1;display:grid;grid-template-columns:1fr 1fr}.arcade-widget-result-actions button,.arcade-widget-result-actions a{width:auto}.arcade-widget-result-meta{gap:6px 11px}.arcade-widget-result-copy>strong{font-size:1.7rem}}@media(max-width:390px){.arcade-widget-copy p{display:none}.arcade-widget-form{margin-top:10px}.arcade-widget-open-link span{display:none}.arcade-widget-open-link{padding:7px}.arcade-widget-result{grid-template-columns:1fr}.arcade-widget-result-copy{grid-template-columns:auto auto;align-items:end;column-gap:6px}.arcade-widget-result-copy>span{grid-column:1/-1}.arcade-widget-result-copy>small{margin-bottom:2px}.arcade-widget-result-actions{grid-column:auto}.arcade-widget-result-meta{margin-top:-3px}}@media(prefers-color-scheme:light){.arcade-widget-card{border-color:rgba(99,102,241,.18);background:radial-gradient(circle at 100% 0,rgba(37,99,235,.09),transparent 34%),linear-gradient(135deg,#fff,#f8faff 70%,#f5f3ff);box-shadow:0 10px 28px rgba(71,85,105,.12);color:#172033}.arcade-widget-copy h2,.arcade-widget-result-copy>strong{color:#172033}.arcade-widget-copy p,.arcade-widget-result-copy>small,.arcade-widget-result-copy>span{color:#64748b}.arcade-widget-head span{color:#94a3b8}.arcade-widget-open-link{background:#fff;border-color:rgba(99,102,241,.13);color:#64748b}.arcade-widget-open-link:hover{color:#6d28d9;border-color:#ddd6fe}.arcade-widget-input{border-color:rgba(99,102,241,.14);background:rgba(255,255,255,.78)}.arcade-widget-input input{color:#172033}.arcade-widget-input input::placeholder{color:#94a3b8}.arcade-widget-result-meta span{color:#64748b}.arcade-widget-result-meta b{color:#6d28d9}.arcade-widget-result-actions button{background:#fff;color:#64748b}.arcade-widget-result-actions a{background:#f5f3ff;color:#6d28d9}} From bfe8a591647a79e88e8a3f896fce06c376ff7f07 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 9 Aug 2026 10:47:44 +0700 Subject: [PATCH 11/14] Polish widget header layout --- components/arcade/arcade-embed-widget.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/components/arcade/arcade-embed-widget.tsx b/components/arcade/arcade-embed-widget.tsx index 499923616..674f86c0f 100644 --- a/components/arcade/arcade-embed-widget.tsx +++ b/components/arcade/arcade-embed-widget.tsx @@ -113,13 +113,15 @@ export default function ArcadeEmbedWidget() {
- -
- ARCADE POINTS - by ePlus.DEV +
+ +
+ ARCADE POINTS + Google Cloud Arcade tracker · ePlus.DEV +
- Open Arcade + Open full dashboard
From 804b3f20b037004c624aa9bb9b54b899704bdb5f Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 9 Aug 2026 10:48:16 +0700 Subject: [PATCH 12/14] Refine widget mobile header styling --- app/widget/widget.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/widget/widget.css b/app/widget/widget.css index 1c0a73c1c..0a439cb28 100644 --- a/app/widget/widget.css +++ b/app/widget/widget.css @@ -1 +1 @@ -html,body{margin:0!important;min-height:100%;background:transparent!important}.arcade-embed-shell{box-sizing:border-box;width:100%;padding:8px;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.arcade-widget-card{box-sizing:border-box;position:relative;width:100%;max-width:760px;margin:0 auto;padding:16px;border:1px solid rgba(139,92,246,.28);border-radius:18px;background:radial-gradient(circle at 100% 0,rgba(37,99,235,.16),transparent 34%),linear-gradient(135deg,#091327,#0b1730 62%,#11102b);box-shadow:0 12px 32px rgba(2,8,23,.22);color:#eef4ff;overflow:hidden}.arcade-widget-card:before{content:"";position:absolute;right:-60px;bottom:-100px;width:220px;height:180px;border-radius:50%;background:rgba(124,58,237,.2);filter:blur(44px);pointer-events:none}.arcade-widget-head{position:relative;z-index:1;display:flex;align-items:center;gap:9px;margin-bottom:10px}.arcade-widget-icon{display:grid;place-items:center;width:38px;height:38px;border-radius:12px;background:linear-gradient(145deg,#7c3aed,#2563eb);color:#fff;box-shadow:0 7px 20px rgba(79,70,229,.25)}.arcade-widget-icon svg{width:21px;height:21px}.arcade-widget-head>div{display:grid;gap:1px}.arcade-widget-head strong{font-size:.72rem;letter-spacing:.09em;color:#c4b5fd}.arcade-widget-head span{font-size:.6rem;color:#71839f}.arcade-widget-open-link{margin-left:auto;display:inline-flex;align-items:center;gap:6px;padding:7px 9px;border:1px solid rgba(148,163,184,.17);border-radius:9px;background:rgba(6,15,32,.32);color:#b9c7da;font-size:.63rem;font-weight:750;text-decoration:none;white-space:nowrap}.arcade-widget-open-link:hover{border-color:rgba(167,139,250,.4);color:#ddd6fe}.arcade-widget-open-link svg{width:12px;height:12px}.arcade-widget-copy{position:relative;z-index:1}.arcade-widget-copy h2{margin:0;color:#fff;font-size:1.08rem;line-height:1.22;letter-spacing:-.02em}.arcade-widget-copy p{margin:4px 0 12px;color:#9fb0c8;font-size:.72rem;line-height:1.45}.arcade-widget-form{position:relative;z-index:1;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px}.arcade-widget-input{display:flex;align-items:center;gap:8px;min-width:0;padding:0 11px;border:1px solid rgba(148,163,184,.2);border-radius:11px;background:rgba(6,15,32,.62);transition:border-color .16s ease,box-shadow .16s ease}.arcade-widget-input:focus-within{border-color:rgba(167,139,250,.7);box-shadow:0 0 0 3px rgba(124,58,237,.12)}.arcade-widget-input.has-error{border-color:rgba(248,113,113,.72)}.arcade-widget-input svg{width:15px;height:15px;color:#8fa2bf;flex:0 0 auto}.arcade-widget-input input{box-sizing:border-box;width:100%;min-width:0;height:42px;border:0;outline:0;background:transparent;color:#eef4ff;font:inherit;font-size:.7rem}.arcade-widget-input input::placeholder{color:#64748b}.arcade-widget-form>button{display:inline-flex;align-items:center;justify-content:center;gap:7px;height:42px;padding:0 14px;border:1px solid rgba(167,139,250,.35);border-radius:11px;background:linear-gradient(90deg,#6d28d9,#7c3aed 55%,#2563eb);color:#fff;font-size:.7rem;font-weight:800;cursor:pointer;white-space:nowrap}.arcade-widget-form>button:disabled{opacity:.72;cursor:wait}.arcade-widget-form>button svg{width:15px;height:15px}.arcade-widget-error{position:relative;z-index:1;margin:7px 2px 0;color:#fca5a5;font-size:.66rem}.spin{animation:arcade-widget-spin .8s linear infinite}@keyframes arcade-widget-spin{to{transform:rotate(360deg)}}.arcade-widget-result{position:relative;z-index:1;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:18px}.arcade-widget-result-copy{display:grid;min-width:112px}.arcade-widget-result-copy>span{max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#aebdd1;font-size:.63rem}.arcade-widget-result-copy>strong{margin-top:1px;color:#fff;font-size:2rem;line-height:1}.arcade-widget-result-copy>small{margin-top:3px;color:#9fb0c8;font-size:.64rem}.arcade-widget-result-meta{display:flex;align-items:center;gap:7px 16px;flex-wrap:wrap}.arcade-widget-result-meta span{color:#b9c7da;font-size:.68rem}.arcade-widget-result-meta b{color:#ddd6fe}.arcade-widget-result-actions{display:flex;align-items:center;gap:7px;white-space:nowrap}.arcade-widget-result-actions button,.arcade-widget-result-actions a{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:38px;padding:0 11px;border-radius:10px;font-size:.66rem;font-weight:800;text-decoration:none;cursor:pointer}.arcade-widget-result-actions button{border:1px solid rgba(148,163,184,.2);background:rgba(6,15,32,.45);color:#b9c7da}.arcade-widget-result-actions a{border:1px solid rgba(167,139,250,.34);background:rgba(124,58,237,.18);color:#ddd6fe}.arcade-widget-result-actions svg{width:13px;height:13px}@media(max-width:640px){.arcade-embed-shell{padding:6px}.arcade-widget-card{padding:14px;border-radius:15px}.arcade-widget-copy h2{font-size:.98rem}.arcade-widget-copy p{font-size:.67rem}.arcade-widget-open-link{padding:6px 8px;font-size:.6rem}.arcade-widget-form{grid-template-columns:1fr}.arcade-widget-form>button{width:100%}.arcade-widget-result{grid-template-columns:auto 1fr;gap:12px}.arcade-widget-result-actions{grid-column:1/-1;display:grid;grid-template-columns:1fr 1fr}.arcade-widget-result-actions button,.arcade-widget-result-actions a{width:auto}.arcade-widget-result-meta{gap:6px 11px}.arcade-widget-result-copy>strong{font-size:1.7rem}}@media(max-width:390px){.arcade-widget-copy p{display:none}.arcade-widget-form{margin-top:10px}.arcade-widget-open-link span{display:none}.arcade-widget-open-link{padding:7px}.arcade-widget-result{grid-template-columns:1fr}.arcade-widget-result-copy{grid-template-columns:auto auto;align-items:end;column-gap:6px}.arcade-widget-result-copy>span{grid-column:1/-1}.arcade-widget-result-copy>small{margin-bottom:2px}.arcade-widget-result-actions{grid-column:auto}.arcade-widget-result-meta{margin-top:-3px}}@media(prefers-color-scheme:light){.arcade-widget-card{border-color:rgba(99,102,241,.18);background:radial-gradient(circle at 100% 0,rgba(37,99,235,.09),transparent 34%),linear-gradient(135deg,#fff,#f8faff 70%,#f5f3ff);box-shadow:0 10px 28px rgba(71,85,105,.12);color:#172033}.arcade-widget-copy h2,.arcade-widget-result-copy>strong{color:#172033}.arcade-widget-copy p,.arcade-widget-result-copy>small,.arcade-widget-result-copy>span{color:#64748b}.arcade-widget-head span{color:#94a3b8}.arcade-widget-open-link{background:#fff;border-color:rgba(99,102,241,.13);color:#64748b}.arcade-widget-open-link:hover{color:#6d28d9;border-color:#ddd6fe}.arcade-widget-input{border-color:rgba(99,102,241,.14);background:rgba(255,255,255,.78)}.arcade-widget-input input{color:#172033}.arcade-widget-input input::placeholder{color:#94a3b8}.arcade-widget-result-meta span{color:#64748b}.arcade-widget-result-meta b{color:#6d28d9}.arcade-widget-result-actions button{background:#fff;color:#64748b}.arcade-widget-result-actions a{background:#f5f3ff;color:#6d28d9}} +html,body{margin:0!important;min-height:100%;background:transparent!important}.arcade-embed-shell{box-sizing:border-box;width:100%;padding:8px;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.arcade-widget-card{box-sizing:border-box;position:relative;width:100%;max-width:760px;margin:0 auto;padding:16px;border:1px solid rgba(139,92,246,.28);border-radius:18px;background:radial-gradient(circle at 100% 0,rgba(37,99,235,.16),transparent 34%),linear-gradient(135deg,#091327,#0b1730 62%,#11102b);box-shadow:0 12px 32px rgba(2,8,23,.22);color:#eef4ff;overflow:hidden}.arcade-widget-card:before{content:"";position:absolute;right:-60px;bottom:-100px;width:220px;height:180px;border-radius:50%;background:rgba(124,58,237,.2);filter:blur(44px);pointer-events:none}.arcade-widget-head{position:relative;z-index:1;display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px;padding-bottom:11px;border-bottom:1px solid rgba(148,163,184,.12)}.arcade-widget-brand-block{display:flex;align-items:center;gap:10px;min-width:0}.arcade-widget-icon{display:grid;place-items:center;width:38px;height:38px;flex:0 0 38px;border-radius:12px;background:linear-gradient(145deg,#7c3aed,#2563eb);color:#fff;box-shadow:0 7px 20px rgba(79,70,229,.25)}.arcade-widget-icon svg{width:21px;height:21px}.arcade-widget-brand-copy{display:grid;gap:1px;min-width:0}.arcade-widget-brand-copy strong{font-size:.72rem;letter-spacing:.09em;color:#c4b5fd}.arcade-widget-brand-copy span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.6rem;color:#71839f}.arcade-widget-open-link{display:inline-flex;align-items:center;gap:6px;padding:7px 9px;flex:0 0 auto;border:1px solid rgba(167,139,250,.22);border-radius:999px;background:rgba(124,58,237,.1);color:#c4b5fd;font-size:.62rem;font-weight:750;text-decoration:none;white-space:nowrap}.arcade-widget-open-link:hover{border-color:rgba(167,139,250,.45);background:rgba(124,58,237,.18);color:#ede9fe}.arcade-widget-open-link svg{width:12px;height:12px}.arcade-widget-copy{position:relative;z-index:1}.arcade-widget-copy h2{margin:0;color:#fff;font-size:1.08rem;line-height:1.22;letter-spacing:-.02em}.arcade-widget-copy p{margin:4px 0 12px;color:#9fb0c8;font-size:.72rem;line-height:1.45}.arcade-widget-form{position:relative;z-index:1;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px}.arcade-widget-input{display:flex;align-items:center;gap:8px;min-width:0;padding:0 11px;border:1px solid rgba(148,163,184,.2);border-radius:11px;background:rgba(6,15,32,.62);transition:border-color .16s ease,box-shadow .16s ease}.arcade-widget-input:focus-within{border-color:rgba(167,139,250,.7);box-shadow:0 0 0 3px rgba(124,58,237,.12)}.arcade-widget-input.has-error{border-color:rgba(248,113,113,.72)}.arcade-widget-input svg{width:15px;height:15px;color:#8fa2bf;flex:0 0 auto}.arcade-widget-input input{box-sizing:border-box;width:100%;min-width:0;height:42px;border:0;outline:0;background:transparent;color:#eef4ff;font:inherit;font-size:.7rem}.arcade-widget-input input::placeholder{color:#64748b}.arcade-widget-form>button{display:inline-flex;align-items:center;justify-content:center;gap:7px;height:42px;padding:0 14px;border:1px solid rgba(167,139,250,.35);border-radius:11px;background:linear-gradient(90deg,#6d28d9,#7c3aed 55%,#2563eb);color:#fff;font-size:.7rem;font-weight:800;cursor:pointer;white-space:nowrap}.arcade-widget-form>button:disabled{opacity:.72;cursor:wait}.arcade-widget-form>button svg{width:15px;height:15px}.arcade-widget-error{position:relative;z-index:1;margin:7px 2px 0;color:#fca5a5;font-size:.66rem}.spin{animation:arcade-widget-spin .8s linear infinite}@keyframes arcade-widget-spin{to{transform:rotate(360deg)}}.arcade-widget-result{position:relative;z-index:1;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:18px}.arcade-widget-result-copy{display:grid;min-width:112px}.arcade-widget-result-copy>span{max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#aebdd1;font-size:.63rem}.arcade-widget-result-copy>strong{margin-top:1px;color:#fff;font-size:2rem;line-height:1}.arcade-widget-result-copy>small{margin-top:3px;color:#9fb0c8;font-size:.64rem}.arcade-widget-result-meta{display:flex;align-items:center;gap:7px 16px;flex-wrap:wrap}.arcade-widget-result-meta span{color:#b9c7da;font-size:.68rem}.arcade-widget-result-meta b{color:#ddd6fe}.arcade-widget-result-actions{display:flex;align-items:center;gap:7px;white-space:nowrap}.arcade-widget-result-actions button,.arcade-widget-result-actions a{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:38px;padding:0 11px;border-radius:10px;font-size:.66rem;font-weight:800;text-decoration:none;cursor:pointer}.arcade-widget-result-actions button{border:1px solid rgba(148,163,184,.2);background:rgba(6,15,32,.45);color:#b9c7da}.arcade-widget-result-actions a{border:1px solid rgba(167,139,250,.34);background:rgba(124,58,237,.18);color:#ddd6fe}.arcade-widget-result-actions svg{width:13px;height:13px}@media(max-width:640px){.arcade-embed-shell{padding:6px}.arcade-widget-card{padding:14px;border-radius:15px}.arcade-widget-head{align-items:center;margin-bottom:12px;padding-bottom:10px}.arcade-widget-brand-block{gap:9px}.arcade-widget-icon{width:34px;height:34px;flex-basis:34px;border-radius:10px}.arcade-widget-icon svg{width:18px;height:18px}.arcade-widget-brand-copy strong{font-size:.66rem}.arcade-widget-brand-copy span{max-width:210px;font-size:.56rem}.arcade-widget-open-link{padding:6px 8px;font-size:.58rem}.arcade-widget-copy h2{font-size:.98rem}.arcade-widget-copy p{font-size:.67rem}.arcade-widget-form{grid-template-columns:1fr}.arcade-widget-form>button{width:100%}.arcade-widget-result{grid-template-columns:auto 1fr;gap:12px}.arcade-widget-result-actions{grid-column:1/-1;display:grid;grid-template-columns:1fr 1fr}.arcade-widget-result-actions button,.arcade-widget-result-actions a{width:auto}.arcade-widget-result-meta{gap:6px 11px}.arcade-widget-result-copy>strong{font-size:1.7rem}}@media(max-width:460px){.arcade-widget-head{display:grid;grid-template-columns:minmax(0,1fr) auto}.arcade-widget-brand-copy span{max-width:175px}.arcade-widget-open-link span{display:none}.arcade-widget-open-link{width:32px;height:32px;padding:0;justify-content:center;border-radius:10px}.arcade-widget-open-link svg{width:14px;height:14px}}@media(max-width:390px){.arcade-widget-copy p{display:none}.arcade-widget-form{margin-top:10px}.arcade-widget-brand-copy span{max-width:150px}.arcade-widget-result{grid-template-columns:1fr}.arcade-widget-result-copy{grid-template-columns:auto auto;align-items:end;column-gap:6px}.arcade-widget-result-copy>span{grid-column:1/-1}.arcade-widget-result-copy>small{margin-bottom:2px}.arcade-widget-result-actions{grid-column:auto}.arcade-widget-result-meta{margin-top:-3px}}@media(prefers-color-scheme:light){.arcade-widget-card{border-color:rgba(99,102,241,.18);background:radial-gradient(circle at 100% 0,rgba(37,99,235,.09),transparent 34%),linear-gradient(135deg,#fff,#f8faff 70%,#f5f3ff);box-shadow:0 10px 28px rgba(71,85,105,.12);color:#172033}.arcade-widget-head{border-bottom-color:rgba(99,102,241,.1)}.arcade-widget-copy h2,.arcade-widget-result-copy>strong{color:#172033}.arcade-widget-copy p,.arcade-widget-result-copy>small,.arcade-widget-result-copy>span{color:#64748b}.arcade-widget-brand-copy span{color:#94a3b8}.arcade-widget-open-link{background:#f5f3ff;border-color:#ddd6fe;color:#6d28d9}.arcade-widget-open-link:hover{background:#ede9fe;color:#5b21b6}.arcade-widget-input{border-color:rgba(99,102,241,.14);background:rgba(255,255,255,.78)}.arcade-widget-input input{color:#172033}.arcade-widget-input input::placeholder{color:#94a3b8}.arcade-widget-result-meta span{color:#64748b}.arcade-widget-result-meta b{color:#6d28d9}.arcade-widget-result-actions button{background:#fff;color:#64748b}.arcade-widget-result-actions a{background:#f5f3ff;color:#6d28d9}} From d42a5dc3277bd3c8e8c14f8aca67dbe0eff436c0 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 9 Aug 2026 11:29:14 +0700 Subject: [PATCH 13/14] Fix widget hydration for query parameters --- components/arcade/arcade-embed-widget.tsx | 28 ++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/components/arcade/arcade-embed-widget.tsx b/components/arcade/arcade-embed-widget.tsx index 674f86c0f..7073f0759 100644 --- a/components/arcade/arcade-embed-widget.tsx +++ b/components/arcade/arcade-embed-widget.tsx @@ -2,7 +2,7 @@ import { ExternalLink, Gamepad2, LoaderCircle, Search, Trophy } from "lucide-react" import type { FormEvent } from "react" -import { useMemo, useState } from "react" +import { useEffect, useState } from "react" import { getFacilitatorAdjustedPoints } from "@/components/arcade/facilitator-points" import { readFacilitatorParticipation } from "@/components/arcade/facilitator-participation" import { @@ -14,32 +14,34 @@ import { } from "@/components/arcade/model" import type { ArcadeApiResponse } from "@/components/arcade/model" +const DEFAULT_DASHBOARD_URL = "https://arcade.eplus.dev/" + function sanitizeUtm(value: string | null, fallback: string): string { if (!value) return fallback return /^[a-zA-Z0-9._-]{1,80}$/.test(value) ? value : fallback } -function readInitialProfileUrl(): string { - if (typeof window === "undefined") return "" - const value = new URLSearchParams(window.location.search).get("url")?.trim() ?? "" - return PROFILE_URL_PATTERN.test(value.replace(/\/$/, "")) ? value : "" -} - export default function ArcadeEmbedWidget() { - const [profileUrl, setProfileUrl] = useState(readInitialProfileUrl) + const [profileUrl, setProfileUrl] = useState("") + const [fullResultUrl, setFullResultUrl] = useState(DEFAULT_DASHBOARD_URL) const [loading, setLoading] = useState(false) const [error, setError] = useState("") const [result, setResult] = useState(null) - const fullResultUrl = useMemo(() => { - const target = new URL("https://arcade.eplus.dev/") - if (typeof window === "undefined") return target.toString() - + useEffect(() => { const params = new URLSearchParams(window.location.search) + const requestedProfileUrl = params.get("url")?.trim() ?? "" + const normalizedRequestedProfileUrl = requestedProfileUrl.replace(/\/$/, "") + + if (PROFILE_URL_PATTERN.test(normalizedRequestedProfileUrl)) { + setProfileUrl(requestedProfileUrl) + } + + const target = new URL(DEFAULT_DASHBOARD_URL) target.searchParams.set("utm_source", sanitizeUtm(params.get("utm_source"), "hashnode")) target.searchParams.set("utm_medium", sanitizeUtm(params.get("utm_medium"), "widget")) target.searchParams.set("utm_campaign", sanitizeUtm(params.get("utm_campaign"), "arcade-widget")) - return target.toString() + setFullResultUrl(target.toString()) }, []) const normalizedProfileUrl = profileUrl.trim().replace(/\/$/, "") From 5a625a5dc0594b5ed5710fc453571e3fff3f6f68 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 9 Aug 2026 11:29:44 +0700 Subject: [PATCH 14/14] Restrict cookie suppression to widget routes --- components/privacy/cookie-consent.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/privacy/cookie-consent.tsx b/components/privacy/cookie-consent.tsx index c8dffc264..1314a9460 100644 --- a/components/privacy/cookie-consent.tsx +++ b/components/privacy/cookie-consent.tsx @@ -10,6 +10,7 @@ export const COOKIE_PREFERENCES_EVENT = "arcade:open-cookie-preferences" const COOKIE_NOTICE_STORAGE_KEY = "arcade-cookie-notice-v1" const COOKIE_NOTICE_PREVIEW_STORAGE_KEY = "arcade-cookie-notice-preview-v1" +const WIDGET_PATH_PATTERN = /^\/(?:[a-z]{2}(?:-[a-z]{2})?\/)?widget\/?$/i const COPY = { en: { eyebrow:"Cookie information", title:"We use cookies and analytics", description:"Essential storage supports core features. Google Analytics measures anonymous usage so we can improve the service.", usageTitle:"Cookie usage details", essentialTitle:"Essential storage", essentialStatus:"Always active", essentialDescription:"Stores interface preferences such as theme, language, and recent calculator results on your device.", analyticsTitle:"Google Analytics", analyticsStatus:"Enabled", analyticsPreviewStatus:"Disabled in preview", analyticsDescription:"Collects aggregated information such as page visits and browser or device details. It does not provide access to your Google account or private profile data.", preview:"Preview mode: this notice is displayed for review, but Google Analytics is not loaded on the preview URL.", moreTitle:"More information", moreDescription:"Read the Privacy Policy for details about stored information, third-party services, and contact options.", privacy:"Privacy Policy", showDetails:"View details", hideDetails:"Hide details", acknowledge:"I understand", close:"Close cookie information" }, @@ -25,7 +26,7 @@ export default function CookieConsent({analyticsEnabled,previewMode=false}:Cooki const pathname=usePathname() const [isOpen,setIsOpen]=useState(false) const [showDetails,setShowDetails]=useState(false) - const isEmbedWidget=pathname === "/widget" || pathname?.endsWith("/widget") || pathname?.endsWith("/widget/") + const isEmbedWidget=WIDGET_PATH_PATTERN.test(pathname??"") const copy=useMemo(()=>getWebsiteLocaleFromPathname(pathname??"/")==="vi"?COPY.vi:COPY.en,[pathname]) const storageKey=previewMode?COOKIE_NOTICE_PREVIEW_STORAGE_KEY:COOKIE_NOTICE_STORAGE_KEY const noticeEnabled=(analyticsEnabled||previewMode)&&!isEmbedWidget