Merge branch 'feat/ui-v0.4' of git.backyard.segmentfault.com:opensource/answer into feat/ui-v0.4

This commit is contained in:
haitao(lj) 2022-11-15 12:28:14 +08:00
commit c73723c23d
30 changed files with 218 additions and 422 deletions

View File

@ -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

View File

@ -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',

View File

@ -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;
}

View File

@ -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>

View File

@ -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 });
});
};

View File

@ -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} />;
})}
</>
) : (

View File

@ -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}

View File

@ -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>
);

View File

@ -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>
);
};

View File

@ -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>
);

View File

@ -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);
}

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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

View File

@ -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>
);

View File

@ -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}
/>
);
})
) : (

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -263,10 +263,6 @@ const routes: RouteNode[] = [
path: 'write',
page: 'pages/Admin/Write',
},
{
path: 'labels',
page: 'pages/Admin/Labels',
},
],
},
{

View File

@ -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);
};

View File

@ -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,

View File

@ -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);

View File

@ -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);
};

View File

@ -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;

View File

@ -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;
}

View File

@ -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