Skip to content

Commit 6d92ff7

Browse files
committed
feat: support copy/delete actions on gift code management
1 parent d4015c3 commit 6d92ff7

File tree

12 files changed

+158
-52
lines changed

12 files changed

+158
-52
lines changed

admin/controller.go

+22
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ type GenerateInvitationForm struct {
1414
Number int `json:"number"`
1515
}
1616

17+
type DeleteInvitationForm struct {
18+
Code string `json:"code"`
19+
}
20+
1721
type GenerateRedeemForm struct {
1822
Quota float32 `json:"quota"`
1923
Number int `json:"number"`
@@ -139,6 +143,24 @@ func InvitationPaginationAPI(c *gin.Context) {
139143
c.JSON(http.StatusOK, GetInvitationPagination(db, int64(page)))
140144
}
141145

146+
func DeleteInvitationAPI(c *gin.Context) {
147+
db := utils.GetDBFromContext(c)
148+
149+
var form DeleteInvitationForm
150+
if err := c.ShouldBindJSON(&form); err != nil {
151+
c.JSON(http.StatusOK, gin.H{
152+
"status": false,
153+
"error": err.Error(),
154+
})
155+
return
156+
}
157+
158+
err := DeleteInvitationCode(db, form.Code)
159+
c.JSON(http.StatusOK, gin.H{
160+
"status": err == nil,
161+
"error": err,
162+
})
163+
}
142164
func GenerateInvitationAPI(c *gin.Context) {
143165
db := utils.GetDBFromContext(c)
144166

admin/invitation.go

+7
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ func GetInvitationPagination(db *sql.DB, page int64) PaginationForm {
5353
}
5454
}
5555

56+
func DeleteInvitationCode(db *sql.DB, code string) error {
57+
_, err := globals.ExecDb(db, `
58+
DELETE FROM invitation WHERE code = ?
59+
`, code)
60+
return err
61+
}
62+
5663
func NewInvitationCode(db *sql.DB, code string, quota float32, t string) error {
5764
_, err := globals.ExecDb(db, `
5865
INSERT INTO invitation (code, quota, type)

admin/router.go

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ func Register(app *gin.RouterGroup) {
1717

1818
app.GET("/admin/invitation/list", InvitationPaginationAPI)
1919
app.POST("/admin/invitation/generate", GenerateInvitationAPI)
20+
app.POST("/admin/invitation/delete", DeleteInvitationAPI)
2021

2122
app.GET("/admin/redeem/list", RedeemListAPI)
2223
app.POST("/admin/redeem/generate", GenerateRedeemAPI)

app/src/admin/api/chart.ts

+9
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,15 @@ export async function getInvitationList(
102102
}
103103
}
104104

105+
export async function deleteInvitation(code: string): Promise<CommonResponse> {
106+
try {
107+
const response = await axios.post("/admin/invitation/delete", { code });
108+
return response.data as CommonResponse;
109+
} catch (e) {
110+
return { status: false, message: getErrorMessage(e) };
111+
}
112+
}
113+
105114
export async function generateInvitation(
106115
type: string,
107116
quota: number,

app/src/components/OperationAction.tsx

+15-3
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ import {
1111
PopoverTrigger,
1212
} from "@/components/ui/popover.tsx";
1313
import { Button } from "@/components/ui/button.tsx";
14+
import { cn } from "@/components/ui/lib/utils.ts";
1415

1516
type ActionProps = {
1617
tooltip?: string;
1718
children: React.ReactNode;
1819
onClick?: () => any;
20+
native?: boolean;
1921
variant?:
2022
| "secondary"
2123
| "default"
@@ -26,15 +28,25 @@ type ActionProps = {
2628
| null
2729
| undefined;
2830
};
29-
function OperationAction({ tooltip, children, onClick, variant }: ActionProps) {
31+
function OperationAction({
32+
tooltip,
33+
children,
34+
onClick,
35+
variant,
36+
native,
37+
}: ActionProps) {
3038
return (
3139
<TooltipProvider>
3240
<Tooltip>
3341
<TooltipTrigger asChild>
3442
{variant === "destructive" ? (
3543
<Popover>
3644
<PopoverTrigger asChild>
37-
<Button size={`icon`} className={`w-8 h-8`} variant={variant}>
45+
<Button
46+
size={`icon`}
47+
className={cn(!native && `w-8 h-8`)}
48+
variant={variant}
49+
>
3850
{children}
3951
</Button>
4052
</PopoverTrigger>
@@ -52,7 +64,7 @@ function OperationAction({ tooltip, children, onClick, variant }: ActionProps) {
5264
) : (
5365
<Button
5466
size={`icon`}
55-
className={`w-8 h-8`}
67+
className={cn(!native && `w-8 h-8`)}
5668
onClick={onClick}
5769
variant={variant}
5870
>

app/src/components/admin/InvitationTable.tsx

+43-9
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,24 @@ import {
1818
import { useTranslation } from "react-i18next";
1919
import { useState } from "react";
2020
import { InvitationForm, InvitationResponse } from "@/admin/types.ts";
21-
import { Button } from "@/components/ui/button.tsx";
22-
import { Download, Loader2, RotateCw } from "lucide-react";
21+
import { Button, TemporaryButton } from "@/components/ui/button.tsx";
22+
import { Copy, Download, Loader2, RotateCw, Trash } from "lucide-react";
2323
import { useEffectAsync } from "@/utils/hook.ts";
24-
import { generateInvitation, getInvitationList } from "@/admin/api/chart.ts";
24+
import {
25+
deleteInvitation,
26+
generateInvitation,
27+
getInvitationList,
28+
} from "@/admin/api/chart.ts";
2529
import { Input } from "@/components/ui/input.tsx";
2630
import { useToast } from "@/components/ui/use-toast.ts";
2731
import { Textarea } from "@/components/ui/textarea.tsx";
28-
import { saveAsFile } from "@/utils/dom.ts";
32+
import { copyClipboard, saveAsFile } from "@/utils/dom.ts";
2933
import { PaginationAction } from "@/components/ui/pagination.tsx";
34+
import { Badge } from "@/components/ui/badge.tsx";
35+
import OperationAction from "@/components/OperationAction.tsx";
36+
import { toastState } from "@/api/common.ts";
3037

31-
function GenerateDialog() {
38+
function GenerateDialog({ update }: { update: () => void }) {
3239
const { t } = useTranslation();
3340
const { toast } = useToast();
3441
const [open, setOpen] = useState<boolean>(false);
@@ -43,8 +50,10 @@ function GenerateDialog() {
4350

4451
async function generateCode() {
4552
const data = await generateInvitation(type, Number(quota), Number(number));
46-
if (data.status) setData(data.data.join("\n"));
47-
else
53+
if (data.status) {
54+
setData(data.data.join("\n"));
55+
update();
56+
} else
4857
toast({
4958
title: t("admin.error"),
5059
description: data.message,
@@ -167,16 +176,41 @@ function InvitationTable() {
167176
<TableHead>{t("admin.type")}</TableHead>
168177
<TableHead>{t("admin.used")}</TableHead>
169178
<TableHead>{t("admin.updated-at")}</TableHead>
179+
<TableHead>{t("admin.action")}</TableHead>
170180
</TableRow>
171181
</TableHeader>
172182
<TableBody>
173183
{(data.data || []).map((invitation, idx) => (
174184
<TableRow key={idx} className={`whitespace-nowrap`}>
175185
<TableCell>{invitation.code}</TableCell>
176186
<TableCell>{invitation.quota}</TableCell>
177-
<TableCell>{invitation.type}</TableCell>
187+
<TableCell>
188+
<Badge>{invitation.type}</Badge>
189+
</TableCell>
178190
<TableCell>{t(`admin.used-${invitation.used}`)}</TableCell>
179191
<TableCell>{invitation.updated_at}</TableCell>
192+
<TableCell className={`flex gap-2`}>
193+
<TemporaryButton
194+
size={`icon`}
195+
variant={`outline`}
196+
onClick={() => copyClipboard(invitation.code)}
197+
>
198+
<Copy className={`h-4 w-4`} />
199+
</TemporaryButton>
200+
<OperationAction
201+
native
202+
tooltip={t("delete")}
203+
variant={`destructive`}
204+
onClick={async () => {
205+
const resp = await deleteInvitation(invitation.code);
206+
toastState(toast, t, resp, true);
207+
208+
resp.status && (await update());
209+
}}
210+
>
211+
<Trash className={`h-4 w-4`} />
212+
</OperationAction>
213+
</TableCell>
180214
</TableRow>
181215
))}
182216
</TableBody>
@@ -202,7 +236,7 @@ function InvitationTable() {
202236
<Button variant={`outline`} size={`icon`} onClick={update}>
203237
<RotateCw className={`h-4 w-4`} />
204238
</Button>
205-
<GenerateDialog />
239+
<GenerateDialog update={update} />
206240
</div>
207241
</div>
208242
);

app/src/components/ui/button.tsx

+25-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { cva, type VariantProps } from "class-variance-authority";
44

55
import { cn } from "./lib/utils";
66
import { useEffect, useMemo, useState } from "react";
7-
import { Loader2 } from "lucide-react";
7+
import { Check, Loader2 } from "lucide-react";
8+
import { useTemporaryState } from "@/utils/hook.ts";
89

910
const buttonVariants = cva(
1011
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
@@ -119,4 +120,26 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
119120
);
120121
Button.displayName = "Button";
121122

122-
export { Button, buttonVariants };
123+
type TemporaryButtonProps = ButtonProps & {
124+
interval?: number;
125+
};
126+
127+
const TemporaryButton = React.forwardRef<
128+
HTMLButtonElement,
129+
TemporaryButtonProps
130+
>(({ interval, children, onClick, ...props }, ref) => {
131+
const { state, triggerState } = useTemporaryState(interval);
132+
133+
const event = (e: React.MouseEvent<HTMLButtonElement>) => {
134+
if (onClick) onClick(e);
135+
triggerState();
136+
};
137+
138+
return (
139+
<Button ref={ref} onClick={event} {...props}>
140+
{state ? <Check className={`h-4 w-4`} /> : children}
141+
</Button>
142+
);
143+
});
144+
145+
export { Button, TemporaryButton, buttonVariants };

app/src/resources/i18n/cn.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,7 @@
482482
"operate-success-prompt": "您的操作已成功执行。",
483483
"operate-failed": "操作失败",
484484
"operate-failed-prompt": "操作失败,原因:{{reason}}",
485-
"updated-at": "领取时间",
485+
"updated-at": "更新时间",
486486
"used-true": "已使用",
487487
"used-false": "未使用",
488488
"generate": "批量生成",

app/src/resources/i18n/en.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@
391391
"operate-success-prompt": "Your operation has been successfully executed.",
392392
"operate-failed": "Operate Failed",
393393
"operate-failed-prompt": "Operation failed, reason: {{reason}}",
394-
"updated-at": "Updated At",
394+
"updated-at": "Updated on ",
395395
"used-true": "Used",
396396
"used-false": "Unused",
397397
"generate": "Generate",

app/src/resources/i18n/ja.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@
391391
"operate-success-prompt": "アクションは正常に実行されました。",
392392
"operate-failed": "操作に失敗しました",
393393
"operate-failed-prompt": "{{reason}}の操作が失敗しました",
394-
"updated-at": "乗車時間",
394+
"updated-at": "アップデート時間",
395395
"used-true": "使用済み",
396396
"used-false": "活用していない",
397397
"generate": "バッチ生成",

app/src/resources/i18n/ru.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@
391391
"operate-success-prompt": "Ваша операция была успешно выполнена.",
392392
"operate-failed": "Не удалось",
393393
"operate-failed-prompt": "Не удалось выполнить операцию, причина: {{reason}}",
394-
"updated-at": "Обновлено",
394+
"updated-at": "Время обновления",
395395
"used-true": "Использовано",
396396
"used-false": "Не использовано",
397397
"generate": "Генерировать",

nginx.conf

+32-34
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,38 @@
1-
worker_processes 1;
1+
server
2+
{
3+
# this is a sample configuration for nginx
4+
listen 80;
25

3-
events {
4-
worker_connections 8192;
5-
multi_accept on;
6-
use epoll;
7-
}
8-
9-
http {
10-
server {
11-
listen 8000 default_server;
12-
listen [::]:8000 default_server;
13-
server_name _;
6+
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|README.md|package.json|package-lock.json|\.env) {
7+
return 404;
8+
}
149

15-
root /app/dist;
16-
index index.html;
10+
if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
11+
return 403;
12+
}
1713

18-
location /api/ {
19-
proxy_pass http://127.0.0.1:8094/;
20-
proxy_set_header Host 127.0.0.1:$server_port;
21-
proxy_set_header X-Real-IP $remote_addr;
22-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
23-
proxy_set_header REMOTE-HOST $remote_addr;
24-
proxy_set_header X-Host $host:$server_port;
25-
proxy_set_header X-Scheme $scheme;
26-
proxy_connect_timeout 30s;
27-
proxy_read_timeout 86400s;
28-
proxy_send_timeout 30s;
29-
proxy_http_version 1.1;
30-
proxy_set_header Upgrade $http_upgrade;
31-
proxy_set_header Connection "upgrade";
32-
}
14+
location ~ /purge(/.*) {
15+
proxy_cache_purge cache_one 127.0.0.1$request_uri$is_args$args;
16+
}
3317

34-
location / {
35-
root /usr/share/nginx/html;
36-
try_files $uri $uri/ /index.html;
37-
error_page 404 =200 /index.html;
38-
}
18+
location / {
19+
# if you are using compile deployment mode, please use the http://localhost:8094 instead
20+
proxy_pass http://127.0.0.1:8000;
21+
proxy_set_header Host 127.0.0.1:$server_port;
22+
proxy_set_header X-Real-IP $remote_addr;
23+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
24+
proxy_set_header REMOTE-HOST $remote_addr;
25+
add_header X-Cache $upstream_cache_status;
26+
proxy_set_header X-Host $host:$server_port;
27+
proxy_set_header X-Scheme $scheme;
28+
proxy_connect_timeout 30s;
29+
proxy_read_timeout 86400s;
30+
proxy_send_timeout 30s;
31+
proxy_http_version 1.1;
32+
proxy_set_header Upgrade $http_upgrade;
33+
proxy_set_header Connection "upgrade";
3934
}
35+
36+
access_log /www/wwwlogs/chatnio.log;
37+
error_log /www/wwwlogs/chatnio.error.log;
4038
}

0 commit comments

Comments
 (0)