Conversation
a1f3178 to
595095e
Compare
There was a problem hiding this comment.
Hey,我发现了 3 个问题,并给出了一些整体性反馈:
- 现在
.gradient-border类上添加了pointer-events: none,这会导致容器内部的所有点击/输入都失效(例如演示中的输入框和按钮);建议只将pointer-events: none应用于装饰性的伪元素,或者单独的遮罩层容器,而不是内容容器本身。 Home组件中的typeText函数创建了一个 interval,但在组件卸载或者新一轮模拟开始时从未被清理,这可能导致内存泄漏以及在已卸载组件上更新状态;建议用 ref 记录该 interval,并在清理 effect 中或在开始新的模拟前清除它。- 主题/语言环境(locale)处理以及部分图标在
app/page.tsx与app/exchange/page.tsx之间存在重复;建议抽取共享的 hooks/组件(例如带主题/语言切换的布局头部),以减少重复并保持各页面行为一致。
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `.gradient-border` class now has `pointer-events: none`, which will prevent all clicks/inputs inside those containers (e.g., the demo input and buttons) from working; consider applying `pointer-events: none` only to the decorative pseudo-element or a separate overlay wrapper instead of the content container.
- The `typeText` function in `Home` creates an interval that is never cleared on unmount or when a new simulation starts, which can cause memory leaks and state updates on unmounted components; consider tracking the interval with a ref and clearing it in a cleanup effect or before starting a new simulation.
- The theme/locale handling and several icons are duplicated between `app/page.tsx` and `app/exchange/page.tsx`; consider extracting shared hooks/components (e.g., a layout header with theme/locale toggles) to reduce duplication and keep behavior consistent across pages.
## Individual Comments
### Comment 1
<location> `app/globals.css:100` </location>
<code_context>
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
+ pointer-events: none;
}
</code_context>
<issue_to_address>
**issue (bug_risk):** Using `pointer-events: none` on `.gradient-border` likely disables interaction for its children.
On HTML elements, `pointer-events: none` also disables pointer events for all descendants, so wrapping inputs/buttons in `.gradient-border` will likely make them unclickable. If you just want the decorative border to ignore pointer events, move that styling to a pseudo-element (e.g. `.gradient-border::before`) with `pointer-events: none` and keep the main container interactive.
</issue_to_address>
### Comment 2
<location> `app/page.tsx:182-191` </location>
<code_context>
};
+ // 流式输出单行文字
+ const typeText = (text: string, onComplete: () => void) => {
+ let index = 0;
+ setCurrentTypingText("");
+ const interval = setInterval(() => {
+ if (index < text.length) {
+ setCurrentTypingText(text.slice(0, index + 1));
+ index++;
+ } else {
+ clearInterval(interval);
+ onComplete();
+ }
+ }, 30 + Math.random() * 20); // 随机延迟模拟真实打字效果
+ return interval;
+ };
+
</code_context>
<issue_to_address>
**issue (bug_risk):** The typing simulation interval is never cleaned up on component unmount.
The interval created in `typeText` is only cleared when typing completes. If the component unmounts mid-typing (e.g. during navigation), it can keep running and call `setState` on an unmounted component. Please store the interval id in a ref and clear it in a `useEffect` cleanup, or refactor to a `setTimeout` + effect pattern that automatically cancels on unmount.
</issue_to_address>
### Comment 3
<location> `app/page.tsx:240-248` </location>
<code_context>
+ setIsSimulating(false);
+
+ // 自动跳转到下载页面
+ if (DOWNLOAD_URL) {
+ setTimeout(() => {
+ window.open(DOWNLOAD_URL, "_blank");
+ }, 1000);
+ }
</code_context>
<issue_to_address>
**🚨 suggestion (security):** Opening a new window without `noopener`/`noreferrer` can introduce a security risk.
`window.open(DOWNLOAD_URL, "_blank")` lets the new tab access and potentially modify `window.opener`. To mitigate reverse tabnabbing, use `window.open(DOWNLOAD_URL, "_blank", "noopener,noreferrer")` or set `newWindow.opener = null` after opening.
```suggestion
setSimulationComplete(true);
setIsSimulating(false);
// 自动跳转到下载页面
if (DOWNLOAD_URL) {
setTimeout(() => {
const newWindow = window.open(DOWNLOAD_URL, "_blank", "noopener,noreferrer");
if (newWindow) {
newWindow.opener = null;
}
}, 1000);
}
```
</issue_to_address>帮我变得更有用!请对每条评论点 👍 或 👎,我会根据你的反馈改进后续评审。
Original comment in English
Hey - I've found 3 issues, and left some high level feedback:
- The
.gradient-borderclass now haspointer-events: none, which will prevent all clicks/inputs inside those containers (e.g., the demo input and buttons) from working; consider applyingpointer-events: noneonly to the decorative pseudo-element or a separate overlay wrapper instead of the content container. - The
typeTextfunction inHomecreates an interval that is never cleared on unmount or when a new simulation starts, which can cause memory leaks and state updates on unmounted components; consider tracking the interval with a ref and clearing it in a cleanup effect or before starting a new simulation. - The theme/locale handling and several icons are duplicated between
app/page.tsxandapp/exchange/page.tsx; consider extracting shared hooks/components (e.g., a layout header with theme/locale toggles) to reduce duplication and keep behavior consistent across pages.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `.gradient-border` class now has `pointer-events: none`, which will prevent all clicks/inputs inside those containers (e.g., the demo input and buttons) from working; consider applying `pointer-events: none` only to the decorative pseudo-element or a separate overlay wrapper instead of the content container.
- The `typeText` function in `Home` creates an interval that is never cleared on unmount or when a new simulation starts, which can cause memory leaks and state updates on unmounted components; consider tracking the interval with a ref and clearing it in a cleanup effect or before starting a new simulation.
- The theme/locale handling and several icons are duplicated between `app/page.tsx` and `app/exchange/page.tsx`; consider extracting shared hooks/components (e.g., a layout header with theme/locale toggles) to reduce duplication and keep behavior consistent across pages.
## Individual Comments
### Comment 1
<location> `app/globals.css:100` </location>
<code_context>
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
+ pointer-events: none;
}
</code_context>
<issue_to_address>
**issue (bug_risk):** Using `pointer-events: none` on `.gradient-border` likely disables interaction for its children.
On HTML elements, `pointer-events: none` also disables pointer events for all descendants, so wrapping inputs/buttons in `.gradient-border` will likely make them unclickable. If you just want the decorative border to ignore pointer events, move that styling to a pseudo-element (e.g. `.gradient-border::before`) with `pointer-events: none` and keep the main container interactive.
</issue_to_address>
### Comment 2
<location> `app/page.tsx:182-191` </location>
<code_context>
};
+ // 流式输出单行文字
+ const typeText = (text: string, onComplete: () => void) => {
+ let index = 0;
+ setCurrentTypingText("");
+ const interval = setInterval(() => {
+ if (index < text.length) {
+ setCurrentTypingText(text.slice(0, index + 1));
+ index++;
+ } else {
+ clearInterval(interval);
+ onComplete();
+ }
+ }, 30 + Math.random() * 20); // 随机延迟模拟真实打字效果
+ return interval;
+ };
+
</code_context>
<issue_to_address>
**issue (bug_risk):** The typing simulation interval is never cleaned up on component unmount.
The interval created in `typeText` is only cleared when typing completes. If the component unmounts mid-typing (e.g. during navigation), it can keep running and call `setState` on an unmounted component. Please store the interval id in a ref and clear it in a `useEffect` cleanup, or refactor to a `setTimeout` + effect pattern that automatically cancels on unmount.
</issue_to_address>
### Comment 3
<location> `app/page.tsx:240-248` </location>
<code_context>
+ setIsSimulating(false);
+
+ // 自动跳转到下载页面
+ if (DOWNLOAD_URL) {
+ setTimeout(() => {
+ window.open(DOWNLOAD_URL, "_blank");
+ }, 1000);
+ }
</code_context>
<issue_to_address>
**🚨 suggestion (security):** Opening a new window without `noopener`/`noreferrer` can introduce a security risk.
`window.open(DOWNLOAD_URL, "_blank")` lets the new tab access and potentially modify `window.opener`. To mitigate reverse tabnabbing, use `window.open(DOWNLOAD_URL, "_blank", "noopener,noreferrer")` or set `newWindow.opener = null` after opening.
```suggestion
setSimulationComplete(true);
setIsSimulating(false);
// 自动跳转到下载页面
if (DOWNLOAD_URL) {
setTimeout(() => {
const newWindow = window.open(DOWNLOAD_URL, "_blank", "noopener,noreferrer");
if (newWindow) {
newWindow.opener = null;
}
}, 1000);
}
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); | ||
| -webkit-mask-composite: xor; | ||
| mask-composite: exclude; | ||
| pointer-events: none; |
There was a problem hiding this comment.
issue (bug_risk): 在 .gradient-border 上使用 pointer-events: none 很可能会禁用其子元素的交互。
在 HTML 元素上,pointer-events: none 同样会禁用所有后代元素的指针事件,因此将输入框/按钮包裹在 .gradient-border 中,很可能会导致它们无法点击。如果你只是希望装饰性的边框不响应指针事件,可以把这个样式移动到伪元素上(例如 .gradient-border::before)并设置 pointer-events: none,同时保持主容器仍然可交互。
Original comment in English
issue (bug_risk): Using pointer-events: none on .gradient-border likely disables interaction for its children.
On HTML elements, pointer-events: none also disables pointer events for all descendants, so wrapping inputs/buttons in .gradient-border will likely make them unclickable. If you just want the decorative border to ignore pointer events, move that styling to a pseudo-element (e.g. .gradient-border::before) with pointer-events: none and keep the main container interactive.
| const typeText = (text: string, onComplete: () => void) => { | ||
| let index = 0; | ||
| setCurrentTypingText(""); | ||
| const interval = setInterval(() => { | ||
| if (index < text.length) { | ||
| setCurrentTypingText(text.slice(0, index + 1)); | ||
| index++; | ||
| } else { | ||
| clearInterval(interval); | ||
| onComplete(); |
There was a problem hiding this comment.
issue (bug_risk): 打字模拟用到的 interval 在组件卸载时从未被清理。
typeText 中创建的 interval 只会在打字完成时被清除。如果组件在打字过程中卸载(例如导航到其他页面),该 interval 仍会继续运行,并在已卸载组件上调用 setState。请将 interval 的 id 存在一个 ref 中,并在 useEffect 的清理函数中清除它,或者重构为 setTimeout + effect 的模式,使其在组件卸载时自动取消。
Original comment in English
issue (bug_risk): The typing simulation interval is never cleaned up on component unmount.
The interval created in typeText is only cleared when typing completes. If the component unmounts mid-typing (e.g. during navigation), it can keep running and call setState on an unmounted component. Please store the interval id in a ref and clear it in a useEffect cleanup, or refactor to a setTimeout + effect pattern that automatically cancels on unmount.
| setSimulationComplete(true); | ||
| setIsSimulating(false); | ||
|
|
||
| // 自动跳转到下载页面 | ||
| if (DOWNLOAD_URL) { | ||
| setTimeout(() => { | ||
| window.open(DOWNLOAD_URL, "_blank"); | ||
| }, 1000); | ||
| } |
There was a problem hiding this comment.
🚨 suggestion (security): 在没有使用 noopener/noreferrer 的情况下打开新窗口会带来安全风险。
window.open(DOWNLOAD_URL, "_blank") 允许新标签访问并潜在地修改 window.opener。为了缓解反向标签劫持(reverse tabnabbing)风险,请使用 window.open(DOWNLOAD_URL, "_blank", "noopener,noreferrer"),或者在打开之后设置 newWindow.opener = null。
| setSimulationComplete(true); | |
| setIsSimulating(false); | |
| // 自动跳转到下载页面 | |
| if (DOWNLOAD_URL) { | |
| setTimeout(() => { | |
| window.open(DOWNLOAD_URL, "_blank"); | |
| }, 1000); | |
| } | |
| setSimulationComplete(true); | |
| setIsSimulating(false); | |
| // 自动跳转到下载页面 | |
| if (DOWNLOAD_URL) { | |
| setTimeout(() => { | |
| const newWindow = window.open(DOWNLOAD_URL, "_blank", "noopener,noreferrer"); | |
| if (newWindow) { | |
| newWindow.opener = null; | |
| } | |
| }, 1000); | |
| } |
Original comment in English
🚨 suggestion (security): Opening a new window without noopener/noreferrer can introduce a security risk.
window.open(DOWNLOAD_URL, "_blank") lets the new tab access and potentially modify window.opener. To mitigate reverse tabnabbing, use window.open(DOWNLOAD_URL, "_blank", "noopener,noreferrer") or set newWindow.opener = null after opening.
| setSimulationComplete(true); | |
| setIsSimulating(false); | |
| // 自动跳转到下载页面 | |
| if (DOWNLOAD_URL) { | |
| setTimeout(() => { | |
| window.open(DOWNLOAD_URL, "_blank"); | |
| }, 1000); | |
| } | |
| setSimulationComplete(true); | |
| setIsSimulating(false); | |
| // 自动跳转到下载页面 | |
| if (DOWNLOAD_URL) { | |
| setTimeout(() => { | |
| const newWindow = window.open(DOWNLOAD_URL, "_blank", "noopener,noreferrer"); | |
| if (newWindow) { | |
| newWindow.opener = null; | |
| } | |
| }, 1000); | |
| } |
There was a problem hiding this comment.
Pull request overview
This PR introduces a new CDK (activation code) exchange page that allows users to redeem codes for trial access. The implementation includes full localization support (English and Chinese), theme toggling, backend API integration, and an interactive demo simulation on the home page.
Key Changes:
- New
/exchangepage with form validation and API integration for CDK redemption - Enhanced home page demo with terminal-style simulation showing browser limitations
- Updated navigation to prioritize CDK exchange over GitHub repository link
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 21 comments.
Show a summary per file
| File | Description |
|---|---|
| locales/zh.json | Added Chinese translations for exchange page and demo simulation messages |
| locales/en.json | Added English translations for exchange page and demo simulation messages |
| app/page.tsx | Added gift icon, simulation logic with typing effects, replaced primary CTA with exchange link |
| app/globals.css | Added pointer-events: none to gradient borders to prevent interaction issues |
| app/exchange/page.tsx | New page implementing CDK exchange functionality with modal success dialog |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| {showModal && result && ( | ||
| <div className="fixed inset-0 z-50 flex items-center justify-center p-4"> | ||
| <div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={closeModal}></div> | ||
| <div className="relative bg-surface rounded-2xl p-8 max-w-md w-full shadow-2xl border border-border"> | ||
| <button | ||
| onClick={closeModal} | ||
| className="absolute top-4 right-4 p-2 rounded-lg hover:bg-surface-light transition-colors text-foreground/50 hover:text-foreground" | ||
| > | ||
| <CloseIcon /> | ||
| </button> | ||
|
|
||
| <div className="text-center mb-6"> | ||
| <div className="w-16 h-16 rounded-full bg-green-500/10 flex items-center justify-center mx-auto mb-4"> | ||
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="w-8 h-8 text-green-500"> | ||
| <polyline points="20 6 9 17 4 12"/> | ||
| </svg> | ||
| </div> | ||
| <h2 className="text-2xl font-bold text-foreground">{t.exchange.successTitle}</h2> | ||
| </div> | ||
|
|
||
| <div className="space-y-4"> | ||
| {/* New CDK */} | ||
| <div className="p-4 rounded-lg bg-surface-light border border-border"> | ||
| <div className="text-sm text-foreground/50 mb-2">{t.exchange.newCdk}</div> | ||
| <div className="flex items-center gap-2"> | ||
| <code className="flex-1 font-mono text-sm text-accent break-all">{result.new_cdk}</code> | ||
| <button | ||
| onClick={handleCopy} | ||
| className="p-2 rounded-lg hover:bg-surface transition-colors text-foreground/50 hover:text-foreground flex-shrink-0" | ||
| title={t.exchange.copy} | ||
| > | ||
| {copied ? <CheckIcon /> : <CopyIcon />} | ||
| </button> | ||
| </div> | ||
| {copied && ( | ||
| <div className="text-xs text-green-500 mt-2">{t.exchange.copySuccess}</div> | ||
| )} | ||
| </div> | ||
|
|
||
| {/* Stats */} | ||
| <div className="grid grid-cols-2 gap-4"> | ||
| <div className="p-4 rounded-lg bg-surface-light border border-border text-center"> | ||
| <div className="text-sm text-foreground/50 mb-1">{t.exchange.points}</div> | ||
| <div className="text-2xl font-bold text-accent">{result.points}</div> | ||
| </div> | ||
| <div className="p-4 rounded-lg bg-surface-light border border-border text-center"> | ||
| <div className="text-sm text-foreground/50 mb-1">{t.exchange.remainingDays}</div> | ||
| <div className="text-2xl font-bold text-foreground">{result.remaining_days}</div> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Expiry */} | ||
| <div className="p-4 rounded-lg bg-surface-light border border-border"> | ||
| <div className="text-sm text-foreground/50 mb-1">{t.exchange.expiredAt}</div> | ||
| <div className="text-foreground font-medium">{result.expired_at}</div> | ||
| </div> | ||
|
|
||
| <button | ||
| onClick={closeModal} | ||
| className="w-full btn-secondary py-3" | ||
| > | ||
| {t.exchange.close} | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
The modal backdrop does not trap focus within the modal dialog. When the modal is open, users can still tab to elements behind it. Consider adding focus trap functionality to ensure keyboard navigation stays within the modal, and set focus to the first interactive element when the modal opens. Also, the close button and backdrop should handle Escape key press for better accessibility.
| /> | ||
| </div> | ||
|
|
||
| {error && ( | ||
| <div className="p-4 rounded-lg bg-red-500/10 border border-red-500/30 text-red-500 text-sm text-center"> |
There was a problem hiding this comment.
The input field lacks proper validation feedback for accessibility. When an error occurs, the error message should be associated with the input using aria-describedby to ensure screen reader users are informed of the validation error. Consider adding an id to the error div and referencing it in the input's aria-describedby attribute.
| /> | |
| </div> | |
| {error && ( | |
| <div className="p-4 rounded-lg bg-red-500/10 border border-red-500/30 text-red-500 text-sm text-center"> | |
| aria-invalid={!!error} | |
| aria-describedby={error ? "cdk-error" : undefined} | |
| /> | |
| </div> | |
| {error && ( | |
| <div | |
| id="cdk-error" | |
| className="p-4 rounded-lg bg-red-500/10 border border-red-500/30 text-red-500 text-sm text-center" | |
| > |
| </div> | ||
| )} | ||
|
|
||
| {/* 示例命令列表 */} |
There was a problem hiding this comment.
The comment is in Chinese. Should be translated to English: "// Example command list"
| {/* 示例命令列表 */} | |
| {/* Example command list */} |
| return; | ||
| } | ||
|
|
||
| //验证 CDK 格式(20-32位字母数字) |
There was a problem hiding this comment.
The comment is in Chinese. Should be translated to English: "// Validate CDK format (20-32 alphanumeric characters)"
| //验证 CDK 格式(20-32位字母数字) | |
| // Validate CDK format (20-32 alphanumeric characters) |
| // Fallback for older browsers | ||
| const textArea = document.createElement("textarea"); | ||
| textArea.value = result.new_cdk; | ||
| document.body.appendChild(textArea); | ||
| textArea.select(); | ||
| document.execCommand("copy"); | ||
| document.body.removeChild(textArea); | ||
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 2000); |
There was a problem hiding this comment.
Using document.execCommand('copy') is deprecated and may not work in all modern browsers. Since you already have a try-catch block with navigator.clipboard.writeText, the fallback should handle the error more gracefully or remove this deprecated approach entirely. Consider showing an error message to the user if clipboard access fails.
| // Fallback for older browsers | |
| const textArea = document.createElement("textarea"); | |
| textArea.value = result.new_cdk; | |
| document.body.appendChild(textArea); | |
| textArea.select(); | |
| document.execCommand("copy"); | |
| document.body.removeChild(textArea); | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 2000); | |
| // Handle clipboard access failure gracefully without using deprecated APIs | |
| setError( | |
| (t.exchange?.errors && | |
| (t.exchange.errors.clipboardFailed || | |
| t.exchange.errors.generic)) || | |
| "Failed to copy to clipboard. Please copy the code manually." | |
| ); |
| const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.example.com"; | ||
|
|
There was a problem hiding this comment.
The API_BASE_URL fallback value "https://api.example.com" is a non-functional placeholder domain. This will cause all API requests to fail if the environment variable is not set. Consider using a more descriptive error or at least documenting that NEXT_PUBLIC_API_BASE_URL must be configured before deployment.
| const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.example.com"; | |
| const API_BASE_URL_ENV = process.env.NEXT_PUBLIC_API_BASE_URL; | |
| if (!API_BASE_URL_ENV) { | |
| throw new Error( | |
| "NEXT_PUBLIC_API_BASE_URL is not set. Please configure this environment variable before deploying the exchange page." | |
| ); | |
| } | |
| const API_BASE_URL = API_BASE_URL_ENV; |
| <a href="/exchange" className="btn-primary inline-flex items-center justify-center gap-2"> | ||
| <GiftIcon /> | ||
| {t.hero.exchangeCdk} | ||
| </a> |
There was a problem hiding this comment.
The primary call-to-action button in the hero section was changed from linking to the GitHub repository (https://github.com/MaaXYZ/MaaMCP) to the exchange page (/exchange). This significantly changes the user journey - the original "Get Started" action directed users to the main project repository, while now it directs to a CDK exchange page. This may not align with the primary goal for first-time visitors who want to learn about or use the product. Consider keeping the GitHub link as the primary CTA and making the exchange link secondary, or adding both options.
| <a href="/exchange" className="btn-primary inline-flex items-center justify-center gap-2"> | |
| <GiftIcon /> | |
| {t.hero.exchangeCdk} | |
| </a> | |
| <a href="https://github.com/MaaXYZ/MaaMCP" className="btn-primary inline-flex items-center justify-center gap-2"> | |
| <GiftIcon /> | |
| {t.hero.exchangeCdk} | |
| </a> | |
| <a href="/exchange" className="btn-secondary inline-flex items-center justify-center gap-2"> | |
| <GiftIcon /> | |
| {t.hero.exchangeCdk} | |
| </a> |
| {useCases.map((useCase, index) => ( | ||
| <button | ||
| key={index} | ||
| onClick={() => { |
There was a problem hiding this comment.
The runSimulation function can be triggered multiple times by clicking example commands even when a simulation is already running, due to the check happening after the click handler sets the command. The example buttons at lines 458-472 call runSimulation(useCase.command) directly but the disabled state only prevents clicking the same button again, not starting a new simulation from a different example button. Consider checking isSimulating before calling runSimulation in the example button handlers.
| onClick={() => { | |
| onClick={() => { | |
| if (isSimulating) { | |
| return; | |
| } |
| clearInterval(interval); | ||
| onComplete(); | ||
| } | ||
| }, 30 + Math.random() * 20); // 随机延迟模拟真实打字效果 |
There was a problem hiding this comment.
The typing delay uses 30 + Math.random() * 20 which generates a new random value on every character. This creates inconsistent timing between characters within the same line. For a more realistic typing effect, consider generating the delay once per line or using a consistent delay value. The current implementation also makes the typing speed unpredictable for testing purposes.
| return "en"; | ||
| } | ||
|
|
||
| // 下载链接(待填写) |
There was a problem hiding this comment.
The comment is in Chinese while the codebase appears to use English for comments. This should be translated to English for consistency: "// Download link (to be filled in)"
Summary by Sourcery
添加一个专用的 CDK 兑换页面,并增强主页演示,加入交互式模拟与下载流程。
新功能:
/exchange页面,支持本地化、主题定制、API 集成以及兑换成功弹窗界面。功能改进:
Original summary in English
Summary by Sourcery
Add a dedicated CDK exchange page and enhance the homepage demo with an interactive simulation and download flow.
New Features:
Enhancements:
Original summary in English
Summary by Sourcery
添加一个专用的 CDK 兑换页面,并增强主页演示,加入交互式模拟与下载流程。
新功能:
/exchange页面,支持本地化、主题定制、API 集成以及兑换成功弹窗界面。功能改进:
Original summary in English
Summary by Sourcery
Add a dedicated CDK exchange page and enhance the homepage demo with an interactive simulation and download flow.
New Features:
Enhancements: