Skip to content

Commit 6172633

Browse files
committed
Add history of used addresses
1 parent 71d6aa5 commit 6172633

File tree

9 files changed

+546
-352
lines changed

9 files changed

+546
-352
lines changed

components/actions.tsx

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22
import React, { useEffect, useState } from "react";
33
import { Button } from "@/components/ui/button";
4-
import { CheckCircle, Edit, Shuffle } from "lucide-react";
4+
import { CheckCircle, History, PenLine, Shuffle } from "lucide-react";
55
import { Input } from "@/components/ui/input";
66
import SelectWrap from "@/components/select-wrap";
77
import { DOMAIN_LIST } from "@/lib/constant";
@@ -11,6 +11,7 @@ import { Skeleton } from "@/components/ui/skeleton";
1111
import { randomMail } from "@/lib/utils";
1212
import { emitter, mittKey } from "@/lib/mitt";
1313
import { toast } from "sonner";
14+
import MailHistory from "@/components/mail-history";
1415

1516
function Actions() {
1617
const config = useConfig();
@@ -36,12 +37,17 @@ function Actions() {
3637

3738
function onRandom() {
3839
const random = randomMail();
39-
setMail(random);
4040
config.update(random, domain);
4141
setTimeout(() => emitter.emit(mittKey.REFRESH));
4242
toast.success("已随机至新地址 " + random + domain);
4343
}
4444

45+
function onChange(mail: string, domain: string) {
46+
config.update(mail, domain);
47+
setTimeout(() => emitter.emit(mittKey.REFRESH));
48+
toast.success("已切换至新地址 " + mail + domain);
49+
}
50+
4551
function onMailChange(e: React.ChangeEvent<HTMLInputElement>) {
4652
const value = e.currentTarget.value.replace(/[^a-zA-Z0-9-_.]/g, "");
4753
if (value.length > 64) {
@@ -60,40 +66,50 @@ function Actions() {
6066

6167
const [edited, setEdited] = useState(false);
6268
return (
63-
<div className="flex flex-wrap items-center gap-1">
64-
<Mounted fallback={<Skeleton className="h-8 w-28" />}>
65-
<Input
66-
onKeyDown={onKeyDown}
67-
disabled={!edited}
68-
value={mail}
69-
className="w-28 text-end"
70-
onChange={onMailChange}
71-
/>
72-
</Mounted>
73-
<Mounted fallback={<Skeleton className="h-8 w-28" />}>
74-
<SelectWrap
75-
value={domain}
76-
setValue={(domain) => setDomain(domain)}
77-
list={DOMAIN_LIST}
78-
disabled={!edited}
79-
/>
80-
</Mounted>
69+
<div className="flex flex-wrap items-center gap-2">
70+
<div className="flex h-9 items-center gap-1">
71+
<Mounted fallback={<Skeleton className="h-8 w-28" />}>
72+
<Input
73+
onKeyDown={onKeyDown}
74+
disabled={!edited}
75+
value={mail}
76+
className="w-28 text-end"
77+
onChange={onMailChange}
78+
/>
79+
</Mounted>
80+
<Mounted fallback={<Skeleton className="h-8 w-[118px]" />}>
81+
<SelectWrap
82+
value={domain}
83+
setValue={(domain) => setDomain(domain)}
84+
list={DOMAIN_LIST}
85+
disabled={!edited}
86+
className="w-[118px] px-2"
87+
/>
88+
</Mounted>
89+
</div>
8190

82-
<div className="ml-1">
83-
{!edited && (
84-
<Button variant="ghost" size="icon" onClick={() => setEdited(true)}>
85-
<Edit size={20} strokeWidth={1.8} />
86-
</Button>
87-
)}
88-
{edited && (
89-
<Button variant="ghost" size="icon" onClick={onSave}>
90-
<CheckCircle size={20} strokeWidth={1.8} />
91-
</Button>
92-
)}
93-
<Button variant="ghost" size="icon" onClick={onRandom}>
94-
<Shuffle size={20} strokeWidth={1.8} />
91+
{!edited && (
92+
<Button variant="outline" size="sm" onClick={() => setEdited(true)}>
93+
<PenLine size={14} className="mr-1" />
94+
编辑
9595
</Button>
96-
</div>
96+
)}
97+
{edited && (
98+
<Button variant="outline" size="sm" onClick={onSave}>
99+
<CheckCircle size={14} className="mr-1" />
100+
保存
101+
</Button>
102+
)}
103+
<Button variant="outline" size="sm" onClick={onRandom}>
104+
<Shuffle size={14} className="mr-1" />
105+
随机
106+
</Button>
107+
<MailHistory onChange={onChange}>
108+
<Button variant="outline" size="sm">
109+
<History size={14} className="mr-1" />
110+
历史
111+
</Button>
112+
</MailHistory>
97113
</div>
98114
);
99115
}

components/mail-history.tsx

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import React, { useState } from "react";
2+
import {
3+
Popover,
4+
PopoverContent,
5+
PopoverTrigger,
6+
} from "@/components/ui/popover";
7+
import { useConfig } from "@/lib/store/config";
8+
import { Button } from "@/components/ui/button";
9+
import { Eraser, MailQuestion, Trash } from "lucide-react";
10+
import { toast } from "sonner";
11+
12+
function MailHistory({
13+
onChange,
14+
children,
15+
}: {
16+
onChange: (mail: string, domain: string) => void;
17+
children: React.ReactNode;
18+
}) {
19+
const [history, clearHistory] = useConfig((state) => [
20+
state.history,
21+
state.clearHistory,
22+
]);
23+
24+
function onChangeClick(value: string, index: number) {
25+
setOpen(false);
26+
setTimeout(() => {
27+
clearHistory(index);
28+
const atIndex = value.indexOf("@");
29+
onChange(value.substring(0, atIndex), value.substring(atIndex));
30+
}, 100);
31+
}
32+
33+
const [open, setOpen] = useState(false);
34+
35+
return (
36+
<Popover open={open} onOpenChange={setOpen}>
37+
<PopoverTrigger asChild>{children}</PopoverTrigger>
38+
<PopoverContent className="w-fit overflow-hidden p-0">
39+
{history.length === 0 ? (
40+
<div className="flex items-center gap-1 p-3 text-sm text-muted-foreground">
41+
<MailQuestion size={18} strokeWidth={1.8} />
42+
这里什么也没有
43+
</div>
44+
) : (
45+
<>
46+
<div className="flex max-h-96 flex-col divide-y divide-dashed overflow-auto border-b">
47+
{history.map((value, index) => (
48+
<div
49+
key={index}
50+
className="flex items-center gap-1 px-4 py-2 text-muted-foreground hover:bg-secondary hover:text-foreground"
51+
>
52+
<div
53+
className="cursor-pointer"
54+
onClick={() => onChangeClick(value, index)}
55+
>
56+
{value}
57+
</div>
58+
<div className="flex-1" />
59+
<Trash
60+
size={16}
61+
onClick={() => clearHistory(index)}
62+
className="cursor-pointer hover:text-destructive"
63+
/>
64+
</div>
65+
))}
66+
</div>
67+
<div className="flex bg-secondary p-2">
68+
<div className="flex-1" />
69+
<Button
70+
variant="destructive"
71+
size="sm"
72+
onClick={() => {
73+
setOpen(false);
74+
setTimeout(clearHistory, 100);
75+
toast.success("已清空所有历史纪录");
76+
}}
77+
>
78+
<Eraser size={14} className="mr-1" />
79+
清空历史
80+
</Button>
81+
</div>
82+
</>
83+
)}
84+
</PopoverContent>
85+
</Popover>
86+
);
87+
}
88+
89+
export default MailHistory;

components/mail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import MailList from "@/components/mail-list";
55

66
function Mail() {
77
return (
8-
<div className="flex h-0 flex-1 flex-col rounded-md border shadow">
8+
<div className="mb-3 flex h-0 flex-1 flex-col rounded-md border shadow">
99
<div className="border-b p-3">
1010
<div className="flex flex-wrap items-center gap-2">
1111
<MailTitle />

components/select-wrap.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,24 @@ import {
66
SelectTrigger,
77
SelectValue,
88
} from "@/components/ui/select";
9+
import { cn } from "@/lib/utils";
910

1011
function SelectWrap({
1112
value,
1213
setValue,
1314
list,
1415
disabled,
16+
className,
1517
}: {
1618
value: string;
1719
setValue: (v: string) => void;
1820
list: string[];
1921
disabled?: boolean;
22+
className?: string;
2023
}) {
2124
return (
2225
<Select disabled={disabled} value={value} onValueChange={setValue}>
23-
<SelectTrigger className="w-fit gap-1">
26+
<SelectTrigger className={cn("w-fit gap-1", className)}>
2427
<SelectValue />
2528
</SelectTrigger>
2629
<SelectContent>

components/ui/popover.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import * as PopoverPrimitive from "@radix-ui/react-popover";
5+
6+
import { cn } from "@/lib/utils";
7+
8+
const Popover = PopoverPrimitive.Root;
9+
10+
const PopoverTrigger = PopoverPrimitive.Trigger;
11+
12+
const PopoverAnchor = PopoverPrimitive.Anchor;
13+
14+
const PopoverContent = React.forwardRef<
15+
React.ElementRef<typeof PopoverPrimitive.Content>,
16+
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
17+
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
18+
<PopoverPrimitive.Portal>
19+
<PopoverPrimitive.Content
20+
ref={ref}
21+
align={align}
22+
sideOffset={sideOffset}
23+
className={cn(
24+
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
25+
className,
26+
)}
27+
{...props}
28+
/>
29+
</PopoverPrimitive.Portal>
30+
));
31+
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
32+
33+
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

lib/constant.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const DOMAIN_LIST = ["@sliu.eu.org"];
1+
export const DOMAIN_LIST = ["@isco.eu.org", "@sliu.eu.org"];
22

33
export const REFRESH_SECONDS =
44
process.env.NODE_ENV === "development" ? 100 : 10;

lib/store/config.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,39 @@ import { DOMAIN_LIST } from "@/lib/constant";
66
interface Config {
77
mail: string;
88
domain: string;
9+
history: string[];
910

1011
update(mail: string, domain: string): void;
12+
clearHistory(index?: number): void;
1113
}
1214

1315
export const useConfig = create<Config>()(
1416
persist(
15-
(set) => ({
17+
(set, get) => ({
1618
mail: randomMail(),
1719
domain: DOMAIN_LIST[0],
20+
history: [],
1821
update(mail: string, domain: string) {
19-
set({ mail, domain });
22+
const history = get().history;
23+
const old = get().mail + get().domain;
24+
const index = history.indexOf(old);
25+
if (index >= 0) {
26+
history.splice(index, 1);
27+
}
28+
history.unshift(old);
29+
set({ mail, domain, history });
30+
},
31+
clearHistory(index?: number) {
32+
if (index === undefined) {
33+
set({ history: [] });
34+
return;
35+
}
36+
const history = get().history;
37+
if (index >= history.length) {
38+
return;
39+
}
40+
history.splice(index, 1);
41+
set({ history });
2042
},
2143
}),
2244
{

package.json

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,17 @@
1010
},
1111
"dependencies": {
1212
"@radix-ui/react-dialog": "^1.0.5",
13+
"@radix-ui/react-popover": "^1.0.7",
1314
"@radix-ui/react-select": "^2.0.0",
1415
"@radix-ui/react-slot": "^1.0.2",
1516
"@vercel/analytics": "^1.2.2",
1617
"class-variance-authority": "^0.7.0",
1718
"clsx": "^2.1.0",
18-
"imapflow": "^1.0.156",
19-
"lucide-react": "^0.358.0",
19+
"imapflow": "^1.0.158",
20+
"lucide-react": "^0.359.0",
2021
"mailparser": "^3.6.9",
2122
"mitt": "^3.0.1",
22-
"next": "^14.1.3",
23+
"next": "^14.1.4",
2324
"next-themes": "^0.3.0",
2425
"react": "^18.2.0",
2526
"react-dom": "^18.2.0",
@@ -30,14 +31,14 @@
3031
"devDependencies": {
3132
"@types/imapflow": "^1.0.18",
3233
"@types/mailparser": "^3.4.4",
33-
"@types/node": "^20.11.28",
34-
"@types/react": "^18.2.66",
34+
"@types/node": "^20.11.30",
35+
"@types/react": "^18.2.67",
3536
"@types/react-dom": "^18.2.22",
3637
"autoprefixer": "^10.4.18",
3738
"eslint": "^8.57.0",
38-
"eslint-config-next": "^14.1.3",
39+
"eslint-config-next": "^14.1.4",
3940
"eslint-config-prettier": "^9.1.0",
40-
"postcss": "^8.4.36",
41+
"postcss": "^8.4.37",
4142
"prettier": "^3.2.5",
4243
"prettier-plugin-tailwindcss": "^0.5.12",
4344
"tailwindcss": "^3.4.1",

0 commit comments

Comments
 (0)