+4
-2
@@ -504,6 +504,7 @@ ui:
|
||||
title: Notifications
|
||||
inbox: Inbox
|
||||
achievement: Achievements
|
||||
new_alerts: New alerts
|
||||
all_read: Mark all as read
|
||||
show_more: Show more
|
||||
someone: Someone
|
||||
@@ -729,7 +730,7 @@ ui:
|
||||
tip_answer: >-
|
||||
Use comments to reply to other users or notify them of changes. If you are
|
||||
adding new information, edit your post instead of commenting.
|
||||
tip_vote: It adds something useful to the post
|
||||
tip_vote: It is useful
|
||||
edit_answer:
|
||||
title: Edit Answer
|
||||
default_reason: Edit answer
|
||||
@@ -940,7 +941,6 @@ ui:
|
||||
gravatar: Gravatar
|
||||
gravatar_text: You can change image on
|
||||
custom: Custom
|
||||
btn_refresh: Refresh
|
||||
custom_text: You can upload your image.
|
||||
default: System
|
||||
msg: Please upload an avatar
|
||||
@@ -1025,6 +1025,7 @@ ui:
|
||||
useful: Useful
|
||||
question_useful: It is useful and clear
|
||||
question_un_useful: It is unclear or not useful
|
||||
question_bookmark: Bookmark this question
|
||||
answer_useful: It is useful
|
||||
answer_un_useful: It is not useful
|
||||
answers:
|
||||
@@ -1092,6 +1093,7 @@ ui:
|
||||
question: Question
|
||||
answer: Answer
|
||||
comment: Comment
|
||||
refresh: Refresh
|
||||
search:
|
||||
title: Search Results
|
||||
keywords: Keywords
|
||||
|
||||
@@ -131,6 +131,7 @@ const Index: FC<Props> = ({ className, data, source }) => {
|
||||
{!data?.hideCollect && (
|
||||
<Button
|
||||
variant="outline-secondary ms-3"
|
||||
title={t('question_detail.question_bookmark')}
|
||||
active={bookmarkState.state}
|
||||
onClick={handleBookmark}>
|
||||
<Icon name="bookmark-fill" />
|
||||
|
||||
@@ -11,9 +11,16 @@ interface IProps {
|
||||
size: string;
|
||||
searchStr?: string;
|
||||
className?: string;
|
||||
alt: string;
|
||||
}
|
||||
|
||||
const Index: FC<IProps> = ({ avatar, size, className, searchStr = '' }) => {
|
||||
const Index: FC<IProps> = ({
|
||||
avatar,
|
||||
size,
|
||||
className,
|
||||
searchStr = '',
|
||||
alt,
|
||||
}) => {
|
||||
let url = '';
|
||||
if (typeof avatar === 'string') {
|
||||
if (avatar.length > 1) {
|
||||
@@ -31,13 +38,17 @@ const Index: FC<IProps> = ({ avatar, size, className, searchStr = '' }) => {
|
||||
className && className.indexOf('rounded') !== -1 ? '' : 'rounded';
|
||||
|
||||
return (
|
||||
<img
|
||||
src={url || DefaultAvatar}
|
||||
width={size}
|
||||
height={size}
|
||||
className={classNames(roundedCls, className)}
|
||||
alt=""
|
||||
/>
|
||||
<>
|
||||
{/* eslint-disable jsx-a11y/no-noninteractive-element-to-interactive-role,jsx-a11y/control-has-associated-label */}
|
||||
<img
|
||||
role="button"
|
||||
src={url || DefaultAvatar}
|
||||
width={size}
|
||||
height={size}
|
||||
className={classNames(roundedCls, className)}
|
||||
alt={alt}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ const Index: FC<Props> = ({
|
||||
size={avatarSize}
|
||||
className={`me-1 ${avatarClass}`}
|
||||
searchStr={avatarSearchStr}
|
||||
alt={data?.display_name}
|
||||
/>
|
||||
)}
|
||||
<span className="me-1 text-break">{data?.display_name}</span>
|
||||
@@ -45,6 +46,7 @@ const Index: FC<Props> = ({
|
||||
size={avatarSize}
|
||||
className={`me-1 ${avatarClass}`}
|
||||
searchStr={avatarSearchStr}
|
||||
alt={data?.display_name}
|
||||
/>
|
||||
)}
|
||||
<span className="me-1 text-break">{data?.display_name}</span>
|
||||
|
||||
@@ -34,7 +34,10 @@ const Index: FC = () => {
|
||||
--bs-primary-rgb: ${primaryColor.rgb().array().join(',')};
|
||||
--bs-link-color: ${primaryColor.hex()};
|
||||
--bs-link-color-rgb: ${primaryColor.rgb().array().join(',')};
|
||||
--bs-link-hover-color: ${shiftColor(primaryColor, 0.8)};
|
||||
--bs-link-hover-color: ${shiftColor(primaryColor, 0.8).hex()};
|
||||
--bs-link-hover-color-rgb: ${shiftColor(primaryColor, 0.8)
|
||||
.round()
|
||||
.array()}
|
||||
}
|
||||
.nav-pills {
|
||||
--bs-nav-pills-link-active-bg: ${primaryColor.hex()};
|
||||
|
||||
@@ -10,7 +10,7 @@ import { userCenterStore } from '@/stores';
|
||||
|
||||
interface Props {
|
||||
redDot: Type.NotificationStatus | undefined;
|
||||
userInfo;
|
||||
userInfo: Type.UserInfoRes;
|
||||
logOut: () => void;
|
||||
}
|
||||
|
||||
@@ -32,18 +32,30 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
|
||||
<Nav.Link
|
||||
as={NavLink}
|
||||
to="/users/notifications/inbox"
|
||||
title={t('inbox', { keyPrefix: 'notifications' })}
|
||||
className="icon-link d-flex align-items-center justify-content-center p-0 me-3 position-relative">
|
||||
<Icon name="bell-fill" className="fs-4" />
|
||||
{(redDot?.inbox || 0) > 0 && <div className="unread-dot bg-danger" />}
|
||||
{(redDot?.inbox || 0) > 0 && (
|
||||
<div className="unread-dot bg-danger">
|
||||
<span className="visually-hidden">
|
||||
{t('new_alerts', { keyPrefix: 'notifications' })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Nav.Link>
|
||||
|
||||
<Nav.Link
|
||||
as={NavLink}
|
||||
to="/users/notifications/achievement"
|
||||
title={t('achievement', { keyPrefix: 'notifications' })}
|
||||
className="icon-link d-flex align-items-center justify-content-center p-0 me-3 position-relative">
|
||||
<Icon name="trophy-fill" className="fs-4" />
|
||||
{(redDot?.achievement || 0) > 0 && (
|
||||
<div className="unread-dot bg-danger" />
|
||||
<div className="unread-dot bg-danger">
|
||||
<span className="visually-hidden">
|
||||
{t('new_alerts', { keyPrefix: 'notifications' })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Nav.Link>
|
||||
</Nav>
|
||||
@@ -54,7 +66,12 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
|
||||
id="dropdown-basic"
|
||||
as="a"
|
||||
className="no-toggle pointer">
|
||||
<Avatar size="36px" avatar={userInfo?.avatar} searchStr="s=96" />
|
||||
<Avatar
|
||||
size="36px"
|
||||
avatar={userInfo?.avatar}
|
||||
alt={userInfo?.display_name}
|
||||
searchStr="s=96"
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
|
||||
@@ -45,7 +45,7 @@ const Header: FC = () => {
|
||||
const siteInfo = siteInfoStore((state) => state.siteInfo);
|
||||
const brandingInfo = brandingStore((state) => state.branding);
|
||||
const loginSetting = loginSettingStore((state) => state.login);
|
||||
const { updateReiview, updateVisible } = sideNavStore();
|
||||
const { updateReview, updateVisible } = sideNavStore();
|
||||
const { data: redDot } = useQueryNotificationStatus();
|
||||
/**
|
||||
* Automatically append `tag` information when creating a question
|
||||
@@ -57,7 +57,7 @@ const Header: FC = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateReiview({
|
||||
updateReview({
|
||||
can_revision: Boolean(redDot?.can_revision),
|
||||
revision: Number(redDot?.revision),
|
||||
});
|
||||
@@ -126,13 +126,13 @@ const Header: FC = () => {
|
||||
<img
|
||||
className="d-none d-lg-block logo me-0"
|
||||
src={brandingInfo.logo}
|
||||
alt=""
|
||||
alt={siteInfo.name}
|
||||
/>
|
||||
|
||||
<img
|
||||
className="lg-none logo me-0"
|
||||
src={brandingInfo.mobile_logo || brandingInfo.logo}
|
||||
alt=""
|
||||
alt={siteInfo.name}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
@@ -178,6 +178,7 @@ const Header: FC = () => {
|
||||
className="w-100 maxw-400"
|
||||
onSubmit={handleSearch}>
|
||||
<FormControl
|
||||
type="search"
|
||||
placeholder={t('header.search.placeholder')}
|
||||
className="placeholder-search"
|
||||
value={searchStr}
|
||||
|
||||
@@ -17,7 +17,7 @@ const Icon: FC<IProps> = ({
|
||||
className,
|
||||
size,
|
||||
onClick,
|
||||
title = '',
|
||||
title,
|
||||
}) => {
|
||||
return (
|
||||
<i
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
cursor: zoom-out !important;
|
||||
}
|
||||
|
||||
.img-viewer img:not(a img, img.broken) {
|
||||
.img-viewer img:not(a img, img.broken, img.invisible) {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ const Index: React.FC<IProps> = ({
|
||||
<Button
|
||||
onClick={refreshImgCode}
|
||||
variant="outline-secondary"
|
||||
title={t('refresh', { keyPrefix: 'btns' })}
|
||||
style={{
|
||||
borderTopRightRadius: '0.375rem',
|
||||
borderBottomRightRadius: '0.375rem',
|
||||
|
||||
@@ -246,8 +246,9 @@ const TagSelector: FC<IProps> = ({
|
||||
e.preventDefault();
|
||||
}}>
|
||||
<FormControl
|
||||
placeholder={t('search_tag')}
|
||||
autoFocus
|
||||
type="search"
|
||||
placeholder={t('search_tag')}
|
||||
value={searchValue}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
|
||||
@@ -32,6 +32,7 @@ const Index: FC<Props> = ({
|
||||
size="40px"
|
||||
className="me-2 d-none d-md-block"
|
||||
searchStr="s=96"
|
||||
alt={data?.display_name}
|
||||
/>
|
||||
|
||||
<Avatar
|
||||
@@ -39,6 +40,7 @@ const Index: FC<Props> = ({
|
||||
size="24px"
|
||||
className="me-2 d-block d-md-none"
|
||||
searchStr="s=48"
|
||||
alt={data?.display_name}
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
@@ -48,6 +50,7 @@ const Index: FC<Props> = ({
|
||||
size="40px"
|
||||
className="me-2 d-none d-md-block"
|
||||
searchStr="s=96"
|
||||
alt={data?.display_name}
|
||||
/>
|
||||
|
||||
<Avatar
|
||||
@@ -55,6 +58,7 @@ const Index: FC<Props> = ({
|
||||
size="24px"
|
||||
className="me-2 d-block d-md-none"
|
||||
searchStr="s=48"
|
||||
alt={data?.display_name}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -201,6 +201,7 @@ const useTagModal = (props: IProps = {}) => {
|
||||
<Form.Group controlId="displayName" className="mb-3">
|
||||
<Form.Label>{t('form.fields.display_name.label')}</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={formData.displayName.value}
|
||||
onChange={handleDisplayNameChange}
|
||||
isInvalid={formData.displayName.isInvalid}
|
||||
@@ -212,6 +213,7 @@ const useTagModal = (props: IProps = {}) => {
|
||||
<Form.Group controlId="slugName" className="mb-3">
|
||||
<Form.Label>{t('form.fields.slug_name.label')}</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={formData.slugName.value}
|
||||
onChange={handleSlugNameChange}
|
||||
isInvalid={formData.slugName.isInvalid}
|
||||
|
||||
@@ -10,6 +10,27 @@ const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement,
|
||||
);
|
||||
|
||||
/**
|
||||
* Uniformly hide broken images
|
||||
*/
|
||||
document.addEventListener(
|
||||
'error',
|
||||
(err) => {
|
||||
const { target } = err;
|
||||
if (target === null || !(target instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (/IMG/.test(target.nodeName)) {
|
||||
const altText = target.getAttribute('alt')?.trim();
|
||||
if (!altText) {
|
||||
target.classList.add('invisible');
|
||||
}
|
||||
}
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
|
||||
@@ -103,7 +103,7 @@ const Answers: FC = () => {
|
||||
value={curQuery}
|
||||
onChange={handleFilter}
|
||||
size="sm"
|
||||
type="input"
|
||||
type="search"
|
||||
placeholder={t('filter.placeholder')}
|
||||
style={{ width: '12.25rem' }}
|
||||
/>
|
||||
|
||||
@@ -112,7 +112,7 @@ const Users: FC = () => {
|
||||
<td className="text-end">
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle variant="link" className="no-toggle">
|
||||
<Icon name="three-dots-vertical" />
|
||||
<Icon name="three-dots-vertical" title={t('action')} />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{plugin.enabled ? (
|
||||
|
||||
@@ -112,7 +112,7 @@ const Questions: FC = () => {
|
||||
<Form.Control
|
||||
value={curQuery}
|
||||
size="sm"
|
||||
type="input"
|
||||
type="search"
|
||||
placeholder={t('filter.placeholder')}
|
||||
onChange={handleFilter}
|
||||
style={{ width: '12.25rem' }}
|
||||
|
||||
@@ -193,6 +193,7 @@ const Users: FC = () => {
|
||||
|
||||
<Form.Control
|
||||
size="sm"
|
||||
type="search"
|
||||
value={curQuery}
|
||||
onChange={handleFilter}
|
||||
placeholder={t('filter.placeholder')}
|
||||
@@ -270,7 +271,7 @@ const Users: FC = () => {
|
||||
<td className="text-end">
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle variant="link" className="no-toggle">
|
||||
<Icon name="three-dots-vertical" />
|
||||
<Icon name="three-dots-vertical" title={t('action')} />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{showActionPassword ? (
|
||||
|
||||
@@ -347,6 +347,7 @@ const Ask = () => {
|
||||
<Form.Group controlId="title" className="mb-3">
|
||||
<Form.Label>{t('form.fields.title.label')}</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={formData.title.value}
|
||||
isInvalid={formData.title.isInvalid}
|
||||
onChange={handleTitleChange}
|
||||
|
||||
@@ -67,9 +67,11 @@ const Index: FC<Props> = ({
|
||||
}, 100);
|
||||
}
|
||||
}, [data.id, answerRef.current]);
|
||||
|
||||
if (!data?.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id={data.id} ref={answerRef} className="answer-item py-4">
|
||||
{data.status === 10 && (
|
||||
|
||||
@@ -130,6 +130,7 @@ const Index: FC<Props> = ({
|
||||
{toggleState ? (
|
||||
<Form.Control
|
||||
autoFocus
|
||||
type="search"
|
||||
placeholder={t('search')}
|
||||
value={searchValue}
|
||||
onChange={handleSearch}
|
||||
@@ -144,7 +145,12 @@ const Index: FC<Props> = ({
|
||||
active={idx === currentIndex}
|
||||
className={idx === 0 ? 'mt-2' : ''}>
|
||||
<div className="d-flex align-items-center text-nowrap">
|
||||
<Avatar avatar={p.avatar} size="24" className="rounded-1" />
|
||||
<Avatar
|
||||
avatar={p.avatar}
|
||||
size="24"
|
||||
alt={p.display_name}
|
||||
className="rounded-1"
|
||||
/>
|
||||
<div className="d-flex flex-wrap text-truncate">
|
||||
<span className="ms-2 text-truncate">{p.display_name}</span>
|
||||
<small className="text-secondary text-truncate ms-2">
|
||||
|
||||
@@ -119,6 +119,7 @@ const Index: FC<Props> = ({ questionId, readOnly = false }) => {
|
||||
avatar={user.avatar}
|
||||
size="20"
|
||||
className="rounded-1"
|
||||
alt={user.display_name}
|
||||
/>
|
||||
<span className="text-nowrap ms-2">{user.display_name}</span>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
|
||||
@@ -135,7 +136,12 @@ const Index: FC<Props> = ({ questionId, readOnly = false }) => {
|
||||
key={user.username}
|
||||
to={`/users/${user.username}`}
|
||||
className="mx-2 my-1 d-inline-flex flex-nowrap">
|
||||
<Avatar avatar={user.avatar} size="24" className="rounded-1" />
|
||||
<Avatar
|
||||
avatar={user.avatar}
|
||||
size="24"
|
||||
alt={user.display_name}
|
||||
className="rounded-1"
|
||||
/>
|
||||
<small className="text-nowrap ms-2">{user.display_name}</small>
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -147,6 +147,7 @@ const Index = () => {
|
||||
<Form.Group controlId="display_name" className="mb-3">
|
||||
<Form.Label>{t('form.fields.display_name.label')}</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={formData.displayName.value}
|
||||
isInvalid={formData.displayName.isInvalid}
|
||||
disabled={role_id !== 2 && role_id !== 3}
|
||||
@@ -160,6 +161,7 @@ const Index = () => {
|
||||
<Form.Group controlId="slug_name" className="mb-3">
|
||||
<Form.Label>{t('form.fields.slug_name.label')}</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={formData.slugName.value}
|
||||
isInvalid={formData.slugName.isInvalid}
|
||||
disabled={role_id !== 2 && role_id !== 3}
|
||||
|
||||
@@ -62,7 +62,7 @@ const Tags = () => {
|
||||
<Form.Control
|
||||
value={searchTag}
|
||||
placeholder={t('search_placeholder')}
|
||||
type="text"
|
||||
type="search"
|
||||
onChange={handleChange}
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
@@ -39,10 +39,20 @@ const Index: FC<Props> = ({ data }) => {
|
||||
<div className="d-flex flex-column flex-md-row mb-4">
|
||||
{data?.status !== 'deleted' ? (
|
||||
<Link to={`/users/${data.username}`} reloadDocument>
|
||||
<Avatar avatar={data.avatar} size="160px" searchStr="s=256" />
|
||||
<Avatar
|
||||
avatar={data.avatar}
|
||||
size="160px"
|
||||
searchStr="s=256"
|
||||
alt={data.display_name}
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<Avatar avatar={data.avatar} size="160px" searchStr="s=256" />
|
||||
<Avatar
|
||||
avatar={data.avatar}
|
||||
size="160px"
|
||||
searchStr="s=256"
|
||||
alt={data.display_name}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="ms-0 ms-md-4 mt-4 mt-md-0">
|
||||
|
||||
@@ -62,29 +62,7 @@ const Index: React.FC<Props> = ({ callback }) => {
|
||||
|
||||
const checkValidated = (): boolean => {
|
||||
let bol = true;
|
||||
const { name, e_mail, pass } = formData;
|
||||
if (!name.value) {
|
||||
bol = false;
|
||||
formData.name = {
|
||||
value: '',
|
||||
isInvalid: true,
|
||||
errorMsg: t('name.msg.empty'),
|
||||
};
|
||||
} else if (/[^a-z0-9\-._]/.test(name.value)) {
|
||||
bol = false;
|
||||
formData.name = {
|
||||
value: name.value,
|
||||
isInvalid: true,
|
||||
errorMsg: t('name.msg.character'),
|
||||
};
|
||||
} else if ([...name.value].length > 30) {
|
||||
bol = false;
|
||||
formData.name = {
|
||||
value: name.value,
|
||||
isInvalid: true,
|
||||
errorMsg: t('name.msg.range'),
|
||||
};
|
||||
}
|
||||
const { e_mail, pass } = formData;
|
||||
|
||||
if (!e_mail.value) {
|
||||
bol = false;
|
||||
|
||||
@@ -353,6 +353,7 @@ const Index: React.FC = () => {
|
||||
count > 0 ? `&t=${new Date().valueOf()}` : ''
|
||||
}`}
|
||||
className="me-3 rounded"
|
||||
alt={formData.display_name.value}
|
||||
/>
|
||||
<Form.Text className="text-muted mt-1">
|
||||
<span>{t('avatar.gravatar_text')}</span>
|
||||
@@ -381,6 +382,7 @@ const Index: React.FC = () => {
|
||||
searchStr="s=256"
|
||||
avatar={formData.avatar.custom}
|
||||
className="me-2 bg-gray-300 "
|
||||
alt={formData.display_name.value}
|
||||
/>
|
||||
<ButtonGroup vertical className="fit-content">
|
||||
<UploadImg
|
||||
@@ -405,7 +407,11 @@ const Index: React.FC = () => {
|
||||
</Stack>
|
||||
)}
|
||||
{formData.avatar.type === 'default' && (
|
||||
<Avatar size="160px" avatar="" />
|
||||
<Avatar
|
||||
size="160px"
|
||||
avatar=""
|
||||
alt={formData.display_name.value}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ImgViewer>
|
||||
|
||||
@@ -55,6 +55,7 @@ const Users = () => {
|
||||
size="48px"
|
||||
avatar={user?.avatar}
|
||||
searchStr="s=96"
|
||||
alt={user.display_name}
|
||||
/>
|
||||
</Link>
|
||||
<div className="ms-2">
|
||||
|
||||
@@ -10,7 +10,7 @@ interface ErrorCodeType {
|
||||
can_revision: boolean;
|
||||
revision: number;
|
||||
updateVisible: () => void;
|
||||
updateReiview: (params: reviewData) => void;
|
||||
updateReview: (params: reviewData) => void;
|
||||
}
|
||||
|
||||
const Index = create<ErrorCodeType>((set) => ({
|
||||
@@ -22,7 +22,7 @@ const Index = create<ErrorCodeType>((set) => ({
|
||||
return { visible: !state.visible };
|
||||
});
|
||||
},
|
||||
updateReiview: (params: reviewData) => {
|
||||
updateReview: (params: reviewData) => {
|
||||
set(() => {
|
||||
return { ...params };
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user