Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 497f654cdf | |||
| a98c7ecb52 | |||
| 0d79e9a6fd | |||
| 93d4091254 |
Binary file not shown.
@@ -92,7 +92,7 @@
|
|||||||
"iconUrlInvalid": "Must be a valid URL",
|
"iconUrlInvalid": "Must be a valid URL",
|
||||||
"isFallback": "Fallback Prize",
|
"isFallback": "Fallback Prize",
|
||||||
"isFallbackHint": "Awarded when limited prizes are sold out",
|
"isFallbackHint": "Awarded when limited prizes are sold out",
|
||||||
"manualPrizeHint": "Manual prize: the winner submits claim info, then an operator approves and marks it paid/shipped under the Claims tab.",
|
"manualPrizeHint": "Manual prize: no extra config. When won, the user contacts customer service with a screenshot to redeem — no in-app claim form.",
|
||||||
"name": "Prize Name",
|
"name": "Prize Name",
|
||||||
"namePlaceholder": "e.g. 1-Day Pass",
|
"namePlaceholder": "e.g. 1-Day Pass",
|
||||||
"nameRequired": "Name is required",
|
"nameRequired": "Name is required",
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
"type": "Prize Type",
|
"type": "Prize Type",
|
||||||
"typeCommission": "Commission",
|
"typeCommission": "Commission",
|
||||||
"typeCoupon": "Coupon",
|
"typeCoupon": "Coupon",
|
||||||
"typeCrypto": "Crypto",
|
"typeCrypto": "Manual Grant",
|
||||||
"typeManualOther": "Manual (Other)",
|
"typeManualOther": "Manual (Other)",
|
||||||
"typeNone": "No Prize (Thanks)",
|
"typeNone": "No Prize (Thanks)",
|
||||||
"typePhysical": "Physical",
|
"typePhysical": "Physical",
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
"iconUrlInvalid": "请输入有效的 URL",
|
"iconUrlInvalid": "请输入有效的 URL",
|
||||||
"isFallback": "保底奖品",
|
"isFallback": "保底奖品",
|
||||||
"isFallbackHint": "限量奖品抽完后发放该奖品",
|
"isFallbackHint": "限量奖品抽完后发放该奖品",
|
||||||
"manualPrizeHint": "人工奖:用户先提交领奖信息,运营在「领奖工单」页审核并标记发放/发货。",
|
"manualPrizeHint": "人工发放奖品:无需额外配置。中奖后用户凭截图联系人工客服兑换,无需在 app 内填领奖表单。",
|
||||||
"name": "奖品名称",
|
"name": "奖品名称",
|
||||||
"namePlaceholder": "例如:1 天时长卡",
|
"namePlaceholder": "例如:1 天时长卡",
|
||||||
"nameRequired": "请输入奖品名称",
|
"nameRequired": "请输入奖品名称",
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
"type": "奖品类型",
|
"type": "奖品类型",
|
||||||
"typeCommission": "佣金",
|
"typeCommission": "佣金",
|
||||||
"typeCoupon": "优惠券",
|
"typeCoupon": "优惠券",
|
||||||
"typeCrypto": "加密货币",
|
"typeCrypto": "人工发放",
|
||||||
"typeManualOther": "人工发放(其他)",
|
"typeManualOther": "人工发放(其他)",
|
||||||
"typeNone": "谢谢参与",
|
"typeNone": "谢谢参与",
|
||||||
"typePhysical": "实物奖品",
|
"typePhysical": "实物奖品",
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ const grantSchema = z.object({
|
|||||||
source_ref: z.string().trim().min(1),
|
source_ref: z.string().trim().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 数字输入统一用 type="text" + 该 formatOutput,避开 number 类型 input 的实时钳制问题。
|
||||||
|
const numberOutput = (v: string | number): number | undefined => {
|
||||||
|
const s = String(v ?? "").trim();
|
||||||
|
if (s === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const n = Number(s);
|
||||||
|
return Number.isNaN(n) ? undefined : n;
|
||||||
|
};
|
||||||
|
|
||||||
type GrantFormValues = z.infer<typeof grantSchema>;
|
type GrantFormValues = z.infer<typeof grantSchema>;
|
||||||
|
|
||||||
const SOURCE_REF_RANDOM_LENGTH = 6;
|
const SOURCE_REF_RANDOM_LENGTH = 6;
|
||||||
@@ -123,10 +133,11 @@ export default function GrantDialog({
|
|||||||
<FormLabel>{t("grant.userId", "User ID")}</FormLabel>
|
<FormLabel>{t("grant.userId", "User ID")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<EnhancedInput
|
<EnhancedInput
|
||||||
min={1}
|
formatOutput={numberOutput}
|
||||||
|
inputMode="numeric"
|
||||||
onValueChange={(value) => field.onChange(value)}
|
onValueChange={(value) => field.onChange(value)}
|
||||||
placeholder={t("grant.userIdPlaceholder", "e.g. 87433")}
|
placeholder={t("grant.userIdPlaceholder", "e.g. 87433")}
|
||||||
type="number"
|
type="text"
|
||||||
value={field.value}
|
value={field.value}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -148,10 +159,11 @@ export default function GrantDialog({
|
|||||||
<FormLabel>{t("grant.amount", "Amount")}</FormLabel>
|
<FormLabel>{t("grant.amount", "Amount")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<EnhancedInput
|
<EnhancedInput
|
||||||
min={1}
|
formatOutput={numberOutput}
|
||||||
|
inputMode="numeric"
|
||||||
onValueChange={(value) => field.onChange(value)}
|
onValueChange={(value) => field.onChange(value)}
|
||||||
placeholder="1"
|
placeholder="1"
|
||||||
type="number"
|
type="text"
|
||||||
value={field.value}
|
value={field.value}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -42,10 +42,21 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const CENTS_PER_YUAN = 100;
|
|
||||||
// 概率单位约定:weight 1000 = 1%(支持 3 位小数概率,总权重 100000 = 100%)。
|
// 概率单位约定:weight 1000 = 1%(支持 3 位小数概率,总权重 100000 = 100%)。
|
||||||
const WEIGHT_PER_PERCENT = 1000;
|
const WEIGHT_PER_PERCENT = 1000;
|
||||||
|
|
||||||
|
// numberOutput 把 text 输入转成数字(空/非法→undefined)。数字输入统一用 type="text"
|
||||||
|
// + 该 formatOutput,避开 EnhancedInput 在 type="number" 下 min/max 实时钳制导致的
|
||||||
|
// “输入 0 / 改中间位跳变”问题;提交仍是数字,后端收 int。
|
||||||
|
const numberOutput = (v: string | number): number | undefined => {
|
||||||
|
const s = String(v ?? "").trim();
|
||||||
|
if (s === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const n = Number(s);
|
||||||
|
return Number.isNaN(n) ? undefined : n;
|
||||||
|
};
|
||||||
|
|
||||||
type TranslateFn = (key: string, defaultValue: string) => string;
|
type TranslateFn = (key: string, defaultValue: string) => string;
|
||||||
|
|
||||||
const PRIZE_TYPE_OPTIONS: {
|
const PRIZE_TYPE_OPTIONS: {
|
||||||
@@ -59,43 +70,21 @@ const PRIZE_TYPE_OPTIONS: {
|
|||||||
labelDefault: "VPN Duration",
|
labelDefault: "VPN Duration",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "commission",
|
value: "crypto",
|
||||||
labelKey: "prize.typeCommission",
|
labelKey: "prize.typeCrypto",
|
||||||
labelDefault: "Commission",
|
labelDefault: "Crypto",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "none",
|
value: "none",
|
||||||
labelKey: "prize.typeNone",
|
labelKey: "prize.typeNone",
|
||||||
labelDefault: "No Prize (Thanks)",
|
labelDefault: "No Prize (Thanks)",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
value: "crypto",
|
|
||||||
labelKey: "prize.typeCrypto",
|
|
||||||
labelDefault: "Crypto",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "physical",
|
|
||||||
labelKey: "prize.typePhysical",
|
|
||||||
labelDefault: "Physical",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "manual_other",
|
|
||||||
labelKey: "prize.typeManualOther",
|
|
||||||
labelDefault: "Manual (Other)",
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const getPrizeSchema = (t: TranslateFn) =>
|
const getPrizeSchema = (t: TranslateFn) =>
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
type: z.enum([
|
type: z.enum(["vpn_duration", "crypto", "none"]),
|
||||||
"vpn_duration",
|
|
||||||
"commission",
|
|
||||||
"none",
|
|
||||||
"crypto",
|
|
||||||
"physical",
|
|
||||||
"manual_other",
|
|
||||||
]),
|
|
||||||
name: z.string().min(1, t("prize.nameRequired", "Name is required")),
|
name: z.string().min(1, t("prize.nameRequired", "Name is required")),
|
||||||
icon_url: z
|
icon_url: z
|
||||||
.string()
|
.string()
|
||||||
@@ -111,13 +100,6 @@ const getPrizeSchema = (t: TranslateFn) =>
|
|||||||
is_fallback: z.boolean(),
|
is_fallback: z.boolean(),
|
||||||
duration_days: z.number().optional(),
|
duration_days: z.number().optional(),
|
||||||
subscribe_id: z.number().optional(),
|
subscribe_id: z.number().optional(),
|
||||||
amount_yuan: z.number().optional(),
|
|
||||||
// 人工奖(crypto / physical / manual_other)通用/专属字段
|
|
||||||
crypto_amount: z.string().optional(),
|
|
||||||
crypto_currency: z.string().optional(),
|
|
||||||
crypto_networks: z.string().optional(), // 逗号分隔,提交时 split 成 string[]
|
|
||||||
sku_name: z.string().optional(),
|
|
||||||
claim_ttl_hours: z.number().optional(),
|
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
const issue = (path: string, message: string) => {
|
const issue = (path: string, message: string) => {
|
||||||
@@ -134,63 +116,6 @@ const getPrizeSchema = (t: TranslateFn) =>
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (data.type === "commission") {
|
|
||||||
const yuan = data.amount_yuan ?? 0;
|
|
||||||
const cents = yuan * CENTS_PER_YUAN;
|
|
||||||
if (yuan <= 0) {
|
|
||||||
issue(
|
|
||||||
"amount_yuan",
|
|
||||||
t("prize.amountRequired", "Amount must be greater than 0")
|
|
||||||
);
|
|
||||||
} else if (Math.abs(Math.round(cents) - cents) > 1e-6) {
|
|
||||||
issue(
|
|
||||||
"amount_yuan",
|
|
||||||
t(
|
|
||||||
"prize.amountPrecision",
|
|
||||||
"Amount supports at most 2 decimal places"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (data.type === "crypto") {
|
|
||||||
if (!data.crypto_amount?.trim()) {
|
|
||||||
issue(
|
|
||||||
"crypto_amount",
|
|
||||||
t("prize.cryptoAmountRequired", "Amount is required")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!data.crypto_currency?.trim()) {
|
|
||||||
issue(
|
|
||||||
"crypto_currency",
|
|
||||||
t("prize.cryptoCurrencyRequired", "Currency is required")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const networks = (data.crypto_networks ?? "")
|
|
||||||
.split(",")
|
|
||||||
.map((n) => n.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
if (networks.length === 0) {
|
|
||||||
issue(
|
|
||||||
"crypto_networks",
|
|
||||||
t(
|
|
||||||
"prize.cryptoNetworksRequired",
|
|
||||||
"At least one network is required (comma-separated)"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
data.claim_ttl_hours !== undefined &&
|
|
||||||
(!Number.isInteger(data.claim_ttl_hours) || data.claim_ttl_hours <= 0)
|
|
||||||
) {
|
|
||||||
issue(
|
|
||||||
"claim_ttl_hours",
|
|
||||||
t(
|
|
||||||
"prize.claimTtlInvalid",
|
|
||||||
"Claim window (hours) must be a positive integer"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!(data.unlimited_stock || isPositiveInt(data.total_stock))) {
|
if (!(data.unlimited_stock || isPositiveInt(data.total_stock))) {
|
||||||
issue(
|
issue(
|
||||||
"total_stock",
|
"total_stock",
|
||||||
@@ -215,9 +140,6 @@ interface PrizeFormProps {
|
|||||||
|
|
||||||
function toFormValues(prize?: API.LotteryPrize): PrizeFormValues {
|
function toFormValues(prize?: API.LotteryPrize): PrizeFormValues {
|
||||||
const config = prize?.config ?? {};
|
const config = prize?.config ?? {};
|
||||||
const networks = Array.isArray(config.networks)
|
|
||||||
? (config.networks as string[]).join(", ")
|
|
||||||
: undefined;
|
|
||||||
return {
|
return {
|
||||||
type: (prize?.type as PrizeFormValues["type"]) ?? "vpn_duration",
|
type: (prize?.type as PrizeFormValues["type"]) ?? "vpn_duration",
|
||||||
name: prize?.name ?? "",
|
name: prize?.name ?? "",
|
||||||
@@ -228,26 +150,10 @@ function toFormValues(prize?: API.LotteryPrize): PrizeFormValues {
|
|||||||
is_fallback: prize?.is_fallback ?? false,
|
is_fallback: prize?.is_fallback ?? false,
|
||||||
duration_days: config.duration_days as number | undefined,
|
duration_days: config.duration_days as number | undefined,
|
||||||
subscribe_id: config.subscribe_id as number | undefined,
|
subscribe_id: config.subscribe_id as number | undefined,
|
||||||
amount_yuan:
|
|
||||||
typeof config.amount_cents === "number"
|
|
||||||
? config.amount_cents / CENTS_PER_YUAN
|
|
||||||
: undefined,
|
|
||||||
crypto_amount: config.amount as string | undefined,
|
|
||||||
crypto_currency: config.currency as string | undefined,
|
|
||||||
crypto_networks: networks,
|
|
||||||
sku_name: config.sku_name as string | undefined,
|
|
||||||
claim_ttl_hours: config.claim_ttl_hours as number | undefined,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function toConfig(values: PrizeFormValues): Record<string, any> {
|
function toConfig(values: PrizeFormValues): Record<string, any> {
|
||||||
// claim_ttl_hours 对所有人工奖通用:控制领奖窗口(缺省后端默认 7 天)。
|
|
||||||
const withTtl = (base: Record<string, any>): Record<string, any> => {
|
|
||||||
if (values.claim_ttl_hours && values.claim_ttl_hours > 0) {
|
|
||||||
base.claim_ttl_hours = values.claim_ttl_hours;
|
|
||||||
}
|
|
||||||
return base;
|
|
||||||
};
|
|
||||||
if (values.type === "vpn_duration") {
|
if (values.type === "vpn_duration") {
|
||||||
const config: Record<string, any> = { duration_days: values.duration_days };
|
const config: Record<string, any> = { duration_days: values.duration_days };
|
||||||
// 可选:无活跃订阅时按该套餐新建订阅发放时长。0/未选则沿用旧行为(跳过发放)。
|
// 可选:无活跃订阅时按该套餐新建订阅发放时长。0/未选则沿用旧行为(跳过发放)。
|
||||||
@@ -256,28 +162,7 @@ function toConfig(values: PrizeFormValues): Record<string, any> {
|
|||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
if (values.type === "commission") {
|
// crypto(人工发放) / none 无需额外配置:中奖后凭截图联系客服线下发放。
|
||||||
return {
|
|
||||||
amount_cents: Math.round((values.amount_yuan ?? 0) * CENTS_PER_YUAN),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (values.type === "crypto") {
|
|
||||||
return withTtl({
|
|
||||||
amount: values.crypto_amount?.trim() ?? "",
|
|
||||||
currency: values.crypto_currency?.trim() ?? "",
|
|
||||||
networks: (values.crypto_networks ?? "")
|
|
||||||
.split(",")
|
|
||||||
.map((n) => n.trim())
|
|
||||||
.filter(Boolean),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (values.type === "physical" || values.type === "manual_other") {
|
|
||||||
const config: Record<string, any> = {};
|
|
||||||
if (values.sku_name?.trim()) {
|
|
||||||
config.sku_name = values.sku_name.trim();
|
|
||||||
}
|
|
||||||
return withTtl(config);
|
|
||||||
}
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,18 +251,7 @@ export default function PrizeForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderInput = (
|
const renderInput = (
|
||||||
name:
|
name: "name" | "icon_url" | "weight" | "total_stock" | "duration_days",
|
||||||
| "name"
|
|
||||||
| "icon_url"
|
|
||||||
| "weight"
|
|
||||||
| "total_stock"
|
|
||||||
| "duration_days"
|
|
||||||
| "amount_yuan"
|
|
||||||
| "crypto_amount"
|
|
||||||
| "crypto_currency"
|
|
||||||
| "crypto_networks"
|
|
||||||
| "sku_name"
|
|
||||||
| "claim_ttl_hours",
|
|
||||||
label: string,
|
label: string,
|
||||||
inputProps: Partial<EnhancedInputProps<any>>,
|
inputProps: Partial<EnhancedInputProps<any>>,
|
||||||
description?: string
|
description?: string
|
||||||
@@ -494,9 +368,9 @@ export default function PrizeForm({
|
|||||||
"duration_days",
|
"duration_days",
|
||||||
t("prize.durationDays", "Duration (days)"),
|
t("prize.durationDays", "Duration (days)"),
|
||||||
{
|
{
|
||||||
type: "number",
|
type: "text",
|
||||||
min: 1,
|
inputMode: "numeric",
|
||||||
step: 1,
|
formatOutput: numberOutput,
|
||||||
placeholder: t("prize.durationDaysPlaceholder", "e.g. 1"),
|
placeholder: t("prize.durationDaysPlaceholder", "e.g. 1"),
|
||||||
suffix: t("prize.daysSuffix", "days"),
|
suffix: t("prize.daysSuffix", "days"),
|
||||||
}
|
}
|
||||||
@@ -548,23 +422,6 @@ export default function PrizeForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{type === "commission" &&
|
|
||||||
renderInput(
|
|
||||||
"amount_yuan",
|
|
||||||
t("prize.amountYuan", "Commission Amount (CNY)"),
|
|
||||||
{
|
|
||||||
type: "number",
|
|
||||||
min: 0,
|
|
||||||
step: 0.01,
|
|
||||||
placeholder: "0.00",
|
|
||||||
prefix: "¥",
|
|
||||||
suffix: t("prize.yuanSuffix", "CNY"),
|
|
||||||
},
|
|
||||||
t(
|
|
||||||
"prize.amountUnitHint",
|
|
||||||
"Entered in CNY (yuan), stored as cents; up to 2 decimal places"
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
{type === "none" && (
|
{type === "none" && (
|
||||||
<p className="rounded-md bg-muted/50 p-3 text-muted-foreground text-xs">
|
<p className="rounded-md bg-muted/50 p-3 text-muted-foreground text-xs">
|
||||||
{t(
|
{t(
|
||||||
@@ -574,79 +431,20 @@ export default function PrizeForm({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{type === "crypto" && (
|
{type === "crypto" && (
|
||||||
<>
|
<p className="rounded-md bg-muted/50 p-3 text-muted-foreground text-xs">
|
||||||
{renderInput(
|
{t(
|
||||||
"crypto_amount",
|
"prize.manualPrizeHint",
|
||||||
t("prize.cryptoAmount", "Amount"),
|
"Manual prize: no extra config. When won, the user contacts customer service with a screenshot to redeem — no in-app claim form."
|
||||||
{
|
|
||||||
placeholder: t(
|
|
||||||
"prize.cryptoAmountPlaceholder",
|
|
||||||
"e.g. 0.01"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)}
|
)}
|
||||||
{renderInput(
|
</p>
|
||||||
"crypto_currency",
|
|
||||||
t("prize.cryptoCurrency", "Currency"),
|
|
||||||
{ placeholder: "USDT / BTC / ETH" }
|
|
||||||
)}
|
|
||||||
{renderInput(
|
|
||||||
"crypto_networks",
|
|
||||||
t("prize.cryptoNetworks", "Networks"),
|
|
||||||
{ placeholder: "TRC20, ERC20, BTC" },
|
|
||||||
t(
|
|
||||||
"prize.cryptoNetworksHint",
|
|
||||||
"Comma-separated. The user picks one of these when claiming."
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{(type === "physical" || type === "manual_other") &&
|
|
||||||
renderInput(
|
|
||||||
"sku_name",
|
|
||||||
t("prize.skuName", "Item name (optional)"),
|
|
||||||
{
|
|
||||||
placeholder: t(
|
|
||||||
"prize.skuNamePlaceholder",
|
|
||||||
"e.g. Limited T-shirt"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
{(type === "crypto" ||
|
|
||||||
type === "physical" ||
|
|
||||||
type === "manual_other") && (
|
|
||||||
<>
|
|
||||||
{renderInput(
|
|
||||||
"claim_ttl_hours",
|
|
||||||
t("prize.claimTtlHours", "Claim window (hours)"),
|
|
||||||
{
|
|
||||||
type: "number",
|
|
||||||
min: 1,
|
|
||||||
step: 1,
|
|
||||||
placeholder: "168",
|
|
||||||
suffix: t("prize.hoursSuffix", "hours"),
|
|
||||||
},
|
|
||||||
t(
|
|
||||||
"prize.claimTtlHint",
|
|
||||||
"How long the user has to submit claim info. Default 168h (7 days)."
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<p className="rounded-md bg-muted/50 p-3 text-muted-foreground text-xs">
|
|
||||||
{t(
|
|
||||||
"prize.manualPrizeHint",
|
|
||||||
"Manual prize: the winner submits claim info, then an operator approves and marks it paid/shipped under the Claims tab."
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{renderInput(
|
{renderInput(
|
||||||
"weight",
|
"weight",
|
||||||
t("prize.weight", "Win probability (%)"),
|
t("prize.weight", "Win probability (%)"),
|
||||||
{
|
{
|
||||||
type: "number",
|
type: "text",
|
||||||
min: 0,
|
inputMode: "decimal",
|
||||||
max: 100,
|
formatOutput: numberOutput,
|
||||||
step: 0.001,
|
|
||||||
suffix: "%",
|
suffix: "%",
|
||||||
disabled: isFallback,
|
disabled: isFallback,
|
||||||
},
|
},
|
||||||
@@ -669,9 +467,9 @@ export default function PrizeForm({
|
|||||||
"total_stock",
|
"total_stock",
|
||||||
t("prize.totalStock", "Total Stock"),
|
t("prize.totalStock", "Total Stock"),
|
||||||
{
|
{
|
||||||
type: "number",
|
type: "text",
|
||||||
min: 1,
|
inputMode: "numeric",
|
||||||
step: 1,
|
formatOutput: numberOutput,
|
||||||
placeholder: t("prize.totalStockPlaceholder", "e.g. 100"),
|
placeholder: t("prize.totalStockPlaceholder", "e.g. 100"),
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user