Conversation
TestGru AssignmentSummary
Tip You can |
Preview mcp_server Image: |
Preview sandbox Image: |
Preview fastgpt Image: |
There was a problem hiding this comment.
Pull request overview
This PR adds a promotional activity configuration system for subscription plans, including a new activity advertisement modal, visual enhancements with winter/holiday theming (snowflake decorations), and support for activity-based bonus points.
Key Changes
- New activity ad modal component with localStorage-based dismissal tracking (24-hour cooldown)
- Default subscription mode changed from monthly to yearly
- Activity expiration time display and bonus points visualization across pricing components
Reviewed changes
Copilot reviewed 11 out of 21 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
projects/app/src/web/common/system/api.ts |
Added new API endpoint getActivityAd for fetching activity advertisements |
projects/app/src/pageComponents/price/Standard.tsx |
Added activity expiration banner, snowflake decorations, bonus points display, and changed default subscription mode to yearly |
projects/app/src/pageComponents/price/ExtraPlan.tsx |
Added activity-themed decorations and bonus points badges to extra plan packages |
projects/app/src/components/support/wallet/StandardPlanContentList.tsx |
Added strike-through pricing and bonus points display for annual plans during activities |
projects/app/src/components/support/activity/ActivityAdModal.tsx |
New modal component for displaying activity promotions with localStorage-based dismissal logic |
projects/app/src/components/Layout/index.tsx |
Integrated ActivityAdModal into main layout |
projects/app/public/imgs/system/*.svg |
Added 7 new SVG files for snowflake and ribbon decorative elements |
packages/web/i18n/*/common.json |
Added translation keys for activity ad modal in English, Simplified Chinese, and Traditional Chinese |
packages/web/components/common/Icon/icons/price/right.svg |
Modified SVG to support dynamic fill color instead of hardcoded color |
packages/web/common/file/hooks/useUploadAvatar.tsx |
Enhanced hook to accept configurable image compression parameters |
packages/global/support/wallet/sub/type.d.ts |
Added annualBonusPoints and activityBonusPoints fields for promotional features |
packages/global/common/system/config/constants.ts |
Added activityAd configuration type |
Comments suppressed due to low confidence (1)
packages/web/common/file/hooks/useUploadAvatar.tsx:61
- Missing dependencies in the
handleUploadAvatarcallback. The dependency array on line 61 is missingmaxW,maxH, andmaxSizewhich are used in the function body on lines 45-47. This could lead to stale closures where the callback uses outdated values if these parameters change.
const handleUploadAvatar = useCallback(
async (file: File) => {
if (!file.name.match(/\.(jpg|png|jpeg)$/)) {
toast({ title: t('account_info:avatar_can_only_select_jpg_png'), status: 'warning' });
return;
}
startUpload(async () => {
const compressed = base64ToFile(
await compressBase64Img({
base64Img: await fileToBase64(file),
maxW,
maxH,
maxSize
}),
file.name
);
const { url, fields } = await api({ filename: file.name });
const formData = new FormData();
Object.entries(fields).forEach(([k, v]) => formData.set(k, v));
formData.set('file', compressed);
const res = await fetch(url, { method: 'POST', body: formData }); // 204
if (res.ok && res.status === 204) {
onSuccess?.(`${imageBaseUrl}${fields.key}`);
}
});
},
[t, toast, api, onSuccess]
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 35 out of 47 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 39 out of 51 changed files in this pull request and generated 18 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 40 out of 52 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| useEffect(() => { | ||
| const calculateQRSize = () => { | ||
| const windowHeight = window.innerHeight; | ||
| const reservedSpace = 470 + (tip ? 60 : 0) + (discountCouponName ? 30 : 0); | ||
| const availableHeight = windowHeight - reservedSpace; | ||
|
|
||
| const newSize = Math.min(QR_CODE_SIZE, Math.max(MIN_QR_SIZE, availableHeight)); | ||
|
|
||
| setDynamicQRSize(newSize); | ||
| }; | ||
|
|
||
| window.addEventListener('resize', calculateQRSize); | ||
|
|
||
| return () => { | ||
| window.removeEventListener('resize', calculateQRSize); | ||
| }; | ||
| }, [tip, discountCouponName]); |
There was a problem hiding this comment.
The calculateQRSize function is only called once during the initial mount when the resize event listener is added, but it's never called initially to set the correct size. This means the QR code will always start with the default QR_CODE_SIZE regardless of the current window size. You should call calculateQRSize() immediately after defining it or before adding the event listener.
| const { data } = useRequest2( | ||
| async () => { | ||
| if (!feConfigs?.isPlus || !userInfo) return; | ||
| return getActivityAd(); | ||
| }, | ||
| { | ||
| manual: false, | ||
| onSuccess(res) { | ||
| const shouldShowAd = (() => { | ||
| if (!res?.id) return false; | ||
| if (!closedData) return true; | ||
|
|
||
| try { | ||
| const { timestamp, adId } = JSON.parse(closedData) as { | ||
| timestamp: number; | ||
| adId: string; | ||
| }; | ||
| // 不同的广告 id,一定展示 | ||
| if (adId && res.id !== adId) return true; | ||
| const now = Date.now(); | ||
| // Show if 24 hours passed | ||
| return now - timestamp > CLOSED_AD_DURATION; | ||
| } catch { | ||
| return true; | ||
| } | ||
| })(); | ||
|
|
||
| if (res?.activityAdImage && shouldShowAd) { | ||
| onOpen(); | ||
| } | ||
| }, | ||
| refreshDeps: [userInfo] | ||
| } |
There was a problem hiding this comment.
The refreshDeps for useRequest2 is set to [userInfo], but the request also depends on feConfigs?.isPlus. If feConfigs changes after initial mount, the request won't re-run. Consider adding feConfigs?.isPlus to the refreshDeps array, or make the hook manual: true and trigger it manually when both conditions are met.
| z.enum(StandardSubLevelEnum), | ||
| TeamStandardSubPlanItemSchema | ||
| ); | ||
| export type StandSubPlanLevelMapType = z.infer<typeof StandSubPlanLevelMapSchema>; | ||
|
|
||
| export const PointsPackageItemSchema = z.object({ | ||
| points: z.int(), | ||
| month: z.int(), | ||
| price: z.number(), | ||
| activityBonusPoints: z.int().optional() // 活动赠送积分 | ||
| }); | ||
| export type PointsPackageItem = z.infer<typeof PointsPackageItemSchema>; | ||
|
|
||
| export const SubPlanSchema = z.object({ | ||
| [SubTypeEnum.standard]: StandSubPlanLevelMapSchema.optional(), | ||
| [SubTypeEnum.extraDatasetSize]: z.object({ price: z.number() }).optional(), | ||
| [SubTypeEnum.extraPoints]: z.object({ packages: PointsPackageItemSchema.array() }).optional(), | ||
| planDescriptionUrl: z.string().optional(), | ||
| appRegistrationUrl: z.string().optional(), | ||
| communitySupportTip: z.string().optional(), | ||
| activityExpirationTime: z.date().optional() | ||
| }); | ||
| export type SubPlanType = z.infer<typeof SubPlanSchema>; | ||
|
|
||
| export const TeamSubSchema = z.object({ | ||
| _id: ObjectIdSchema, | ||
| teamId: ObjectIdSchema, | ||
| type: z.enum(SubTypeEnum), | ||
| startTime: z.date(), | ||
| expiredTime: z.date(), | ||
|
|
||
| currentMode: z.enum(SubModeEnum), | ||
| nextMode: z.enum(SubModeEnum), | ||
| currentSubLevel: z.enum(StandardSubLevelEnum), | ||
| nextSubLevel: z.enum(StandardSubLevelEnum), |
There was a problem hiding this comment.
The z.enum() function expects a tuple of string literals, not a TypeScript enum. Since StandardSubLevelEnum, SubModeEnum, and SubTypeEnum are TypeScript enums (as defined in constants.ts), you should use z.nativeEnum() instead of z.enum(). This applies to lines 39, 66, 70-73 where these enums are used with z.enum().
No description provided.