✨ feat: add redemption code feature to user dashboard
- Add redemption code input component in dashboard - Integrate redemption API endpoint - Add i18n support for Chinese and English - Display redemption section alongside subscriptions
This commit is contained in:
parent
902e5fe59a
commit
1f9b7ea1db
@ -31,5 +31,13 @@
|
|||||||
"subscriptionUrl": "Subscription URL",
|
"subscriptionUrl": "Subscription URL",
|
||||||
"totalTraffic": "Total Traffic",
|
"totalTraffic": "Total Traffic",
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"used": "Used"
|
"used": "Used",
|
||||||
|
"redeemCode": "Redeem Code",
|
||||||
|
"redeemCodeTitle": "Redeem Code",
|
||||||
|
"redeemCodeDescription": "Enter redemption code to get subscription plan",
|
||||||
|
"enterRedemptionCode": "Please enter redemption code",
|
||||||
|
"redeemButton": "Redeem Now",
|
||||||
|
"redeeming": "Redeeming...",
|
||||||
|
"redeemSuccess": "Redemption successful! Subscription has been activated.",
|
||||||
|
"redeemFailed": "Redemption failed"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,5 +31,13 @@
|
|||||||
"subscriptionUrl": "订阅链接",
|
"subscriptionUrl": "订阅链接",
|
||||||
"totalTraffic": "总流量",
|
"totalTraffic": "总流量",
|
||||||
"unknown": "未知",
|
"unknown": "未知",
|
||||||
"used": "已使用"
|
"used": "已使用",
|
||||||
|
"redeemCode": "兑换 CDK",
|
||||||
|
"redeemCodeTitle": "CDK 兑换",
|
||||||
|
"redeemCodeDescription": "输入兑换码以获取订阅套餐",
|
||||||
|
"enterRedemptionCode": "请输入兑换码",
|
||||||
|
"redeemButton": "立即兑换",
|
||||||
|
"redeeming": "兑换中...",
|
||||||
|
"redeemSuccess": "兑换成功!",
|
||||||
|
"redeemFailed": "兑换失败"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,6 +47,7 @@ import Subscribe from "../../subscribe";
|
|||||||
import Renewal from "../../subscribe/renewal";
|
import Renewal from "../../subscribe/renewal";
|
||||||
import ResetTraffic from "../../subscribe/reset-traffic";
|
import ResetTraffic from "../../subscribe/reset-traffic";
|
||||||
import Unsubscribe from "../../subscribe/unsubscribe";
|
import Unsubscribe from "../../subscribe/unsubscribe";
|
||||||
|
import RedeemCode from "./redeem-code";
|
||||||
|
|
||||||
const platforms: (keyof API.DownloadLink)[] = [
|
const platforms: (keyof API.DownloadLink)[] = [
|
||||||
"windows",
|
"windows",
|
||||||
@ -164,6 +165,7 @@ export default function Content() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<RedeemCode onSuccess={refetch} />
|
||||||
<div className="flex flex-wrap justify-between gap-4">
|
<div className="flex flex-wrap justify-between gap-4">
|
||||||
{availablePlatforms.length > 0 && (
|
{availablePlatforms.length > 0 && (
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|||||||
85
apps/user/src/sections/user/dashboard/redeem-code.tsx
Normal file
85
apps/user/src/sections/user/dashboard/redeem-code.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { Button } from "@workspace/ui/components/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@workspace/ui/components/card";
|
||||||
|
import { Input } from "@workspace/ui/components/input";
|
||||||
|
import { Label } from "@workspace/ui/components/label";
|
||||||
|
import { redeemCode } from "@workspace/ui/services/user/user";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface RedeemCodeProps {
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RedeemCode({ onSuccess }: RedeemCodeProps) {
|
||||||
|
const { t } = useTranslation("dashboard");
|
||||||
|
const [code, setCode] = useState("");
|
||||||
|
|
||||||
|
const redeemMutation = useMutation({
|
||||||
|
mutationFn: (code: string) => redeemCode({ code }),
|
||||||
|
onSuccess: (response) => {
|
||||||
|
toast.success(response.data.message || t("redeemSuccess", "兑换成功"));
|
||||||
|
setCode("");
|
||||||
|
onSuccess?.();
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
const errorMessage =
|
||||||
|
error?.response?.data?.message || t("redeemFailed", "兑换失败");
|
||||||
|
toast.error(errorMessage);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!code.trim()) {
|
||||||
|
toast.error(t("pleaseEnterCode", "请输入兑换码"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redeemMutation.mutate(code.trim());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{t("redeemCode", "CDK 兑换")}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{t("redeemCodeDescription", "输入兑换码即可兑换对应的订阅套餐")}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="redemption-code">
|
||||||
|
{t("redemptionCode", "兑换码")}
|
||||||
|
</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="redemption-code"
|
||||||
|
onChange={(e) => setCode(e.target.value)}
|
||||||
|
placeholder={t(
|
||||||
|
"enterRedemptionCode",
|
||||||
|
"请输入兑换码"
|
||||||
|
)}
|
||||||
|
value={code}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
disabled={redeemMutation.isPending || !code.trim()}
|
||||||
|
loading={redeemMutation.isPending}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{t("redeem", "兑换")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -268,6 +268,24 @@ export async function updateUserRules(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Redeem Code POST /v1/public/redemption/ */
|
||||||
|
export async function redeemCode(
|
||||||
|
body: { code: string },
|
||||||
|
options?: { [key: string]: any }
|
||||||
|
) {
|
||||||
|
return request<API.Response & { data?: { message: string } }>(
|
||||||
|
`${import.meta.env.VITE_API_PREFIX || ""}/v1/public/redemption/`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
data: body,
|
||||||
|
...(options || {}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Query User Subscribe GET /v1/public/user/subscribe */
|
/** Query User Subscribe GET /v1/public/user/subscribe */
|
||||||
export async function queryUserSubscribe(options?: { [key: string]: any }) {
|
export async function queryUserSubscribe(options?: { [key: string]: any }) {
|
||||||
return request<API.Response & { data?: API.QueryUserSubscribeListResponse }>(
|
return request<API.Response & { data?: API.QueryUserSubscribeListResponse }>(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user