mirror of https://gitee.com/answerdev/answer.git
Merge branch 'feat/ui-v0.4' of git.backyard.segmentfault.com:opensource/answer into feat/ui-v0.4
This commit is contained in:
commit
c73723c23d
|
@ -934,7 +934,7 @@ ui:
|
|||
smtp: SMTP
|
||||
branding: Branding
|
||||
legal: Legal
|
||||
labels: Labels
|
||||
write: Write
|
||||
dashboard:
|
||||
title: Dashboard
|
||||
welcome: Welcome to Answer Admin!
|
||||
|
@ -1135,32 +1135,21 @@ ui:
|
|||
page_title: Legal
|
||||
terms_of_service:
|
||||
label: Terms of Service
|
||||
msg: Terms of service cannot be empty.
|
||||
text: "You can add terms of service content here. If you already have a document hosted elsewhere, provide the full URL here."
|
||||
privacy_policy:
|
||||
label: Privacy Policy
|
||||
msg: Privacy policy cannot be empty.
|
||||
text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here."
|
||||
labels:
|
||||
title: Labels
|
||||
new_label: New Label
|
||||
name: Name
|
||||
color: color
|
||||
description: Description
|
||||
action: Action
|
||||
form:
|
||||
title: Create New Label
|
||||
back: Back
|
||||
display_name:
|
||||
label: Display Name
|
||||
url_slug:
|
||||
label: URL Slug
|
||||
text: Must use the character set “a-z”, “0-9”, “+ # - .”
|
||||
description:
|
||||
label: Description (optional)
|
||||
color:
|
||||
label: Color
|
||||
|
||||
write:
|
||||
page_title: Write
|
||||
recommend_tags:
|
||||
label: Recommend Tags
|
||||
text: "Please input tag slug above, one tag per line."
|
||||
required_tag:
|
||||
label: Required Tag
|
||||
text: "Every new question must have at least one recommend tag"
|
||||
reserved_tags:
|
||||
label: Reserved Tags
|
||||
text: "Reserved tags can only be added to a post by moderator."
|
||||
form:
|
||||
empty: cannot be empty
|
||||
invalid: is invalid
|
||||
|
|
|
@ -43,7 +43,7 @@ export const ADMIN_NAV_MENUS = [
|
|||
},
|
||||
{
|
||||
name: 'contents',
|
||||
child: [{ name: 'questions' }, { name: 'answers' }, { name: 'labels' }],
|
||||
child: [{ name: 'questions' }, { name: 'answers' }],
|
||||
},
|
||||
{
|
||||
name: 'users',
|
||||
|
|
|
@ -126,7 +126,8 @@ export interface UserInfoRes extends UserInfoBase {
|
|||
[prop: string]: any;
|
||||
}
|
||||
|
||||
export interface AvatarUploadReq {
|
||||
export type UploadType = 'post' | 'avatar' | 'branding';
|
||||
export interface UploadReq {
|
||||
file: FormData;
|
||||
}
|
||||
|
||||
|
@ -296,7 +297,7 @@ export interface AdminSettingsLegal {
|
|||
|
||||
export interface AdminSettingsWrite {
|
||||
recommend_tags: string;
|
||||
required_tags: string;
|
||||
required_tag: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -359,3 +360,10 @@ export interface AdminDashboard {
|
|||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface BrandReqParams {
|
||||
logo: string;
|
||||
square_icon: string;
|
||||
mobile_logo?: string;
|
||||
favicon?: string;
|
||||
}
|
||||
|
|
|
@ -2,22 +2,17 @@ import { FC } from 'react';
|
|||
import { ButtonGroup, Button } from 'react-bootstrap';
|
||||
|
||||
import { Icon, UploadImg } from '@/components';
|
||||
import { uploadAvatar } from '@/services';
|
||||
import { UploadType } from '@/common/interface';
|
||||
|
||||
interface Props {
|
||||
type: 'logo' | 'avatar' | 'mobile_logo' | 'square_icon' | 'favicon';
|
||||
type: UploadType;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const Index: FC<Props> = ({ type = 'logo', value, onChange }) => {
|
||||
const onUpload = (file: any) => {
|
||||
return new Promise((resolve) => {
|
||||
uploadAvatar(file).then((res) => {
|
||||
onChange(res);
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
const Index: FC<Props> = ({ type = 'post', value, onChange }) => {
|
||||
const onUpload = (imgPath: string) => {
|
||||
onChange(imgPath);
|
||||
};
|
||||
|
||||
const onRemove = () => {
|
||||
|
@ -29,7 +24,7 @@ const Index: FC<Props> = ({ type = 'logo', value, onChange }) => {
|
|||
<img src={value} alt="" height={100} />
|
||||
</div>
|
||||
<ButtonGroup vertical className="fit-content">
|
||||
<UploadImg type={type} upload={onUpload} className="mb-0">
|
||||
<UploadImg type={type} uploadCallback={onUpload} className="mb-0">
|
||||
<Icon name="cloud-upload" />
|
||||
</UploadImg>
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ const Image: FC<IEditorContext> = ({ editor }) => {
|
|||
files: FileList,
|
||||
): Promise<{ url: string; name: string }[]> => {
|
||||
const promises = Array.from(files).map(async (file) => {
|
||||
const url = await uploadImage(file);
|
||||
const url = await uploadImage({ file, type: 'post' });
|
||||
|
||||
return {
|
||||
name: file.name,
|
||||
|
@ -209,7 +209,7 @@ const Image: FC<IEditorContext> = ({ editor }) => {
|
|||
return;
|
||||
}
|
||||
|
||||
uploadImage(e.target.files[0]).then((url) => {
|
||||
uploadImage({ file: e.target.files[0], type: 'post' }).then((url) => {
|
||||
setLink({ ...link, value: url });
|
||||
});
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { TagSelector, Tag } from '@/components';
|
||||
import { tryLoggedAndActicevated } from '@/utils/guard';
|
||||
import { tryLoggedAndActivated } from '@/utils/guard';
|
||||
import { useFollowingTags, followTags } from '@/services';
|
||||
|
||||
const Index: FC = () => {
|
||||
|
@ -32,7 +32,7 @@ const Index: FC = () => {
|
|||
});
|
||||
};
|
||||
|
||||
if (!tryLoggedAndActicevated().ok) {
|
||||
if (!tryLoggedAndActivated().ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -73,11 +73,7 @@ const Index: FC = () => {
|
|||
<>
|
||||
{followingTags.map((item) => {
|
||||
const slugName = item?.slug_name;
|
||||
return (
|
||||
<Tag key={slugName} className="m-1" href={`/tags/${slugName}`}>
|
||||
{slugName}
|
||||
</Tag>
|
||||
);
|
||||
return <Tag key={slugName} className="m-1" data={item} />;
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
|
|
|
@ -158,14 +158,7 @@ const QuestionList: FC<Props> = ({ source }) => {
|
|||
{Array.isArray(li.tags)
|
||||
? li.tags.map((tag) => {
|
||||
return (
|
||||
<Tag
|
||||
key={tag.slug_name}
|
||||
className="m-1"
|
||||
href={`/tags/${
|
||||
tag.main_tag_slug_name || tag.slug_name
|
||||
}`}>
|
||||
{tag.slug_name}
|
||||
</Tag>
|
||||
<Tag key={tag.slug_name} className="m-1" data={tag} />
|
||||
);
|
||||
})
|
||||
: null}
|
||||
|
|
|
@ -54,7 +54,7 @@ export interface UISchema {
|
|||
invalid?: string;
|
||||
validator?: (value) => boolean;
|
||||
textRender?: () => React.ReactElement;
|
||||
imageType?: 'avatar' | 'logo' | 'mobile_logo' | 'square_icon' | 'favicon';
|
||||
imageType?: Type.UploadType;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -225,6 +225,9 @@ const SchemaForm: FC<IProps> = ({
|
|||
);
|
||||
})}
|
||||
</Stack>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData[key]?.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
<Form.Text className="text-muted">{description}</Form.Text>
|
||||
</Form.Group>
|
||||
);
|
||||
|
@ -243,6 +246,9 @@ const SchemaForm: FC<IProps> = ({
|
|||
feedbackType="invalid"
|
||||
isInvalid={formData[key].isInvalid}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData[key]?.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
<Form.Text className="text-muted">{description}</Form.Text>
|
||||
</Form.Group>
|
||||
);
|
||||
|
@ -255,6 +261,14 @@ const SchemaForm: FC<IProps> = ({
|
|||
value={formData[key]?.value}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Control
|
||||
name={key}
|
||||
className="d-none"
|
||||
isInvalid={formData[key].isInvalid}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData[key]?.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
<Form.Text className="text-muted">{description}</Form.Text>
|
||||
</Form.Group>
|
||||
);
|
||||
|
@ -269,6 +283,14 @@ const SchemaForm: FC<IProps> = ({
|
|||
value={formData[key]?.value}
|
||||
onChange={(value) => handleUploadChange(key, value)}
|
||||
/>
|
||||
<Form.Control
|
||||
name={key}
|
||||
className="d-none"
|
||||
isInvalid={formData[key].isInvalid}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData[key]?.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
<Form.Text className="text-muted">{description}</Form.Text>
|
||||
</Form.Group>
|
||||
);
|
||||
|
|
|
@ -2,17 +2,20 @@ import React, { memo, FC } from 'react';
|
|||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Tag } from '@/common/interface';
|
||||
|
||||
interface IProps {
|
||||
data: Tag;
|
||||
href?: string;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
href: string;
|
||||
}
|
||||
|
||||
const Index: FC<IProps> = ({ className = '', children, href }) => {
|
||||
href = href.toLowerCase();
|
||||
const Index: FC<IProps> = ({ className = '', href, data }) => {
|
||||
href =
|
||||
href || `/tags/${data.main_tag_slug_name || data.slug_name}`.toLowerCase();
|
||||
return (
|
||||
<a href={href} className={classNames('badge-tag rounded-1', className)}>
|
||||
{children}
|
||||
{data.slug_name}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,14 +1,22 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { uploadImage } from '@/services';
|
||||
import * as Type from '@/common/interface';
|
||||
|
||||
interface IProps {
|
||||
type: string;
|
||||
type: Type.UploadType;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
upload: (data: FormData) => Promise<any>;
|
||||
uploadCallback: (img: string) => void;
|
||||
}
|
||||
|
||||
const Index: React.FC<IProps> = ({ type, upload, children, className }) => {
|
||||
const Index: React.FC<IProps> = ({
|
||||
type,
|
||||
uploadCallback,
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [status, setStatus] = useState(false);
|
||||
|
||||
|
@ -26,13 +34,13 @@ const Index: React.FC<IProps> = ({ type, upload, children, className }) => {
|
|||
// return;
|
||||
// }
|
||||
setStatus(true);
|
||||
const data = new FormData();
|
||||
|
||||
data.append('file', e.target.files[0]);
|
||||
// do
|
||||
upload(data).finally(() => {
|
||||
setStatus(false);
|
||||
});
|
||||
uploadImage({ file: e.target.files[0], type })
|
||||
.then((res) => {
|
||||
uploadCallback(res);
|
||||
})
|
||||
.finally(() => {
|
||||
setStatus(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -44,7 +52,6 @@ const Index: React.FC<IProps> = ({ type, upload, children, className }) => {
|
|||
className="d-none"
|
||||
accept="image/jpeg,image/jpg,image/png,image/webp"
|
||||
onChange={onChange}
|
||||
id={type}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
|
|
|
@ -74,6 +74,15 @@ a {
|
|||
}
|
||||
}
|
||||
|
||||
.badge-tag-required {
|
||||
background: rgba($gray-400, 0.5);
|
||||
color: $gray-700;
|
||||
&:hover {
|
||||
color: $gray-700;
|
||||
background: rgba($gray-400, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.divide-line {
|
||||
border-bottom: 1px solid rgba(33, 37, 41, 0.25);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,23 @@
|
|||
import { FC, memo, useState } from 'react';
|
||||
import { FC, memo, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { JSONSchema, SchemaForm, UISchema } from '@/components';
|
||||
import { FormDataType } from '@/common/interface';
|
||||
import { brandSetting, getBrandSetting } from '@/services';
|
||||
import { interfaceStore } from '@/stores';
|
||||
import { useToast } from '@/hooks';
|
||||
|
||||
const uploadType = 'branding';
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'admin.branding',
|
||||
});
|
||||
const { interface: storeInterface, updateLogo } = interfaceStore();
|
||||
const Toast = useToast();
|
||||
|
||||
const [formData, setFormData] = useState<FormDataType>({
|
||||
logo: {
|
||||
value: '',
|
||||
value: storeInterface.logo,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
|
@ -32,24 +38,6 @@ const Index: FC = () => {
|
|||
},
|
||||
});
|
||||
|
||||
// const onChange = (fieldName, fieldValue) => {
|
||||
// if (!formData[fieldName]) {
|
||||
// return;
|
||||
// }
|
||||
// const fieldData: FormDataType = {
|
||||
// [fieldName]: {
|
||||
// value: fieldValue,
|
||||
// isInvalid: false,
|
||||
// errorMsg: '',
|
||||
// },
|
||||
// };
|
||||
// setFormData({ ...formData, ...fieldData });
|
||||
// };
|
||||
|
||||
// const [img, setImg] = useState(
|
||||
// 'https://image-static.segmentfault.com/405/057/4050570037-636c7b0609a49',
|
||||
// );
|
||||
|
||||
const schema: JSONSchema = {
|
||||
title: t('page_title'),
|
||||
properties: {
|
||||
|
@ -80,25 +68,25 @@ const Index: FC = () => {
|
|||
logo: {
|
||||
'ui:widget': 'upload',
|
||||
'ui:options': {
|
||||
imageType: 'logo',
|
||||
imageType: uploadType,
|
||||
},
|
||||
},
|
||||
mobile_logo: {
|
||||
'ui:widget': 'upload',
|
||||
'ui:options': {
|
||||
imageType: 'mobile_logo',
|
||||
imageType: uploadType,
|
||||
},
|
||||
},
|
||||
square_icon: {
|
||||
'ui:widget': 'upload',
|
||||
'ui:options': {
|
||||
imageType: 'square_icon',
|
||||
imageType: uploadType,
|
||||
},
|
||||
},
|
||||
favicon: {
|
||||
'ui:widget': 'upload',
|
||||
'ui:options': {
|
||||
imageType: 'favicon',
|
||||
imageType: uploadType,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -108,9 +96,46 @@ const Index: FC = () => {
|
|||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
// undo
|
||||
const params = {
|
||||
logo: formData.logo.value,
|
||||
mobile_logo: formData.mobile_logo.value,
|
||||
square_icon: formData.square_icon.value,
|
||||
favicon: formData.favicon.value,
|
||||
};
|
||||
brandSetting(params)
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
updateLogo(formData.logo.value);
|
||||
Toast.onShow({
|
||||
msg: t('update', { keyPrefix: 'toast' }),
|
||||
variant: 'success',
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if (err.key) {
|
||||
formData[err.key].isInvalid = true;
|
||||
formData[err.key].errorMsg = err.value;
|
||||
setFormData({ ...formData });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getBrandData = async () => {
|
||||
const res = await getBrandSetting();
|
||||
if (res) {
|
||||
formData.logo.value = res.logo;
|
||||
formData.mobile_logo.value = res.mobile_logo;
|
||||
formData.square_icon.value = res.square_icon;
|
||||
formData.favicon.value = res.favicon;
|
||||
setFormData({ ...formData });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getBrandData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="mb-4">{t('page_title')}</h3>
|
||||
|
|
|
@ -1,121 +0,0 @@
|
|||
import React, { FC, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'react-bootstrap';
|
||||
|
||||
import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
|
||||
import type * as Type from '@/common/interface';
|
||||
import { useToast } from '@/hooks';
|
||||
import { siteInfoStore } from '@/stores';
|
||||
import { useGeneralSetting, updateGeneralSetting } from '@/services';
|
||||
|
||||
interface IProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
const LabelForm: FC<IProps> = ({ onClose }) => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'admin.labels.form',
|
||||
});
|
||||
const Toast = useToast();
|
||||
const updateSiteInfo = siteInfoStore((state) => state.update);
|
||||
|
||||
const { data: setting } = useGeneralSetting();
|
||||
const schema: JSONSchema = {
|
||||
title: t('title'),
|
||||
required: ['name', 'site_url', 'contact_email'],
|
||||
properties: {
|
||||
display_name: {
|
||||
type: 'string',
|
||||
title: t('display_name.label'),
|
||||
},
|
||||
url_slug: {
|
||||
type: 'string',
|
||||
title: t('url_slug.label'),
|
||||
description: t('url_slug.text'),
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
title: t('description.label'),
|
||||
description: t('description.text'),
|
||||
},
|
||||
color: {
|
||||
type: 'string',
|
||||
title: t('color.label'),
|
||||
},
|
||||
},
|
||||
};
|
||||
const uiSchema: UISchema = {
|
||||
color: {
|
||||
'ui:options': {
|
||||
type: 'color',
|
||||
},
|
||||
},
|
||||
};
|
||||
const [formData, setFormData] = useState(initFormData(schema));
|
||||
|
||||
const onSubmit = (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
const reqParams: Type.AdminSettingsGeneral = {
|
||||
name: formData.name.value,
|
||||
description: formData.description.value,
|
||||
short_description: formData.short_description.value,
|
||||
site_url: formData.site_url.value,
|
||||
contact_email: formData.contact_email.value,
|
||||
};
|
||||
|
||||
updateGeneralSetting(reqParams)
|
||||
.then(() => {
|
||||
Toast.onShow({
|
||||
msg: t('update', { keyPrefix: 'toast' }),
|
||||
variant: 'success',
|
||||
});
|
||||
onClose();
|
||||
updateSiteInfo(reqParams);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.isError && err.key) {
|
||||
formData[err.key].isInvalid = true;
|
||||
formData[err.key].errorMsg = err.value;
|
||||
}
|
||||
setFormData({ ...formData });
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!setting) {
|
||||
return;
|
||||
}
|
||||
const formMeta = {};
|
||||
Object.keys(setting).forEach((k) => {
|
||||
formMeta[k] = { ...formData[k], value: setting[k] };
|
||||
});
|
||||
setFormData({ ...formData, ...formMeta });
|
||||
}, [setting]);
|
||||
|
||||
const handleOnChange = (data) => {
|
||||
setFormData(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
className="mb-4"
|
||||
variant="outline-secondary"
|
||||
onClick={onClose}>
|
||||
← {t('back')}
|
||||
</Button>
|
||||
<h3 className="mb-4">{t('title')}</h3>
|
||||
<SchemaForm
|
||||
schema={schema}
|
||||
formData={formData}
|
||||
onSubmit={onSubmit}
|
||||
uiSchema={uiSchema}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LabelForm;
|
|
@ -1,124 +0,0 @@
|
|||
import { FC, useState } from 'react';
|
||||
import { Button, Table } from 'react-bootstrap';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Pagination, BaseUserCard, Empty } from '@/components';
|
||||
import * as Type from '@/common/interface';
|
||||
import { useChangeModal } from '@/hooks';
|
||||
import { useQueryUsers } from '@/services';
|
||||
|
||||
import CreateForm from './Form';
|
||||
|
||||
import '../index.scss';
|
||||
|
||||
const UserFilterKeys: Type.UserFilterBy[] = [
|
||||
'all',
|
||||
'inactive',
|
||||
'suspended',
|
||||
'deleted',
|
||||
];
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
const Users: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.labels' });
|
||||
|
||||
const [urlSearchParams] = useSearchParams();
|
||||
const curFilter = urlSearchParams.get('filter') || UserFilterKeys[0];
|
||||
const curPage = Number(urlSearchParams.get('page') || '1');
|
||||
const curQuery = urlSearchParams.get('query') || '';
|
||||
|
||||
const [isCreate, setCreateState] = useState(true);
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
mutate: refreshUsers,
|
||||
} = useQueryUsers({
|
||||
page: curPage,
|
||||
page_size: PAGE_SIZE,
|
||||
query: curQuery,
|
||||
...(curFilter === 'all' ? {} : { status: curFilter }),
|
||||
});
|
||||
const changeModal = useChangeModal({
|
||||
callback: refreshUsers,
|
||||
});
|
||||
|
||||
const handleClick = ({ user_id, status }) => {
|
||||
changeModal.onShow({
|
||||
id: user_id,
|
||||
type: status,
|
||||
});
|
||||
};
|
||||
|
||||
if (isCreate) {
|
||||
return <CreateForm onClose={() => setCreateState(false)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="mb-4">{t('title')}</h3>
|
||||
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
size="sm"
|
||||
onClick={() => setCreateState(true)}>
|
||||
{t('new_label')}
|
||||
</Button>
|
||||
</div>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('name')}</th>
|
||||
<th style={{ width: '12%' }}>{t('color')}</th>
|
||||
<th style={{ width: '20%' }}>{t('description')}</th>
|
||||
|
||||
{curFilter !== 'deleted' ? (
|
||||
<th style={{ width: '10%' }}>{t('action')}</th>
|
||||
) : null}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="align-middle">
|
||||
{data?.list.map((user) => {
|
||||
return (
|
||||
<tr key={user.user_id}>
|
||||
<td>
|
||||
<BaseUserCard
|
||||
data={user}
|
||||
className="fs-6"
|
||||
avatarSize="24px"
|
||||
avatarSearchStr="s=48"
|
||||
/>
|
||||
</td>
|
||||
<td>{user.rank}</td>
|
||||
<td className="text-break">{user.e_mail}</td>
|
||||
|
||||
{curFilter !== 'deleted' ? (
|
||||
<td>
|
||||
{user.status !== 'deleted' && (
|
||||
<Button
|
||||
className="p-0 btn-no-border"
|
||||
variant="link"
|
||||
onClick={() => handleClick(user)}>
|
||||
{t('change')}
|
||||
</Button>
|
||||
)}
|
||||
</td>
|
||||
) : null}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
{Number(data?.count) <= 0 && !isLoading && <Empty />}
|
||||
<div className="mt-4 mb-2 d-flex justify-content-center">
|
||||
<Pagination
|
||||
currentPage={curPage}
|
||||
totalSize={data?.count || 0}
|
||||
pageSize={PAGE_SIZE}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Users;
|
|
@ -26,10 +26,15 @@ const Legal: FC = () => {
|
|||
title: t('recommend_tags.label'),
|
||||
description: t('recommend_tags.text'),
|
||||
},
|
||||
required_tags: {
|
||||
required_tag: {
|
||||
type: 'boolean',
|
||||
title: t('required_tags.label'),
|
||||
description: t('required_tags.text'),
|
||||
title: t('required_tag.label'),
|
||||
description: t('required_tag.text'),
|
||||
},
|
||||
reserved_tags: {
|
||||
type: 'string',
|
||||
title: t('reserved_tags.label'),
|
||||
description: t('reserved_tags.text'),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -40,9 +45,15 @@ const Legal: FC = () => {
|
|||
rows: 5,
|
||||
},
|
||||
},
|
||||
required_tags: {
|
||||
required_tag: {
|
||||
'ui:widget': 'switch',
|
||||
},
|
||||
reserved_tags: {
|
||||
'ui:widget': 'textarea',
|
||||
'ui:options': {
|
||||
rows: 5,
|
||||
},
|
||||
},
|
||||
};
|
||||
const [formData, setFormData] = useState(initFormData(schema));
|
||||
|
||||
|
@ -52,7 +63,7 @@ const Legal: FC = () => {
|
|||
|
||||
const reqParams: Type.AdminSettingsWrite = {
|
||||
recommend_tags: formData.recommend_tags.value,
|
||||
required_tags: formData.required_tags.value,
|
||||
required_tag: formData.required_tag.value,
|
||||
};
|
||||
|
||||
console.log(reqParams);
|
||||
|
|
|
@ -93,14 +93,7 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
|
|||
</div>
|
||||
<div className="m-n1">
|
||||
{data?.tags?.map((item: any) => {
|
||||
return (
|
||||
<Tag
|
||||
className="m-1"
|
||||
href={`/tags/${item.main_tag_slug_name || item.slug_name}`}
|
||||
key={item.slug_name}>
|
||||
{item.slug_name}
|
||||
</Tag>
|
||||
);
|
||||
return <Tag className="m-1" key={item.slug_name} data={item} />;
|
||||
})}
|
||||
</div>
|
||||
<article
|
||||
|
|
|
@ -67,11 +67,7 @@ const Index: FC<Props> = ({ data }) => {
|
|||
)}
|
||||
|
||||
{data.object?.tags?.map((item) => {
|
||||
return (
|
||||
<Tag href={`/tags/${item.slug_name}`} className="me-1">
|
||||
{item.slug_name}
|
||||
</Tag>
|
||||
);
|
||||
return <Tag key={item.slug_name} className="me-1" data={item} />;
|
||||
})}
|
||||
</ListGroupItem>
|
||||
);
|
||||
|
|
|
@ -152,9 +152,15 @@ const TagIntroduction = () => {
|
|||
<>
|
||||
<div className="mb-3">
|
||||
{t('synonyms.text')}{' '}
|
||||
<Tag className="me-2 mb-2" href="#">
|
||||
{tagName}
|
||||
</Tag>
|
||||
<Tag
|
||||
className="me-2 mb-2"
|
||||
href="#"
|
||||
data={{
|
||||
slug_name: tagName || '',
|
||||
main_tag_slug_name: '',
|
||||
display_name: '',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<TagSelector
|
||||
value={synonymsTags}
|
||||
|
@ -170,9 +176,8 @@ const TagIntroduction = () => {
|
|||
<Tag
|
||||
key={item.tag_id}
|
||||
className="me-2 mb-2"
|
||||
href={`/tags/${item.slug_name}`}>
|
||||
{item.slug_name}
|
||||
</Tag>
|
||||
data={item}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
|
|
|
@ -77,9 +77,8 @@ const Tags = () => {
|
|||
className="mb-4">
|
||||
<Card className="h-100">
|
||||
<Card.Body className="d-flex flex-column align-items-start">
|
||||
<Tag className="mb-3" href={`/tags/${tag.slug_name}`}>
|
||||
{tag.slug_name}
|
||||
</Tag>
|
||||
<Tag className="mb-3" data={tag} />
|
||||
|
||||
<p className="fs-14 flex-fill text-break text-wrap text-truncate-4">
|
||||
{tag.original_text}
|
||||
</p>
|
||||
|
|
|
@ -46,14 +46,7 @@ const Index: FC<Props> = ({ visible, data }) => {
|
|||
</div>
|
||||
<div>
|
||||
{item.question_info?.tags?.map((tag) => {
|
||||
return (
|
||||
<Tag
|
||||
href={`/t/${tag.main_tag_slug_name || tag.slug_name}`}
|
||||
key={tag.slug_name}
|
||||
className="me-1">
|
||||
{tag.slug_name}
|
||||
</Tag>
|
||||
);
|
||||
return <Tag key={tag.slug_name} className="me-1" data={tag} />;
|
||||
})}
|
||||
</div>
|
||||
</ListGroupItem>
|
||||
|
|
|
@ -73,14 +73,7 @@ const Index: FC<Props> = ({ visible, tabName, data }) => {
|
|||
</div>
|
||||
<div>
|
||||
{item.tags?.map((tag) => {
|
||||
return (
|
||||
<Tag
|
||||
href={`/t/${tag.main_tag_slug_name || tag.slug_name}`}
|
||||
className="me-1"
|
||||
key={tag.slug_name}>
|
||||
{tag.slug_name}
|
||||
</Tag>
|
||||
);
|
||||
return <Tag className="me-1" key={tag.slug_name} data={tag} />;
|
||||
})}
|
||||
</div>
|
||||
</ListGroupItem>
|
||||
|
|
|
@ -9,7 +9,7 @@ import type { FormDataType } from '@/common/interface';
|
|||
import { UploadImg, Avatar } from '@/components';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { useToast } from '@/hooks';
|
||||
import { modifyUserInfo, uploadAvatar, getLoggedUserInfo } from '@/services';
|
||||
import { modifyUserInfo, getLoggedUserInfo } from '@/services';
|
||||
|
||||
const Index: React.FC = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
|
@ -60,21 +60,16 @@ const Index: React.FC = () => {
|
|||
setFormData({ ...formData, ...params });
|
||||
};
|
||||
|
||||
const avatarUpload = (file: any) => {
|
||||
return new Promise((resolve) => {
|
||||
uploadAvatar(file).then((res) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
avatar: {
|
||||
...formData.avatar,
|
||||
type: 'custom',
|
||||
custom: res,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
resolve(true);
|
||||
});
|
||||
const avatarUpload = (path: string) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
avatar: {
|
||||
...formData.avatar,
|
||||
type: 'custom',
|
||||
custom: path,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -366,7 +361,7 @@ const Index: React.FC = () => {
|
|||
<div>
|
||||
<UploadImg
|
||||
type="avatar"
|
||||
upload={avatarUpload}
|
||||
uploadCallback={avatarUpload}
|
||||
className="mb-2"
|
||||
/>
|
||||
<div>
|
||||
|
|
|
@ -263,10 +263,6 @@ const routes: RouteNode[] = [
|
|||
path: 'write',
|
||||
page: 'pages/Admin/Write',
|
||||
},
|
||||
{
|
||||
path: 'labels',
|
||||
page: 'pages/Admin/Labels',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -88,3 +88,11 @@ export const getAdminLanguageOptions = () => {
|
|||
const apiUrl = `/answer/admin/api/language/options`;
|
||||
return request.get<Type.LangsType[]>(apiUrl);
|
||||
};
|
||||
|
||||
export const getBrandSetting = () => {
|
||||
return request.get('/answer/admin/api/siteinfo/branding');
|
||||
};
|
||||
|
||||
export const brandSetting = (params: Type.BrandReqParams) => {
|
||||
return request.put('/answer/admin/api/siteinfo/branding', params);
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@ import qs from 'qs';
|
|||
|
||||
import request from '@/utils/request';
|
||||
import type * as Type from '@/common/interface';
|
||||
import { tryLoggedAndActicevated } from '@/utils/guard';
|
||||
import { tryLoggedAndActivated } from '@/utils/guard';
|
||||
|
||||
export const useQueryNotifications = (params) => {
|
||||
const apiUrl = `/answer/api/v1/notification/page?${qs.stringify(params, {
|
||||
|
@ -33,7 +33,7 @@ export const useQueryNotificationStatus = () => {
|
|||
const apiUrl = '/answer/api/v1/notification/status';
|
||||
|
||||
return useSWR<{ inbox: number; achievement: number }>(
|
||||
tryLoggedAndActicevated().ok ? apiUrl : null,
|
||||
tryLoggedAndActivated().ok ? apiUrl : null,
|
||||
request.instance.get,
|
||||
{
|
||||
refreshInterval: 3000,
|
||||
|
|
|
@ -2,7 +2,7 @@ import useSWR from 'swr';
|
|||
|
||||
import request from '@/utils/request';
|
||||
import type * as Type from '@/common/interface';
|
||||
import { tryLoggedAndActicevated } from '@/utils/guard';
|
||||
import { tryLoggedAndActivated } from '@/utils/guard';
|
||||
|
||||
export const deleteTag = (id) => {
|
||||
return request.delete('/answer/api/v1/tag', {
|
||||
|
@ -24,7 +24,7 @@ export const saveSynonymsTags = (params) => {
|
|||
|
||||
export const useFollowingTags = () => {
|
||||
let apiUrl = '';
|
||||
if (tryLoggedAndActicevated().ok) {
|
||||
if (tryLoggedAndActivated().ok) {
|
||||
apiUrl = '/answer/api/v1/tags/following';
|
||||
}
|
||||
const { data, error, mutate } = useSWR<any[]>(apiUrl, request.instance.get);
|
||||
|
|
|
@ -4,12 +4,13 @@ import useSWR from 'swr';
|
|||
import request from '@/utils/request';
|
||||
import type * as Type from '@/common/interface';
|
||||
|
||||
export const uploadImage = (file) => {
|
||||
export const uploadImage = (params: { file: File; type: Type.UploadType }) => {
|
||||
const form = new FormData();
|
||||
|
||||
form.append('file', file);
|
||||
return request.post('/answer/api/v1/user/post/file', form);
|
||||
form.append('source', String(params.type));
|
||||
form.append('file', params.file);
|
||||
return request.post('/answer/api/v1/file', form);
|
||||
};
|
||||
|
||||
export const useQueryQuestionByTitle = (title) => {
|
||||
return useSWR<Record<string, any>>(
|
||||
title ? `/answer/api/v1/question/similar?title=${title}` : '',
|
||||
|
@ -127,10 +128,6 @@ export const modifyUserInfo = (params: Type.ModifyUserReq) => {
|
|||
return request.put('/answer/api/v1/user/info', params);
|
||||
};
|
||||
|
||||
export const uploadAvatar = (params: Type.AvatarUploadReq) => {
|
||||
return request.post('/answer/api/v1/user/avatar/upload', params);
|
||||
};
|
||||
|
||||
export const resetPassword = (params: Type.PasswordResetReq) => {
|
||||
return request.post('/answer/api/v1/user/password/reset', params);
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@ import { DEFAULT_LANG } from '@/common/constants';
|
|||
interface InterfaceType {
|
||||
interface: AdminSettingsInterface;
|
||||
update: (params: AdminSettingsInterface) => void;
|
||||
updateLogo: (logo: string) => void;
|
||||
}
|
||||
|
||||
const interfaceSetting = create<InterfaceType>((set) => ({
|
||||
|
@ -21,6 +22,15 @@ const interfaceSetting = create<InterfaceType>((set) => ({
|
|||
interface: params,
|
||||
};
|
||||
}),
|
||||
updateLogo: (logo) =>
|
||||
set((state) => {
|
||||
return {
|
||||
interface: {
|
||||
...state.interface,
|
||||
logo,
|
||||
},
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
export default interfaceSetting;
|
||||
|
|
|
@ -182,9 +182,10 @@ export const tryNormalLogged = (canNavigate: boolean = false) => {
|
|||
return false;
|
||||
};
|
||||
|
||||
export const tryLoggedAndActicevated = () => {
|
||||
export const tryLoggedAndActivated = () => {
|
||||
const gr: TGuardResult = { ok: true };
|
||||
const us = deriveLoginState();
|
||||
|
||||
if (!us.isLogged || !us.isActivated) {
|
||||
gr.ok = false;
|
||||
}
|
||||
|
|
|
@ -48,10 +48,7 @@ class Request {
|
|||
return data;
|
||||
},
|
||||
(error) => {
|
||||
if (error.isAxiosError) {
|
||||
return Promise.reject(false);
|
||||
}
|
||||
const { status, data: respData, msg: respMsg } = error.response;
|
||||
const { status, data: respData, msg: respMsg } = error.response || {};
|
||||
const { data, msg = '' } = respData;
|
||||
if (status === 400) {
|
||||
// show error message
|
||||
|
|
Loading…
Reference in New Issue