mirror of https://gitee.com/answerdev/answer.git
fix(ui): resolve conflict
This commit is contained in:
commit
b35d2564b5
|
@ -608,7 +608,8 @@ ui:
|
|||
msg:
|
||||
empty: Cannot be empty.
|
||||
login:
|
||||
page_title: Welcome to Answer
|
||||
page_title: Welcome to {{site_name}}
|
||||
login_to_continue: Log in to continue
|
||||
info_sign: Don't have an account? <1>Sign up</1>
|
||||
info_login: Already have an account? <1>Log in</1>
|
||||
agreements: By registering, you agree to the <1>privacy policy</1> and <3>terms of service</3>.
|
||||
|
@ -1288,14 +1289,14 @@ ui:
|
|||
branding:
|
||||
page_title: Branding
|
||||
logo:
|
||||
label: Logo
|
||||
label: Logo (optional)
|
||||
msg: Logo cannot be empty.
|
||||
text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown.
|
||||
mobile_logo:
|
||||
label: Mobile Logo (optional)
|
||||
text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used.
|
||||
square_icon:
|
||||
label: Square Icon
|
||||
label: Square Icon (optional)
|
||||
msg: Square icon cannot be empty.
|
||||
text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
|
||||
favicon:
|
||||
|
|
|
@ -71,7 +71,7 @@ If you would like to help us with the i18n translation, please visit [Answer@Cro
|
|||
├── common (project information/data defined here)
|
||||
├── components (all components of the project)
|
||||
├── hooks (all hooks of the project)
|
||||
├── i18n (Initialize the front-end i18n)
|
||||
├── i18n (Used only to initialize the front-end i18n tool)
|
||||
├── pages (all pages of the project)
|
||||
├── router (Project routing definition)
|
||||
├── services (all data api of the project)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
$link-hover-decoration: none;
|
||||
$enable-negative-margins: true;
|
||||
$blue: #0033FF !default;
|
||||
$placeholder-opacity-max: .2;
|
||||
$placeholder-opacity-min: .1;
|
||||
|
|
|
@ -104,6 +104,7 @@ export interface ModifyUserReq {
|
|||
}
|
||||
|
||||
export interface UserInfoBase {
|
||||
id?: string;
|
||||
avatar: any;
|
||||
username: string;
|
||||
display_name: string;
|
||||
|
|
|
@ -27,7 +27,6 @@ import './index.scss';
|
|||
const Comment = ({ objectId, mode, commentId }) => {
|
||||
const pageUsers = usePageUsers();
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [comments, setComments] = useState<any>([]);
|
||||
const [visibleComment, setVisibleComment] = useState(false);
|
||||
const pageSize = pageIndex === 0 ? 3 : 15;
|
||||
const { data, mutate } = useQueryComments({
|
||||
|
@ -36,6 +35,7 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
page: pageIndex,
|
||||
page_size: pageSize,
|
||||
});
|
||||
const [comments, setComments] = useState<any>([]);
|
||||
|
||||
const reportModal = useReportModal();
|
||||
|
||||
|
@ -75,6 +75,9 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
}, [data]);
|
||||
|
||||
const handleReply = (id) => {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
setComments(
|
||||
comments.map((item) => {
|
||||
if (item.comment_id === id) {
|
||||
|
@ -100,6 +103,9 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
const users = matchedUsers(item.value);
|
||||
const userNames = unionBy(users.map((user) => user.userName));
|
||||
const html = marked.parse(parseUserInfo(item.value));
|
||||
if (!item.value || !html) {
|
||||
return;
|
||||
}
|
||||
const params = {
|
||||
object_id: objectId,
|
||||
original_text: item.value,
|
||||
|
@ -164,9 +170,8 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
deleteComment(id).then(() => {
|
||||
if (pageIndex === 0) {
|
||||
mutate();
|
||||
} else {
|
||||
setComments(comments.filter((item) => item.comment_id !== id));
|
||||
}
|
||||
setComments(comments.filter((item) => item.comment_id !== id));
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -303,7 +308,9 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
variant="link"
|
||||
className="p-0 fs-14 btn-no-border"
|
||||
onClick={() => {
|
||||
setVisibleComment(!visibleComment);
|
||||
if (tryNormalLogged(true)) {
|
||||
setVisibleComment(!visibleComment);
|
||||
}
|
||||
}}>
|
||||
{t('btn_add_comment')}
|
||||
</Button>
|
||||
|
|
|
@ -251,7 +251,7 @@ const Image: FC<IEditorContext> = ({ editor }) => {
|
|||
|
||||
<Form.Group controlId="editor.imgDescription" className="mb-3">
|
||||
<Form.Label>
|
||||
{t('image.form_image.fields.description.label')}
|
||||
{t('image.form_image.fields.desc.label')}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
|
|
|
@ -7,28 +7,31 @@ import {
|
|||
useImperativeHandle,
|
||||
} from 'react';
|
||||
|
||||
import { marked } from 'marked';
|
||||
import { markdownToHtml } from '@/services';
|
||||
|
||||
import { htmlRender } from './utils';
|
||||
|
||||
let scrollTop = 0;
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
});
|
||||
let renderTimer;
|
||||
|
||||
const Index = ({ value }, ref) => {
|
||||
const [html, setHtml] = useState('');
|
||||
|
||||
const previewRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const renderMarkdown = (markdown) => {
|
||||
clearTimeout(renderTimer);
|
||||
const timeout = renderTimer ? 1000 : 0;
|
||||
renderTimer = setTimeout(() => {
|
||||
markdownToHtml(markdown).then((resp) => {
|
||||
scrollTop = previewRef.current?.scrollTop || 0;
|
||||
setHtml(resp);
|
||||
});
|
||||
}, timeout);
|
||||
};
|
||||
useEffect(() => {
|
||||
const previewHtml = marked(value).replace(
|
||||
/<img/gi,
|
||||
'<img referrerpolicy="no-referrer"',
|
||||
);
|
||||
scrollTop = previewRef.current?.scrollTop || 0;
|
||||
setHtml(previewHtml);
|
||||
renderMarkdown(value);
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!html) {
|
||||
return;
|
||||
|
|
|
@ -56,6 +56,7 @@ const ToolItem: FC<IProps> = (props) => {
|
|||
disable ? 'disabled' : ''
|
||||
} `}
|
||||
disabled={disable}
|
||||
tabIndex={-1}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (typeof onClick === 'function') {
|
||||
|
|
|
@ -5,12 +5,10 @@ import { Trans } from 'react-i18next';
|
|||
import dayjs from 'dayjs';
|
||||
|
||||
import { siteInfoStore } from '@/stores';
|
||||
import { DEFAULT_SITE_NAME } from '@/common/constants';
|
||||
|
||||
const Index = () => {
|
||||
const fullYear = dayjs().format('YYYY');
|
||||
const siteName =
|
||||
siteInfoStore((state) => state.siteInfo.name) || DEFAULT_SITE_NAME;
|
||||
const siteName = siteInfoStore((state) => state.siteInfo.name);
|
||||
const cc = `${fullYear} ${siteName}`;
|
||||
return (
|
||||
<footer className="bg-light py-3">
|
||||
|
|
|
@ -28,7 +28,6 @@ import {
|
|||
themeSettingStore,
|
||||
} from '@/stores';
|
||||
import { logout, useQueryNotificationStatus } from '@/services';
|
||||
import { DEFAULT_SITE_NAME } from '@/common/constants';
|
||||
|
||||
import NavItems from './components/NavItems';
|
||||
|
||||
|
@ -61,6 +60,7 @@ const Header: FC = () => {
|
|||
const handleLogout = async () => {
|
||||
await logout();
|
||||
clearUserStore();
|
||||
window.location.replace(window.location.href);
|
||||
};
|
||||
const onLoginClick = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
@ -121,7 +121,7 @@ const Header: FC = () => {
|
|||
/>
|
||||
</>
|
||||
) : (
|
||||
<span>{siteInfo.name || DEFAULT_SITE_NAME}</span>
|
||||
<span>{siteInfo.name}</span>
|
||||
)}
|
||||
</Navbar.Brand>
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react';
|
||||
import { Modal } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { loginToContinueStore, siteInfoStore } from '@/stores';
|
||||
|
||||
interface IProps {
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
const Index: React.FC<IProps> = ({ visible = false }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'login' });
|
||||
const { update: updateStore } = loginToContinueStore();
|
||||
const { siteInfo } = siteInfoStore((_) => _);
|
||||
const closeModal = () => {
|
||||
updateStore({ show: false });
|
||||
};
|
||||
const linkClick = (evt) => {
|
||||
evt.stopPropagation();
|
||||
closeModal();
|
||||
};
|
||||
return (
|
||||
<Modal
|
||||
title="LoginToContinue"
|
||||
show={visible}
|
||||
onHide={closeModal}
|
||||
centered
|
||||
fullscreen="sm-down">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title as="h5">{t('login_to_continue')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body className="p-5">
|
||||
<div className="d-flex flex-column align-items-center text-center text-body">
|
||||
<h3>{t('page_title', { site_name: siteInfo.name })}</h3>
|
||||
<p>{siteInfo.description}</p>
|
||||
</div>
|
||||
<div className="d-grid gap-2">
|
||||
<Link
|
||||
to="/users/login"
|
||||
className="btn btn-primary"
|
||||
onClick={linkClick}>
|
||||
{t('login', { keyPrefix: 'btns' })}
|
||||
</Link>
|
||||
<Link
|
||||
to="/users/register"
|
||||
className="btn btn-link"
|
||||
onClick={linkClick}>
|
||||
{t('signup', { keyPrefix: 'btns' })}
|
||||
</Link>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
export default Index;
|
|
@ -1,6 +1,7 @@
|
|||
import DefaultModal from './Modal';
|
||||
import confirm, { Config } from './Confirm';
|
||||
import PicAuthCodeModal from './PicAuthCodeModal';
|
||||
import LoginToContinueModal from './LoginToContinueModal';
|
||||
|
||||
type ModalType = typeof DefaultModal & {
|
||||
confirm: (config: Config) => void;
|
||||
|
@ -14,3 +15,4 @@ Modal.confirm = function (props: Config) {
|
|||
export default Modal;
|
||||
|
||||
export { PicAuthCodeModal };
|
||||
export { LoginToContinueModal };
|
||||
|
|
|
@ -41,10 +41,10 @@ const Index: FC<IProps> = ({
|
|||
const navigate = useNavigate();
|
||||
const reportModal = useReportModal();
|
||||
|
||||
const refershQuestion = () => {
|
||||
const refreshQuestion = () => {
|
||||
callback?.('default');
|
||||
};
|
||||
const closeModal = useReportModal(refershQuestion);
|
||||
const closeModal = useReportModal(refreshQuestion);
|
||||
const editUrl =
|
||||
type === 'answer' ? `/posts/${qid}/${aid}/edit` : `/posts/${qid}/edit`;
|
||||
|
||||
|
@ -97,7 +97,7 @@ const Index: FC<IProps> = ({
|
|||
deleteAnswer({
|
||||
id: aid,
|
||||
}).then(() => {
|
||||
// refersh page
|
||||
// refresh page
|
||||
toast.onShow({
|
||||
msg: t('tip_answer_deleted'),
|
||||
variant: 'success',
|
||||
|
@ -132,7 +132,7 @@ const Index: FC<IProps> = ({
|
|||
msg: t('success', { keyPrefix: 'question_detail.reopen' }),
|
||||
variant: 'success',
|
||||
});
|
||||
refershQuestion();
|
||||
refreshQuestion();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
.tag-selector-wrap {
|
||||
.btn {
|
||||
&.warning {
|
||||
-webkit-animation: tag-input-warning 2s;
|
||||
animation: tag-input-warning 2s;
|
||||
}
|
||||
}
|
||||
.dropdown-menu {
|
||||
min-width: 15rem;
|
||||
}
|
||||
|
@ -18,23 +12,5 @@
|
|||
color: #212529;
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
@-webkit-keyframes tag-input-warning {
|
||||
0% {
|
||||
background-color: #ffc107;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tag-input-warning {
|
||||
0% {
|
||||
background-color: #ffc107;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -188,7 +188,7 @@ const TagSelector: FC<IProps> = ({
|
|||
key={item.slug_name}
|
||||
className={classNames(
|
||||
'm-1 text-nowrap d-flex align-items-center',
|
||||
index === repeatIndex && 'warning',
|
||||
index === repeatIndex && 'bg-fade-out',
|
||||
)}
|
||||
variant={`outline-${
|
||||
item.reserved ? 'danger' : item.recommend ? 'dark' : 'secondary'
|
||||
|
|
|
@ -14,6 +14,9 @@ const usePageUsers = () => {
|
|||
getUsers,
|
||||
setUsers: (data: Types.PageUser | Types.PageUser[]) => {
|
||||
if (data instanceof Array) {
|
||||
if (data.length === 0) {
|
||||
return;
|
||||
}
|
||||
setUsers(uniqBy([...users, ...data], 'userName'));
|
||||
globalUsers = uniqBy([...globalUsers, ...data], 'userName');
|
||||
} else {
|
||||
|
|
|
@ -18,10 +18,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
|
@ -300,7 +296,7 @@ a {
|
|||
}
|
||||
}
|
||||
.bg-fade-out {
|
||||
animation: 2s ease 1s bg-fade-out;
|
||||
animation: bg-fade-out 2s ease 0.3s;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ const Index: FC = () => {
|
|||
'ui:widget': 'textarea',
|
||||
'ui:options': {
|
||||
rows: 10,
|
||||
className: 'font-monospace',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -18,7 +18,7 @@ const Index: FC<Props> = ({ visible, siteUrl = '' }) => {
|
|||
<p>
|
||||
<Trans i18nKey="install.ready_description">
|
||||
If you ever feel like changing more settings, visit
|
||||
<a href={`${siteUrl}/users/login`}>admin section</a>; find it in the
|
||||
<a href={`${siteUrl}/users/login`}> admin section</a>; find it in the
|
||||
site menu.
|
||||
</Trans>
|
||||
</p>
|
||||
|
|
|
@ -23,7 +23,7 @@ const Index: FC<Props> = ({ visible, errorMsg, nextCallback }) => {
|
|||
<div className="fmt">
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="install.config_yaml.description"
|
||||
i18nKey="install.config_yaml.desc"
|
||||
components={{ 1: <code /> }}
|
||||
/>
|
||||
</p>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { HelmetProvider } from 'react-helmet-async';
|
|||
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
import { toastStore } from '@/stores';
|
||||
import { toastStore, loginToContinueStore } from '@/stores';
|
||||
import {
|
||||
Header,
|
||||
Footer,
|
||||
|
@ -13,13 +13,14 @@ import {
|
|||
CustomizeTheme,
|
||||
PageTags,
|
||||
} from '@/components';
|
||||
import { LoginToContinueModal } from '@/components/Modal';
|
||||
|
||||
const Layout: FC = () => {
|
||||
const { msg: toastMsg, variant, clear: toastClear } = toastStore();
|
||||
const closeToast = () => {
|
||||
toastClear();
|
||||
};
|
||||
|
||||
const { show: showLoginToContinueModal } = loginToContinueStore();
|
||||
return (
|
||||
<HelmetProvider>
|
||||
<PageTags />
|
||||
|
@ -35,6 +36,7 @@ const Layout: FC = () => {
|
|||
<Toast msg={toastMsg} variant={variant} onClose={closeToast} />
|
||||
<Footer />
|
||||
<Customize />
|
||||
<LoginToContinueModal visible={showLoginToContinueModal} />
|
||||
</SWRConfig>
|
||||
</HelmetProvider>
|
||||
);
|
||||
|
|
|
@ -54,14 +54,18 @@ const Index: FC<Props> = ({
|
|||
if (!answerRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
htmlRender(answerRef.current.querySelector('.fmt'));
|
||||
|
||||
if (aid === data.id) {
|
||||
setTimeout(() => {
|
||||
const element = answerRef.current;
|
||||
scrollTop(element);
|
||||
if (!searchParams.get('commentId')) {
|
||||
bgFadeOut(answerRef.current);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
htmlRender(answerRef.current.querySelector('.fmt'));
|
||||
bgFadeOut(answerRef.current);
|
||||
}, [data.id, answerRef.current]);
|
||||
if (!data?.id) {
|
||||
return null;
|
||||
|
|
|
@ -12,10 +12,54 @@ const Index: FC = () => {
|
|||
style={{ height: '24px' }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="placeholder w-100 align-top"
|
||||
style={{ height: '450px', marginBottom: '2rem' }}
|
||||
/>
|
||||
<div style={{ marginBottom: '2rem' }}>
|
||||
<p>
|
||||
<span
|
||||
className="placeholder w-100 d-block align-top mb-1"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
<span
|
||||
className="placeholder w-100 d-block align-top mb-1"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
<span
|
||||
className="placeholder w-100 d-block align-top mb-1"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
|
||||
<span
|
||||
className="placeholder w-100 d-block align-top mb-1"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
<span
|
||||
className="placeholder w-75 d-block align-top"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<span
|
||||
className="placeholder w-100 d-block align-top mb-1"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
<span
|
||||
className="placeholder w-100 d-block align-top mb-1"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
<span
|
||||
className="placeholder w-100 d-block align-top mb-1"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
<span
|
||||
className="placeholder w-100 d-block align-top mb-1"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
<span
|
||||
className="placeholder w-50 d-block align-top"
|
||||
style={{ height: '24px' }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="d-flex">
|
||||
<div
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
FormatTime,
|
||||
htmlRender,
|
||||
} from '@/components';
|
||||
import { formatCount } from '@/utils';
|
||||
import { formatCount, guard } from '@/utils';
|
||||
import { following } from '@/services';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
|
||||
|
@ -33,6 +33,9 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
|
|||
|
||||
const handleFollow = (e) => {
|
||||
e.preventDefault();
|
||||
if (!guard.tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
following({
|
||||
object_id: data?.id,
|
||||
is_cancel: followed,
|
||||
|
|
|
@ -8,6 +8,7 @@ import classNames from 'classnames';
|
|||
import { Editor, Modal, TextArea } from '@/components';
|
||||
import { FormDataType } from '@/common/interface';
|
||||
import { postAnswer } from '@/services';
|
||||
import { guard } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
visible?: boolean;
|
||||
|
@ -31,9 +32,13 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
},
|
||||
});
|
||||
const [showEditor, setShowEditor] = useState<boolean>(visible);
|
||||
const [focusType, setForceType] = useState('');
|
||||
const [focusType, setFocusType] = useState('');
|
||||
const [editorFocusState, setEditorFocusState] = useState(false);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!guard.tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
if (!formData.content.value) {
|
||||
setFormData({
|
||||
content: {
|
||||
|
@ -62,6 +67,9 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
};
|
||||
|
||||
const clickBtn = () => {
|
||||
if (!guard.tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
if (data?.answered && !showEditor) {
|
||||
Modal.confirm({
|
||||
title: t('confirm_title'),
|
||||
|
@ -81,9 +89,14 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
|
||||
handleSubmit();
|
||||
};
|
||||
const handleFocusForTextArea = () => {
|
||||
setForceType('answer');
|
||||
const handleFocusForTextArea = (evt) => {
|
||||
if (!guard.tryNormalLogged(true)) {
|
||||
evt.currentTarget.blur();
|
||||
return;
|
||||
}
|
||||
setFocusType('answer');
|
||||
setShowEditor(true);
|
||||
setEditorFocusState(true);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -114,7 +127,7 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
focusType === 'answer' && 'focus',
|
||||
)}
|
||||
value={formData.content.value}
|
||||
autoFocus
|
||||
autoFocus={editorFocusState}
|
||||
onChange={(val) => {
|
||||
setFormData({
|
||||
content: {
|
||||
|
@ -125,10 +138,10 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
});
|
||||
}}
|
||||
onFocus={() => {
|
||||
setForceType('answer');
|
||||
setFocusType('answer');
|
||||
}}
|
||||
onBlur={() => {
|
||||
setForceType('');
|
||||
setFocusType('');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import Pattern from '@/common/pattern';
|
||||
import { Pagination } from '@/components';
|
||||
import { loggedUserInfoStore, toastStore } from '@/stores';
|
||||
import { scrollTop, bgFadeOut } from '@/utils';
|
||||
import { scrollTop } from '@/utils';
|
||||
import { usePageTags, usePageUsers } from '@/hooks';
|
||||
import type {
|
||||
ListResult,
|
||||
|
@ -81,7 +81,6 @@ const Index = () => {
|
|||
// scroll into view;
|
||||
const element = document.getElementById('answerHeader');
|
||||
scrollTop(element);
|
||||
bgFadeOut(element);
|
||||
}
|
||||
|
||||
res.list.forEach((item) => {
|
||||
|
@ -105,9 +104,24 @@ const Index = () => {
|
|||
const res = await questionDetail(qid);
|
||||
if (res) {
|
||||
setUsers([
|
||||
res.user_info,
|
||||
res?.update_user_info,
|
||||
res?.last_answered_user_info,
|
||||
{
|
||||
id: res.user_info.id,
|
||||
displayName: res.user_info.display_name,
|
||||
userName: res.user_info.username,
|
||||
avatar_url: res.user_info.avatar,
|
||||
},
|
||||
{
|
||||
id: res?.update_user_info?.id,
|
||||
displayName: res?.update_user_info?.display_name,
|
||||
userName: res?.update_user_info?.username,
|
||||
avatar_url: res?.update_user_info?.avatar,
|
||||
},
|
||||
{
|
||||
id: res?.last_answered_user_info?.id,
|
||||
displayName: res?.last_answered_user_info?.display_name,
|
||||
userName: res?.last_answered_user_info?.username,
|
||||
avatar_url: res?.last_answered_user_info?.avatar,
|
||||
},
|
||||
]);
|
||||
setQuestion(res);
|
||||
}
|
||||
|
@ -124,7 +138,6 @@ const Index = () => {
|
|||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'default') {
|
||||
window.scrollTo(0, 0);
|
||||
getDetail();
|
||||
|
@ -151,6 +164,7 @@ const Index = () => {
|
|||
if (!qid) {
|
||||
return;
|
||||
}
|
||||
window.scrollTo(0, 0);
|
||||
getDetail();
|
||||
requestAnswers();
|
||||
}, [qid]);
|
||||
|
@ -214,7 +228,6 @@ const Index = () => {
|
|||
|
||||
{!isLoading && !question?.operation?.operation_type && (
|
||||
<WriteAnswer
|
||||
visible={answers.count === 0}
|
||||
data={{
|
||||
qid,
|
||||
answered: question?.answered,
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { FC } from 'react';
|
||||
import { Container, Row, Col } from 'react-bootstrap';
|
||||
import { useMatch } from 'react-router-dom';
|
||||
import { useMatch, Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { FollowingTags } from '@/components';
|
||||
import QuestionList from '@/components/QuestionList';
|
||||
import HotQuestions from '@/components/HotQuestions';
|
||||
import { siteInfoStore } from '@/stores';
|
||||
import { siteInfoStore, loggedUserInfoStore } from '@/stores';
|
||||
|
||||
const Questions: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||
|
||||
const { user: loggedUser } = loggedUserInfoStore((_) => _);
|
||||
const isIndexPage = useMatch('/');
|
||||
let pageTitle = t('questions', { keyPrefix: 'page_title' });
|
||||
let slogan = '';
|
||||
|
@ -29,7 +29,26 @@ const Questions: FC = () => {
|
|||
<QuestionList source="questions" />
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
<FollowingTags />
|
||||
{!loggedUser.access_token && (
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">
|
||||
{t('page_title', {
|
||||
keyPrefix: 'login',
|
||||
site_name: siteInfo.name,
|
||||
})}
|
||||
</h5>
|
||||
<p className="card-text">{siteInfo.description}</p>
|
||||
<Link to="/users/login" className="btn btn-primary">
|
||||
{t('login', { keyPrefix: 'btns' })}
|
||||
</Link>
|
||||
<Link to="/users/register" className="btn btn-link ms-2">
|
||||
{t('signup', { keyPrefix: 'btns' })}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{loggedUser.access_token && <FollowingTags />}
|
||||
<HotQuestions />
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -9,7 +9,7 @@ import { FollowingTags } from '@/components';
|
|||
import { useTagInfo, useFollow, useQuerySynonymsTags } from '@/services';
|
||||
import QuestionList from '@/components/QuestionList';
|
||||
import HotQuestions from '@/components/HotQuestions';
|
||||
import { escapeRemove } from '@/utils';
|
||||
import { escapeRemove, guard } from '@/utils';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
|
||||
const Questions: FC = () => {
|
||||
|
@ -23,6 +23,9 @@ const Questions: FC = () => {
|
|||
const { data: followResp } = useFollow(tagFollow);
|
||||
const { data: synonymsRes } = useQuerySynonymsTags(tagInfo?.tag_id);
|
||||
const toggleFollow = () => {
|
||||
if (!guard.tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
setTagFollow({
|
||||
is_cancel: tagInfo.is_follower,
|
||||
object_id: tagInfo.tag_id,
|
||||
|
|
|
@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import { usePageTags } from '@/hooks';
|
||||
import { Tag, Pagination, QueryGroup, TagsLoader } from '@/components';
|
||||
import { formatCount } from '@/utils';
|
||||
import { tryNormalLogged } from '@/utils/guard';
|
||||
import { useQueryTags, following } from '@/services';
|
||||
|
||||
const sortBtns = ['popular', 'name', 'newest'];
|
||||
|
@ -35,6 +36,9 @@ const Tags = () => {
|
|||
};
|
||||
|
||||
const handleFollow = (tag) => {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
following({
|
||||
object_id: tag.tag_id,
|
||||
is_cancel: tag.is_follower,
|
||||
|
|
|
@ -13,7 +13,11 @@ import type {
|
|||
} from '@/common/interface';
|
||||
import { Unactivate } from '@/components';
|
||||
import { PluginOauth } from '@/plugins';
|
||||
import { loggedUserInfoStore, loginSettingStore } from '@/stores';
|
||||
import {
|
||||
loggedUserInfoStore,
|
||||
loginSettingStore,
|
||||
siteInfoStore,
|
||||
} from '@/stores';
|
||||
import { guard, floppyNavigation, handleFormError } from '@/utils';
|
||||
import { login, checkImgCode } from '@/services';
|
||||
import { PicAuthCodeModal } from '@/components/Modal';
|
||||
|
@ -24,8 +28,8 @@ const Index: React.FC = () => {
|
|||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [refresh, setRefresh] = useState(0);
|
||||
const updateUser = loggedUserInfoStore((state) => state.update);
|
||||
const storeUser = loggedUserInfoStore((state) => state.user);
|
||||
const { name: siteName } = siteInfoStore((_) => _.siteInfo);
|
||||
const { user: storeUser, update: updateUser } = loggedUserInfoStore((_) => _);
|
||||
const loginSetting = loginSettingStore((state) => state.login);
|
||||
const [formData, setFormData] = useState<FormDataType>({
|
||||
e_mail: {
|
||||
|
@ -171,7 +175,9 @@ const Index: React.FC = () => {
|
|||
});
|
||||
return (
|
||||
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
||||
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
||||
<h3 className="text-center mb-5">
|
||||
{t('page_title', { site_name: siteName })}
|
||||
</h3>
|
||||
{step === 1 && (
|
||||
<Col className="mx-auto" md={3}>
|
||||
<PluginOauth className="mb-5" />
|
||||
|
|
|
@ -4,13 +4,14 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { Unactivate } from '@/components';
|
||||
import { siteInfoStore } from '@/stores';
|
||||
|
||||
import SignUpForm from './components/SignUpForm';
|
||||
|
||||
const Index: React.FC = () => {
|
||||
const [showForm, setShowForm] = useState(true);
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'login' });
|
||||
|
||||
const { name: siteName } = siteInfoStore((_) => _.siteInfo);
|
||||
const onStep = () => {
|
||||
setShowForm((bol) => !bol);
|
||||
};
|
||||
|
@ -19,7 +20,9 @@ const Index: React.FC = () => {
|
|||
});
|
||||
return (
|
||||
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
||||
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
||||
<h3 className="text-center mb-5">
|
||||
{t('page_title', { site_name: siteName })}
|
||||
</h3>
|
||||
{showForm ? (
|
||||
<SignUpForm callback={onStep} />
|
||||
) : (
|
||||
|
|
|
@ -3,7 +3,6 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
|||
|
||||
import { floppyNavigation } from '@/utils';
|
||||
import { TGuardFunc } from '@/utils/guard';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
|
||||
const Index: FC<{
|
||||
children: ReactNode;
|
||||
|
@ -17,8 +16,7 @@ const Index: FC<{
|
|||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { user } = loggedUserInfoStore();
|
||||
const runGuards = () => {
|
||||
const callGuards = () => {
|
||||
if (onEnter) {
|
||||
const gr = onEnter();
|
||||
const redirectUrl = gr.redirect;
|
||||
|
@ -30,13 +28,8 @@ const Index: FC<{
|
|||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
runGuards();
|
||||
callGuards();
|
||||
}, [location]);
|
||||
useEffect(() => {
|
||||
if (!user.access_token) {
|
||||
runGuards();
|
||||
}
|
||||
}, [user]);
|
||||
return (
|
||||
<>
|
||||
{/* Route Guard */}
|
||||
|
|
|
@ -6,14 +6,12 @@ const tagLanding = (slugName: string) => {
|
|||
if (!slugName) {
|
||||
return '/tags';
|
||||
}
|
||||
slugName = slugName.toLowerCase();
|
||||
return urlcat('/tags/:slugName', { slugName });
|
||||
};
|
||||
const tagInfo = (slugName: string) => {
|
||||
if (!slugName) {
|
||||
return '/tags';
|
||||
}
|
||||
slugName = slugName.toLowerCase();
|
||||
return urlcat('/tags/:slugName/info', { slugName });
|
||||
};
|
||||
const tagEdit = (tagId: string) => {
|
||||
|
|
|
@ -268,3 +268,8 @@ export const unsubscribe = (code: string) => {
|
|||
const apiUrl = '/answer/api/v1/user/email/notification';
|
||||
return request.put(apiUrl, { code });
|
||||
};
|
||||
|
||||
export const markdownToHtml = (content: string) => {
|
||||
const apiUrl = '/answer/api/v1/post/render';
|
||||
return request.post(apiUrl, { content });
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ import brandingStore from './branding';
|
|||
import pageTagStore from './pageTags';
|
||||
import customizeStore from './customize';
|
||||
import themeSettingStore from './themeSetting';
|
||||
import loginToContinueStore from './loginToContinue';
|
||||
|
||||
export {
|
||||
toastStore,
|
||||
|
@ -21,4 +22,5 @@ export {
|
|||
customizeStore,
|
||||
themeSettingStore,
|
||||
seoSettingStore,
|
||||
loginToContinueStore,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import create from 'zustand';
|
||||
|
||||
interface IProps {
|
||||
show: boolean;
|
||||
update: (params: { show: boolean }) => void;
|
||||
}
|
||||
|
||||
const loginToContinueStore = create<IProps>((set) => ({
|
||||
show: false,
|
||||
update: (params) =>
|
||||
set({
|
||||
...params,
|
||||
}),
|
||||
}));
|
||||
|
||||
export default loginToContinueStore;
|
|
@ -1,6 +1,7 @@
|
|||
import create from 'zustand';
|
||||
|
||||
import { AdminSettingsGeneral } from '@/common/interface';
|
||||
import { DEFAULT_SITE_NAME } from '@/common/constants';
|
||||
|
||||
interface SiteInfoType {
|
||||
siteInfo: AdminSettingsGeneral;
|
||||
|
@ -9,7 +10,7 @@ interface SiteInfoType {
|
|||
|
||||
const siteInfo = create<SiteInfoType>((set) => ({
|
||||
siteInfo: {
|
||||
name: '',
|
||||
name: DEFAULT_SITE_NAME,
|
||||
description: '',
|
||||
short_description: '',
|
||||
site_url: '',
|
||||
|
@ -19,6 +20,9 @@ const siteInfo = create<SiteInfoType>((set) => ({
|
|||
update: (params) =>
|
||||
set((_) => {
|
||||
const o = { ..._.siteInfo, ...params };
|
||||
if (!o.name) {
|
||||
o.name = DEFAULT_SITE_NAME;
|
||||
}
|
||||
return {
|
||||
siteInfo: o,
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
customizeStore,
|
||||
themeSettingStore,
|
||||
seoSettingStore,
|
||||
loginToContinueStore,
|
||||
} from '@/stores';
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
import Storage from '@/utils/storage';
|
||||
|
@ -225,7 +226,7 @@ export const tryNormalLogged = (canNavigate: boolean = false) => {
|
|||
// must assert logged state first and return
|
||||
if (!us.isLogged) {
|
||||
if (canNavigate) {
|
||||
floppyNavigation.navigateToLogin();
|
||||
loginToContinueStore.getState().update({ show: true });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue