Skip to content

Commit b193a7f

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/input
2 parents f23b815 + 69347e9 commit b193a7f

File tree

29 files changed

+1022
-644
lines changed

29 files changed

+1022
-644
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "omnibox",
33
"private": true,
4-
"version": "0.1.4",
4+
"version": "0.1.10",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",
@@ -45,6 +45,7 @@
4545
"html-react-parser": "^5.2.7",
4646
"i18next": "^25.6.0",
4747
"i18next-browser-languagedetector": "^8.2.0",
48+
"input-otp": "^1.4.2",
4849
"ismobilejs": "^1.1.1",
4950
"jszip": "^3.10.1",
5051
"katex": "^0.16.25",

pnpm-lock.yaml

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/App.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@ const InvitePage = lazy(() => import('@/page/user/invite'));
1616
const ResourcePage = lazy(() => import('@/page/resource'));
1717
const NamespacePage = lazy(() => import('@/page/namespace'));
1818
const RegisterPage = lazy(() => import('@/page/user/register'));
19-
const ForgotPasswordPage = lazy(() => import('@/page/user/password'));
19+
const VerifyOtpPage = lazy(() => import('@/page/user/verify-otp'));
20+
const AcceptInvitePage = lazy(() => import('@/page/user/accept-invite'));
2021
const InviteRedirectPage = lazy(() => import('@/page/invite-redirect'));
21-
const PasswordConfirmPage = lazy(() => import('@/page/user/password-confirm'));
22-
const RegisterConfirmPage = lazy(() => import('@/page/user/register-confirm'));
2322
const WechatAuthConfirmPage = lazy(
2423
() => import('@/page/user/wechat/auth-confirm')
2524
);
@@ -69,16 +68,12 @@ const router = createBrowserRouter([
6968
element: <RegisterPage />,
7069
},
7170
{
72-
path: 'user/sign-up/confirm',
73-
element: <RegisterConfirmPage />,
71+
path: 'user/verify-otp',
72+
element: <VerifyOtpPage />,
7473
},
7574
{
76-
path: 'user/password',
77-
element: <ForgotPasswordPage />,
78-
},
79-
{
80-
path: 'user/password/confirm',
81-
element: <PasswordConfirmPage />,
75+
path: 'user/accept-invite',
76+
element: <AcceptInvitePage />,
8277
},
8378
{
8479
path: 'invite/confirm',

src/components/ui/input-otp.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { OTPInput, OTPInputContext } from 'input-otp';
2+
import { Minus } from 'lucide-react';
3+
import * as React from 'react';
4+
5+
import { cn } from '@/lib/utils';
6+
7+
const InputOTP = React.forwardRef<
8+
React.ElementRef<typeof OTPInput>,
9+
React.ComponentPropsWithoutRef<typeof OTPInput>
10+
>(({ className, containerClassName, ...props }, ref) => (
11+
<OTPInput
12+
ref={ref}
13+
containerClassName={cn(
14+
'flex items-center gap-2 has-[:disabled]:opacity-50',
15+
containerClassName
16+
)}
17+
className={cn('disabled:cursor-not-allowed', className)}
18+
{...props}
19+
/>
20+
));
21+
InputOTP.displayName = 'InputOTP';
22+
23+
const InputOTPGroup = React.forwardRef<
24+
React.ElementRef<'div'>,
25+
React.ComponentPropsWithoutRef<'div'>
26+
>(({ className, ...props }, ref) => (
27+
<div ref={ref} className={cn('flex items-center', className)} {...props} />
28+
));
29+
InputOTPGroup.displayName = 'InputOTPGroup';
30+
31+
const InputOTPSlot = React.forwardRef<
32+
React.ElementRef<'div'>,
33+
React.ComponentPropsWithoutRef<'div'> & { index: number }
34+
>(({ index, className, ...props }, ref) => {
35+
const inputOTPContext = React.useContext(OTPInputContext);
36+
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
37+
38+
return (
39+
<div
40+
ref={ref}
41+
className={cn(
42+
'relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md',
43+
isActive && 'z-10 ring-1 ring-ring',
44+
className
45+
)}
46+
{...props}
47+
>
48+
{char}
49+
{hasFakeCaret && (
50+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
51+
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
52+
</div>
53+
)}
54+
</div>
55+
);
56+
});
57+
InputOTPSlot.displayName = 'InputOTPSlot';
58+
59+
const InputOTPSeparator = React.forwardRef<
60+
React.ElementRef<'div'>,
61+
React.ComponentPropsWithoutRef<'div'>
62+
>(({ ...props }, ref) => (
63+
<div ref={ref} role="separator" {...props}>
64+
<Minus />
65+
</div>
66+
));
67+
InputOTPSeparator.displayName = 'InputOTPSeparator';
68+
69+
export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot };

src/const.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,9 @@ export const WECHAT_GROUP_QRCODE_URL =
1212
export const RESOURCE_TASKS_INTERVAL = 3 * 1000;
1313
export const BIND_CHECK_INTERVAL = 3 * 1000;
1414
export const DISCORD_LINK = 'https://www.omnibox.pro/links/discord';
15+
export const ALLOWED_EMAIL_DOMAINS = [
16+
'gmail.com',
17+
'outlook.com',
18+
'163.com',
19+
'qq.com',
20+
];

src/hooks/use-async.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { useCallback, useState } from 'react';
2+
3+
/**
4+
* Custom hook for managing async operations with loading and error states
5+
*
6+
* @example
7+
* const { loading, error, execute } = useAsync(async (data) => {
8+
* return await http.post('/api/endpoint', data);
9+
* });
10+
*
11+
* // Later in your component
12+
* await execute(formData);
13+
*/
14+
export function useAsync<T, Args extends any[] = any[]>(
15+
asyncFunction: (...args: Args) => Promise<T>
16+
) {
17+
const [loading, setLoading] = useState(false);
18+
const [error, setError] = useState<Error | null>(null);
19+
20+
const execute = useCallback(
21+
async (...args: Args): Promise<T | undefined> => {
22+
setLoading(true);
23+
setError(null);
24+
try {
25+
const result = await asyncFunction(...args);
26+
return result;
27+
} catch (err) {
28+
setError(err as Error);
29+
throw err;
30+
} finally {
31+
setLoading(false);
32+
}
33+
},
34+
[asyncFunction]
35+
);
36+
37+
return { loading, error, execute };
38+
}

src/i18n/locales/en.json

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
"form": {
7373
"or": "or",
7474
"operator": "Action",
75-
"exist_account": "Already have an account?",
75+
"exist_account": "Already have an account? ",
7676
"invalid_request": "Invalid request parameters",
7777
"username": "Username",
7878
"username_min": "Username must be at least 2 characters",
@@ -105,7 +105,10 @@
105105
"submit": "Save",
106106
"button": "Send Invitation",
107107
"success": "Invitation sent",
108-
"add_namespace": "Add namespace"
108+
"add_namespace": "Add namespace",
109+
"please_login_first": "Please log in first to accept this invitation",
110+
"user_mismatch": "This invitation is for {{ username }}",
111+
"user_mismatch_generic": "This invitation is for another user"
109112
},
110113
"namespace": {
111114
"add": "Add Namespace",
@@ -122,6 +125,12 @@
122125
"product_name": "OmniBox",
123126
"logout": "Logout",
124127
"submit": "Login",
128+
"continue": "Continue",
129+
"use_password": "Use password",
130+
"use_email": "Use email code",
131+
"sign_up": "Sign Up",
132+
"error_sending_otp": "Failed to send verification code",
133+
"email_not_exists": "This email doesn't exist. Please sign up to continue.",
125134
"title": "Login to your account",
126135
"description": "Enter your email below to login your account",
127136
"authorizing": "Authorizing, please wait...",
@@ -141,9 +150,38 @@
141150
"description": "Enter your information to create an account",
142151
"submit": "Sign Up",
143152
"success": "Registration successful, please check your email to activate your account",
153+
"error_sending_otp": "Failed to send verification code",
154+
"email_already_exists": "This email is already registered. Please login to continue.",
144155
"conform_title": "Complete Account Creation",
145156
"conform_description": "Enter your information to complete account creation"
146157
},
158+
"verify_otp": {
159+
"title": "Enter verification code",
160+
"description": "We sent a 6-digit code to",
161+
"didnt_receive": "Didn't receive the code?",
162+
"resend": "Resend code",
163+
"resending": "Sending...",
164+
"resend_countdown": "Resend in {{seconds}}s",
165+
"resend_success": "Code sent successfully",
166+
"error_resend": "Failed to resend code",
167+
"error_invalid_code": "Invalid verification code",
168+
"error_invalid_code_with_attempts": "Invalid verification code. {{remaining}} attempts remaining.",
169+
"error_expired_code": "Verification code has expired",
170+
"error_too_many_attempts": "Too many failed attempts. Please request a new code.",
171+
"error_too_many_otp_requests": "Too many OTP requests. Please try again in {{minutes}} minutes.",
172+
"error_magic_link": "Invalid or expired magic link",
173+
"open_email": "Open email app",
174+
"back_to_login": "Back to login",
175+
"verifying": "Verifying...",
176+
"verifying_description": "Please wait..."
177+
},
178+
"accept_invite": {
179+
"accepting": "Accepting invitation...",
180+
"accepting_description": "Creating your account and joining the space",
181+
"error_accept": "Failed to accept invitation. The link may be expired or invalid.",
182+
"error_title": "Invitation Error",
183+
"back_to_login": "Back to login"
184+
},
147185
"password": {
148186
"title": "Reset Password",
149187
"description": "Enter your email address and we'll send you a link to reset your password",

src/i18n/locales/zh.json

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
"email_invalid": "请输入有效的邮箱地址",
8888
"email_limit": "仅支持 Gmail、Outlook、163 和 QQ 邮箱",
8989
"email_limit_rule": "邮箱必须是 Gmail、Outlook、163 或 QQ 邮箱",
90-
"email_or_username": "邮箱或用户名",
90+
"email_or_username": "邮箱 或 用户名",
9191
"email_or_username_invalid": "请输入有效的邮箱地址或用户名",
9292
"role": "角色",
9393
"username_not_emptyStr": "无效的用户名"
@@ -105,7 +105,10 @@
105105
"submit": "保存",
106106
"button": "发送邀请",
107107
"success": "邀请已发送",
108-
"add_namespace": "加入空间"
108+
"add_namespace": "加入空间",
109+
"please_login_first": "请先登录以接受此邀请",
110+
"user_mismatch": "此邀请是给 {{ username }} 的",
111+
"user_mismatch_generic": "此邀请是给其他用户的"
109112
},
110113
"namespace": {
111114
"add": "创建空间",
@@ -122,6 +125,12 @@
122125
"product_name": "小黑",
123126
"logout": "退出登录",
124127
"submit": "登录",
128+
"continue": "继续",
129+
"use_password": "使用密码登录",
130+
"use_email": "使用邮箱验证码",
131+
"sign_up": "注册",
132+
"error_sending_otp": "发送验证码失败",
133+
"email_not_exists": "该邮箱不存在,请注册以继续。",
125134
"title": "登录您的账户",
126135
"description": "请输入您的邮箱以登录账户",
127136
"authorizing": "授权中,请稍候...",
@@ -141,9 +150,38 @@
141150
"description": "输入您的信息以创建账户",
142151
"submit": "注册",
143152
"success": "注册成功,请检查邮箱以激活账户",
153+
"error_sending_otp": "发送验证码失败",
154+
"email_already_exists": "该邮箱已注册,请登录以继续。",
144155
"conform_title": "完成账户创建",
145156
"conform_description": "输入您的信息以完成账户创建"
146157
},
158+
"verify_otp": {
159+
"title": "输入验证码",
160+
"description": "我们已向以下邮箱发送 6 位验证码:",
161+
"didnt_receive": "没有收到验证码?",
162+
"resend": "重新发送",
163+
"resending": "发送中...",
164+
"resend_countdown": "{{seconds}} 秒后可重新发送",
165+
"resend_success": "验证码发送成功",
166+
"error_resend": "重新发送失败",
167+
"error_invalid_code": "验证码无效",
168+
"error_invalid_code_with_attempts": "验证码无效。还剩 {{remaining}} 次尝试机会。",
169+
"error_expired_code": "验证码已过期",
170+
"error_too_many_attempts": "尝试次数过多,请重新获取验证码。",
171+
"error_too_many_otp_requests": "验证码请求过多,请在 {{minutes}} 分钟后重试。",
172+
"error_magic_link": "链接无效或已过期",
173+
"open_email": "打开邮件应用",
174+
"back_to_login": "返回登录",
175+
"verifying": "验证中...",
176+
"verifying_description": "请稍候..."
177+
},
178+
"accept_invite": {
179+
"accepting": "接受邀请中...",
180+
"accepting_description": "正在创建您的账户并加入空间",
181+
"error_accept": "接受邀请失败。链接可能已过期或无效。",
182+
"error_title": "邀请错误",
183+
"back_to_login": "返回登录"
184+
},
147185
"password": {
148186
"title": "重置密码",
149187
"description": "输入您的邮箱地址,我们将向您发送重置密码的链接",

src/lib/email-validation.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ALLOWED_EMAIL_DOMAINS } from '@/const';
2+
3+
/**
4+
* Check if an email domain is in the allowed list
5+
*/
6+
export function isAllowedEmailDomain(email: string): boolean {
7+
const domain = email.split('@')[1];
8+
return ALLOWED_EMAIL_DOMAINS.includes(domain);
9+
}
10+
11+
/**
12+
* Create a validation function for email domains with custom error message
13+
*/
14+
export function createEmailDomainValidator(errorMessage: string) {
15+
return (email: string) => isAllowedEmailDomain(email) || errorMessage;
16+
}
17+
18+
/**
19+
* Get the domain from an email address
20+
*/
21+
export function getEmailDomain(email: string): string {
22+
return email.split('@')[1] || '';
23+
}

src/lib/utils.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,23 @@ import { twMerge } from 'tailwind-merge';
44
export function cn(...inputs: ClassValue[]) {
55
return twMerge(clsx(inputs));
66
}
7+
8+
export function isBlank(value: string | null | undefined): boolean {
9+
return value === null || value === undefined || value === '';
10+
}
11+
12+
export function buildUrl(
13+
url: string,
14+
params: Record<string, string | undefined | null>
15+
): string {
16+
const filteredParams = Object.entries(params)
17+
.filter(([, value]) => !isBlank(value))
18+
.map(([key, value]) => `${key}=${encodeURIComponent(value!)}`)
19+
.join('&');
20+
21+
if (!filteredParams) {
22+
return url;
23+
}
24+
25+
return `${url}?${filteredParams}`;
26+
}

0 commit comments

Comments
 (0)