diff --git a/app/widget/widget.css b/app/widget/widget.css index dab0eb3aa..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;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-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}} diff --git a/components/arcade/arcade-embed-widget.tsx b/components/arcade/arcade-embed-widget.tsx index 4866320b6..7073f0759 100644 --- a/components/arcade/arcade-embed-widget.tsx +++ b/components/arcade/arcade-embed-widget.tsx @@ -1,7 +1,20 @@ "use client" -import { Calendar, ExternalLink, Gamepad2, GraduationCap, Trophy } from "lucide-react" -import { useMemo } from "react" +import { ExternalLink, Gamepad2, LoaderCircle, Search, Trophy } from "lucide-react" +import type { FormEvent } from "react" +import { useEffect, 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" + +const DEFAULT_DASHBOARD_URL = "https://arcade.eplus.dev/" function sanitizeUtm(value: string | null, fallback: string): string { if (!value) return fallback @@ -9,61 +22,162 @@ 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" - } + 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) + useEffect(() => { 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 requestedProfileUrl = params.get("url")?.trim() ?? "" + const normalizedRequestedProfileUrl = requestedProfileUrl.replace(/\/$/, "") - 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) + if (PROFILE_URL_PATTERN.test(normalizedRequestedProfileUrl)) { + setProfileUrl(requestedProfileUrl) + } - return target.toString() + 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")) + setFullResultUrl(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 (!PROFILE_URL_PATTERN.test(normalized)) { + setError("Paste a valid public profile URL from skills.google.") + setResult(null) + return + } + + 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 POINTS - by ePlus.DEV + Google Cloud Arcade tracker · ePlus.DEV
- FREE + + Open full dashboard +
-
-
-

Google Cloud Arcade Tracker

-

Track your Arcade progress in one place.

-

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

-
+ {!result ? ( + <> +
+

Check your Google Cloud Arcade score

+

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

+
-
- Points & tiers - Monthly games - Facilitator bonus +
+ + +
+ {error ?

{error}

: null} + + ) : ( +
+
+ {userName} + {totalPoints} + Arcade points +
+
+ {tier.name} tier + {badgeCount} badges + {facilitatorScore && facilitatorScore.bonus > 0 ? ( + +{facilitatorScore.bonus} Facilitator bonus + ) : null} +
+
+ + + View full result + +
-
- -
- arcade.eplus.dev - - Check your Arcade score - - -
+ )}
) diff --git a/components/privacy/cookie-consent.tsx b/components/privacy/cookie-consent.tsx index 633ea04aa..1314a9460 100644 --- a/components/privacy/cookie-consent.tsx +++ b/components/privacy/cookie-consent.tsx @@ -10,230 +10,28 @@ 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", - }, - 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=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 + 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}
} 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"