mirror of https://gitee.com/answerdev/answer.git
Merge branch 'dev' into fix/search
This commit is contained in:
commit
7bdb6d2016
|
@ -17,6 +17,9 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:16 AS node-builder
|
FROM amd64/node AS node-builder
|
||||||
|
|
||||||
LABEL maintainer="mingcheng<mc@sf.com>"
|
LABEL maintainer="mingcheng<mc@sf.com>"
|
||||||
|
|
||||||
|
|
174
i18n/en_US.yaml
174
i18n/en_US.yaml
|
@ -235,7 +235,7 @@ ui:
|
||||||
show_more: Show more
|
show_more: Show more
|
||||||
suspended:
|
suspended:
|
||||||
title: Your Account has been Suspended
|
title: Your Account has been Suspended
|
||||||
until_time: 'Your account was suspended until {{ time }}.'
|
until_time: "Your account was suspended until {{ time }}."
|
||||||
forever: This user was suspended forever.
|
forever: This user was suspended forever.
|
||||||
end: You don't meet a community guideline.
|
end: You don't meet a community guideline.
|
||||||
editor:
|
editor:
|
||||||
|
@ -420,12 +420,12 @@ ui:
|
||||||
btn_cancel: Cancel
|
btn_cancel: Cancel
|
||||||
dates:
|
dates:
|
||||||
long_date: MMM D
|
long_date: MMM D
|
||||||
long_date_with_year: 'MMM D, YYYY'
|
long_date_with_year: "MMM D, YYYY"
|
||||||
long_date_with_time: 'MMM D, YYYY [at] HH:mm'
|
long_date_with_time: "MMM D, YYYY [at] HH:mm"
|
||||||
now: now
|
now: now
|
||||||
x_seconds_ago: '{{count}}s ago'
|
x_seconds_ago: "{{count}}s ago"
|
||||||
x_minutes_ago: '{{count}}m ago'
|
x_minutes_ago: "{{count}}m ago"
|
||||||
x_hours_ago: '{{count}}h ago'
|
x_hours_ago: "{{count}}h ago"
|
||||||
hour: hour
|
hour: hour
|
||||||
day: day
|
day: day
|
||||||
comment:
|
comment:
|
||||||
|
@ -507,7 +507,7 @@ ui:
|
||||||
add_btn: Add tag
|
add_btn: Add tag
|
||||||
create_btn: Create new tag
|
create_btn: Create new tag
|
||||||
search_tag: Search tag
|
search_tag: Search tag
|
||||||
hint: 'Describe what your question is about, at least one tag is required.'
|
hint: "Describe what your question is about, at least one tag is required."
|
||||||
no_result: No tags matched
|
no_result: No tags matched
|
||||||
header:
|
header:
|
||||||
nav:
|
nav:
|
||||||
|
@ -536,7 +536,7 @@ ui:
|
||||||
first: >-
|
first: >-
|
||||||
You're almost done! We sent an activation mail to <bold>{{mail}}</bold>.
|
You're almost done! We sent an activation mail to <bold>{{mail}}</bold>.
|
||||||
Please follow the instructions in the mail to activate your account.
|
Please follow the instructions in the mail to activate your account.
|
||||||
info: 'If it doesn''t arrive, check your spam folder.'
|
info: "If it doesn't arrive, check your spam folder."
|
||||||
another: >-
|
another: >-
|
||||||
We sent another activation email to you at <bold>{{mail}}</bold>. It might
|
We sent another activation email to you at <bold>{{mail}}</bold>. It might
|
||||||
take a few minutes for it to arrive; be sure to check your spam folder.
|
take a few minutes for it to arrive; be sure to check your spam folder.
|
||||||
|
@ -548,6 +548,7 @@ ui:
|
||||||
page_title: Welcome to Answer
|
page_title: Welcome to Answer
|
||||||
info_sign: Don't have an account? <1>Sign up</1>
|
info_sign: Don't have an account? <1>Sign up</1>
|
||||||
info_login: Already have an account? <1>Log in</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>.
|
||||||
forgot_pass: Forgot password?
|
forgot_pass: Forgot password?
|
||||||
name:
|
name:
|
||||||
label: Name
|
label: Name
|
||||||
|
@ -634,15 +635,15 @@ ui:
|
||||||
label: About Me (optional)
|
label: About Me (optional)
|
||||||
website:
|
website:
|
||||||
label: Website (optional)
|
label: Website (optional)
|
||||||
placeholder: 'https://example.com'
|
placeholder: "https://example.com"
|
||||||
msg: Website incorrect format
|
msg: Website incorrect format
|
||||||
location:
|
location:
|
||||||
label: Location (optional)
|
label: Location (optional)
|
||||||
placeholder: 'City, Country'
|
placeholder: "City, Country"
|
||||||
notification:
|
notification:
|
||||||
email:
|
email:
|
||||||
label: Email Notifications
|
label: Email Notifications
|
||||||
radio: 'Answers to your questions, comments, and more'
|
radio: "Answers to your questions, comments, and more"
|
||||||
account:
|
account:
|
||||||
change_email_btn: Change email
|
change_email_btn: Change email
|
||||||
change_pass_btn: Change password
|
change_pass_btn: Change password
|
||||||
|
@ -732,7 +733,7 @@ ui:
|
||||||
options: Options
|
options: Options
|
||||||
follow: Follow
|
follow: Follow
|
||||||
following: Following
|
following: Following
|
||||||
counts: '{{count}} Results'
|
counts: "{{count}} Results"
|
||||||
more: More
|
more: More
|
||||||
sort_btns:
|
sort_btns:
|
||||||
relevance: Relevance
|
relevance: Relevance
|
||||||
|
@ -742,12 +743,12 @@ ui:
|
||||||
more: More
|
more: More
|
||||||
tips:
|
tips:
|
||||||
title: Advanced Search Tips
|
title: Advanced Search Tips
|
||||||
tag: '<1>[tag]</1> search withing a tag'
|
tag: "<1>[tag]</1> search withing a tag"
|
||||||
user: '<1>user:username</1> search by author'
|
user: "<1>user:username</1> search by author"
|
||||||
answer: '<1>answers:0</1> unanswered questions'
|
answer: "<1>answers:0</1> unanswered questions"
|
||||||
score: '<1>score:3</1> posts with a 3+ score'
|
score: "<1>score:3</1> posts with a 3+ score"
|
||||||
question: '<1>is:question</1> search questions'
|
question: "<1>is:question</1> search questions"
|
||||||
is_answer: '<1>is:answer</1> search answers'
|
is_answer: "<1>is:answer</1> search answers"
|
||||||
empty: We couldn't find anything. <br /> Try different or less specific keywords.
|
empty: We couldn't find anything. <br /> Try different or less specific keywords.
|
||||||
share:
|
share:
|
||||||
name: Share
|
name: Share
|
||||||
|
@ -777,8 +778,8 @@ ui:
|
||||||
follow_tag_tip: Follow tags to curate your list of questions.
|
follow_tag_tip: Follow tags to curate your list of questions.
|
||||||
hot_questions: Hot Questions
|
hot_questions: Hot Questions
|
||||||
all_questions: All Questions
|
all_questions: All Questions
|
||||||
x_questions: '{{ count }} Questions'
|
x_questions: "{{ count }} Questions"
|
||||||
x_answers: '{{ count }} answers'
|
x_answers: "{{ count }} answers"
|
||||||
questions: Questions
|
questions: Questions
|
||||||
answers: Answers
|
answers: Answers
|
||||||
newest: Newest
|
newest: Newest
|
||||||
|
@ -805,12 +806,12 @@ ui:
|
||||||
newest: Newest
|
newest: Newest
|
||||||
score: Score
|
score: Score
|
||||||
edit_profile: Edit Profile
|
edit_profile: Edit Profile
|
||||||
visited_x_days: 'Visited {{ count }} days'
|
visited_x_days: "Visited {{ count }} days"
|
||||||
viewed: Viewed
|
viewed: Viewed
|
||||||
joined: Joined
|
joined: Joined
|
||||||
last_login: Seen
|
last_login: Seen
|
||||||
about_me: About Me
|
about_me: About Me
|
||||||
about_me_empty: '// Hello, World !'
|
about_me_empty: "// Hello, World !"
|
||||||
top_answers: Top Answers
|
top_answers: Top Answers
|
||||||
top_questions: Top Questions
|
top_questions: Top Questions
|
||||||
stats: Stats
|
stats: Stats
|
||||||
|
@ -845,7 +846,7 @@ ui:
|
||||||
msg: Password cannot be empty.
|
msg: Password cannot be empty.
|
||||||
db_host:
|
db_host:
|
||||||
label: Database Host
|
label: Database Host
|
||||||
placeholder: 'db:3306'
|
placeholder: "db:3306"
|
||||||
msg: Database Host cannot be empty.
|
msg: Database Host cannot be empty.
|
||||||
db_name:
|
db_name:
|
||||||
label: Database Name
|
label: Database Name
|
||||||
|
@ -861,7 +862,7 @@ ui:
|
||||||
description: >-
|
description: >-
|
||||||
You can create the <1>config.yaml</1> file manually in the
|
You can create the <1>config.yaml</1> file manually in the
|
||||||
<1>/var/wwww/xxx/</1> directory and paste the following text into it.
|
<1>/var/wwww/xxx/</1> directory and paste the following text into it.
|
||||||
info: 'After you’ve done that, click “Next” button.'
|
info: "After you’ve done that, click “Next” button."
|
||||||
site_information: Site Information
|
site_information: Site Information
|
||||||
admin_account: Admin Account
|
admin_account: Admin Account
|
||||||
site_name:
|
site_name:
|
||||||
|
@ -898,7 +899,7 @@ ui:
|
||||||
ready_description: >-
|
ready_description: >-
|
||||||
If you ever feel like changing more settings, visit <1>admin section</1>;
|
If you ever feel like changing more settings, visit <1>admin section</1>;
|
||||||
find it in the site menu.
|
find it in the site menu.
|
||||||
good_luck: 'Have fun, and good luck!'
|
good_luck: "Have fun, and good luck!"
|
||||||
warn_title: Warning
|
warn_title: Warning
|
||||||
warn_description: >-
|
warn_description: >-
|
||||||
The file <1>config.yaml</1> already exists. If you need to reset any of the
|
The file <1>config.yaml</1> already exists. If you need to reset any of the
|
||||||
|
@ -913,46 +914,51 @@ ui:
|
||||||
This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host’s database server is down.
|
This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host’s database server is down.
|
||||||
|
|
||||||
page_404:
|
page_404:
|
||||||
description: 'Unfortunately, this page doesn''t exist.'
|
description: "Unfortunately, this page doesn't exist."
|
||||||
back_home: Back to homepage
|
back_home: Back to homepage
|
||||||
page_50X:
|
page_50X:
|
||||||
description: The server encountered an error and could not complete your request.
|
description: The server encountered an error and could not complete your request.
|
||||||
back_home: Back to homepage
|
back_home: Back to homepage
|
||||||
page_maintenance:
|
page_maintenance:
|
||||||
description: 'We are under maintenance, we’ll be back soon.'
|
description: "We are under maintenance, we’ll be back soon."
|
||||||
|
nav_menus:
|
||||||
|
dashboard: Dashboard
|
||||||
|
contents: Contents
|
||||||
|
questions: Questions
|
||||||
|
answers: Answers
|
||||||
|
users: Users
|
||||||
|
flags: Flags
|
||||||
|
settings: Settings
|
||||||
|
general: General
|
||||||
|
interface: Interface
|
||||||
|
smtp: SMTP
|
||||||
|
branding: Branding
|
||||||
|
legal: Legal
|
||||||
|
write: Write
|
||||||
|
tos: Terms of Service
|
||||||
|
privacy: Privacy
|
||||||
admin:
|
admin:
|
||||||
admin_header:
|
admin_header:
|
||||||
title: Admin
|
title: Admin
|
||||||
nav_menus:
|
|
||||||
dashboard: Dashboard
|
|
||||||
contents: Contents
|
|
||||||
questions: Questions
|
|
||||||
answers: Answers
|
|
||||||
users: Users
|
|
||||||
flags: Flags
|
|
||||||
settings: Settings
|
|
||||||
general: General
|
|
||||||
interface: Interface
|
|
||||||
smtp: SMTP
|
|
||||||
dashboard:
|
dashboard:
|
||||||
title: Dashboard
|
title: Dashboard
|
||||||
welcome: Welcome to Answer Admin!
|
welcome: Welcome to Answer Admin!
|
||||||
site_statistics: Site Statistics
|
site_statistics: Site Statistics
|
||||||
questions: 'Questions:'
|
questions: "Questions:"
|
||||||
answers: 'Answers:'
|
answers: "Answers:"
|
||||||
comments: 'Comments:'
|
comments: "Comments:"
|
||||||
votes: 'Votes:'
|
votes: "Votes:"
|
||||||
active_users: 'Active users:'
|
active_users: "Active users:"
|
||||||
flags: 'Flags:'
|
flags: "Flags:"
|
||||||
site_health_status: Site Health Status
|
site_health_status: Site Health Status
|
||||||
version: 'Version:'
|
version: "Version:"
|
||||||
https: 'HTTPS:'
|
https: "HTTPS:"
|
||||||
uploading_files: 'Uploading files:'
|
uploading_files: "Uploading files:"
|
||||||
smtp: 'SMTP:'
|
smtp: "SMTP:"
|
||||||
timezone: 'Timezone:'
|
timezone: "Timezone:"
|
||||||
system_info: System Info
|
system_info: System Info
|
||||||
storage_used: 'Storage used:'
|
storage_used: "Storage used:"
|
||||||
uptime: 'Uptime:'
|
uptime: "Uptime:"
|
||||||
answer_links: Answer Links
|
answer_links: Answer Links
|
||||||
documents: Documents
|
documents: Documents
|
||||||
feedback: Feedback
|
feedback: Feedback
|
||||||
|
@ -961,8 +967,8 @@ ui:
|
||||||
update_to: Update to
|
update_to: Update to
|
||||||
latest: Latest
|
latest: Latest
|
||||||
check_failed: Check failed
|
check_failed: Check failed
|
||||||
'yes': 'Yes'
|
"yes": "Yes"
|
||||||
'no': 'No'
|
"no": "No"
|
||||||
not_allowed: Not allowed
|
not_allowed: Not allowed
|
||||||
allowed: Allowed
|
allowed: Allowed
|
||||||
enabled: Enabled
|
enabled: Enabled
|
||||||
|
@ -984,7 +990,7 @@ ui:
|
||||||
suspended_name: suspended
|
suspended_name: suspended
|
||||||
suspended_description: A suspended user can't log in.
|
suspended_description: A suspended user can't log in.
|
||||||
deleted_name: deleted
|
deleted_name: deleted
|
||||||
deleted_description: 'Delete profile, authentication associations.'
|
deleted_description: "Delete profile, authentication associations."
|
||||||
inactive_name: inactive
|
inactive_name: inactive
|
||||||
inactive_description: An inactive user must re-validate their email.
|
inactive_description: An inactive user must re-validate their email.
|
||||||
confirm_title: Delete this user
|
confirm_title: Delete this user
|
||||||
|
@ -993,11 +999,11 @@ ui:
|
||||||
msg:
|
msg:
|
||||||
empty: Please select a reason.
|
empty: Please select a reason.
|
||||||
status_modal:
|
status_modal:
|
||||||
title: 'Change {{ type }} status to...'
|
title: "Change {{ type }} status to..."
|
||||||
normal_name: normal
|
normal_name: normal
|
||||||
normal_description: A normal post available to everyone.
|
normal_description: A normal post available to everyone.
|
||||||
closed_name: closed
|
closed_name: closed
|
||||||
closed_description: 'A closed question can''t answer, but still can edit, vote and comment.'
|
closed_description: "A closed question can't answer, but still can edit, vote and comment."
|
||||||
deleted_name: deleted
|
deleted_name: deleted
|
||||||
deleted_description: All reputation gained and lost will be restored.
|
deleted_description: All reputation gained and lost will be restored.
|
||||||
btn_cancel: Cancel
|
btn_cancel: Cancel
|
||||||
|
@ -1020,7 +1026,7 @@ ui:
|
||||||
deleted: Deleted
|
deleted: Deleted
|
||||||
normal: Normal
|
normal: Normal
|
||||||
filter:
|
filter:
|
||||||
placeholder: 'Filter by name, user:id'
|
placeholder: "Filter by name, user:id"
|
||||||
questions:
|
questions:
|
||||||
page_title: Questions
|
page_title: Questions
|
||||||
normal: Normal
|
normal: Normal
|
||||||
|
@ -1034,7 +1040,7 @@ ui:
|
||||||
action: Action
|
action: Action
|
||||||
change: Change
|
change: Change
|
||||||
filter:
|
filter:
|
||||||
placeholder: 'Filter by title, question:id'
|
placeholder: "Filter by title, question:id"
|
||||||
answers:
|
answers:
|
||||||
page_title: Answers
|
page_title: Answers
|
||||||
normal: Normal
|
normal: Normal
|
||||||
|
@ -1046,13 +1052,13 @@ ui:
|
||||||
action: Action
|
action: Action
|
||||||
change: Change
|
change: Change
|
||||||
filter:
|
filter:
|
||||||
placeholder: 'Filter by title, answer:id'
|
placeholder: "Filter by title, answer:id"
|
||||||
general:
|
general:
|
||||||
page_title: General
|
page_title: General
|
||||||
name:
|
name:
|
||||||
label: Site Name
|
label: Site Name
|
||||||
msg: Site name cannot be empty.
|
msg: Site name cannot be empty.
|
||||||
text: 'The name of this site, as used in the title tag.'
|
text: "The name of this site, as used in the title tag."
|
||||||
site_url:
|
site_url:
|
||||||
label: Site URL
|
label: Site URL
|
||||||
msg: Site url cannot be empty.
|
msg: Site url cannot be empty.
|
||||||
|
@ -1061,11 +1067,11 @@ ui:
|
||||||
short_description:
|
short_description:
|
||||||
label: Short Site Description (optional)
|
label: Short Site Description (optional)
|
||||||
msg: Short site description cannot be empty.
|
msg: Short site description cannot be empty.
|
||||||
text: 'Short description, as used in the title tag on homepage.'
|
text: "Short description, as used in the title tag on homepage."
|
||||||
description:
|
description:
|
||||||
label: Site Description (optional)
|
label: Site Description (optional)
|
||||||
msg: Site description cannot be empty.
|
msg: Site description cannot be empty.
|
||||||
text: 'Describe this site in one sentence, as used in the meta description tag.'
|
text: "Describe this site in one sentence, as used in the meta description tag."
|
||||||
contact_email:
|
contact_email:
|
||||||
label: Contact Email
|
label: Contact Email
|
||||||
msg: Contact email cannot be empty.
|
msg: Contact email cannot be empty.
|
||||||
|
@ -1126,5 +1132,45 @@ ui:
|
||||||
smtp_authentication:
|
smtp_authentication:
|
||||||
label: SMTP Authentication
|
label: SMTP Authentication
|
||||||
msg: SMTP authentication cannot be empty.
|
msg: SMTP authentication cannot be empty.
|
||||||
'yes': 'Yes'
|
"yes": "Yes"
|
||||||
'no': 'No'
|
"no": "No"
|
||||||
|
branding:
|
||||||
|
page_title: Branding
|
||||||
|
logo:
|
||||||
|
label: Logo
|
||||||
|
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
|
||||||
|
msg: Square icon cannot be empty.
|
||||||
|
text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
|
||||||
|
favicon:
|
||||||
|
label: Favicon (optional)
|
||||||
|
text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, “square icon” will be used.
|
||||||
|
legal:
|
||||||
|
page_title: Legal
|
||||||
|
terms_of_service:
|
||||||
|
label: Terms of Service
|
||||||
|
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
|
||||||
|
text: "You can add privacy policy content here. If you already have a document hosted elsewhere, provide the full URL here."
|
||||||
|
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
|
||||||
|
btn_submit: Save
|
||||||
|
not_found_props: "Required property {{ key }} not found."
|
||||||
|
|
|
@ -759,20 +759,20 @@ ui:
|
||||||
page_50X:
|
page_50X:
|
||||||
description: 服务器遇到了一个错误,无法完成你的请求。
|
description: 服务器遇到了一个错误,无法完成你的请求。
|
||||||
back_home: 回到主页
|
back_home: 回到主页
|
||||||
|
nav_menus:
|
||||||
|
dashboard: 后台管理
|
||||||
|
contents: 内容管理
|
||||||
|
questions: 问题
|
||||||
|
answers: 回答
|
||||||
|
users: 用户管理
|
||||||
|
flags: 举报管理
|
||||||
|
settings: 站点设置
|
||||||
|
general: 一般
|
||||||
|
interface: 界面
|
||||||
|
smtp: SMTP
|
||||||
admin:
|
admin:
|
||||||
admin_header:
|
admin_header:
|
||||||
title: 后台管理
|
title: 后台管理
|
||||||
nav_menus:
|
|
||||||
dashboard: 后台管理
|
|
||||||
contents: 内容管理
|
|
||||||
questions: 问题
|
|
||||||
answers: 回答
|
|
||||||
users: 用户管理
|
|
||||||
flags: 举报管理
|
|
||||||
settings: 站点设置
|
|
||||||
general: 一般
|
|
||||||
interface: 界面
|
|
||||||
smtp: SMTP
|
|
||||||
dashboard:
|
dashboard:
|
||||||
title: 后台管理
|
title: 后台管理
|
||||||
welcome: 欢迎来到 Answer 后台管理!
|
welcome: 欢迎来到 Answer 后台管理!
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
REACT_APP_API_URL=http://10.0.10.98:2060
|
PUBLIC_URL
|
||||||
|
REACT_APP_API_URL = http://127.0.0.1
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
REACT_APP_API_URL=/
|
PUBLIC_URL = /
|
||||||
REACT_APP_PUBLIC_PATH=/
|
REACT_APP_API_URL = /
|
||||||
REACT_APP_VERSION=
|
|
||||||
|
|
|
@ -13,10 +13,7 @@
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env.local
|
.env*.local
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
include:
|
|
||||||
- project: 'segmentfault/devops/templates'
|
|
||||||
file:
|
|
||||||
- .deploy-cdn.yml
|
|
||||||
- .deploy-helm.yml
|
|
||||||
|
|
||||||
variables:
|
|
||||||
FF_USE_FASTZIP: 'true'
|
|
||||||
PROJECT_NAME: 'answer_static'
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- install
|
|
||||||
- publish
|
|
||||||
- deploy
|
|
||||||
|
|
||||||
# 静态资源构建
|
|
||||||
install:
|
|
||||||
image: dockerhub.qingcloud.com/sf_base/node-build:14
|
|
||||||
stage: install
|
|
||||||
allow_failure: false
|
|
||||||
|
|
||||||
cache:
|
|
||||||
- key:
|
|
||||||
files:
|
|
||||||
- pnpm-lock.yml
|
|
||||||
paths:
|
|
||||||
- node_modules/
|
|
||||||
policy: pull-push
|
|
||||||
script:
|
|
||||||
- pnpm install
|
|
||||||
- if [ "$CI_COMMIT_BRANCH" = "dev" ]; then
|
|
||||||
sed -i "s/<projectName>/$PROJECT_NAME/g" .env.development;
|
|
||||||
sed -i "s/<version>/$CI_COMMIT_SHORT_SHA/g" .env.development;
|
|
||||||
pnpm run build:dev;
|
|
||||||
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
|
||||||
sed -i "s/<projectName>/$PROJECT_NAME/g" .env.test;
|
|
||||||
sed -i "s/<version>/$CI_COMMIT_SHORT_SHA/g" .env.test;
|
|
||||||
pnpm run build:test;
|
|
||||||
elif [ "$CI_COMMIT_BRANCH" = "release" ]; then
|
|
||||||
sed -i "s/<projectName>/$PROJECT_NAME/g" .env.production;
|
|
||||||
sed -i "s/<version>/$CI_COMMIT_SHORT_SHA/g" .env.production;
|
|
||||||
pnpm run build:prod;
|
|
||||||
fi
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- build/
|
|
||||||
|
|
||||||
publish:cdn:dev:
|
|
||||||
extends: .deploy-cdn
|
|
||||||
stage: publish
|
|
||||||
only:
|
|
||||||
- dev
|
|
||||||
variables:
|
|
||||||
AssetsPath: ./build
|
|
||||||
Project: $PROJECT_NAME
|
|
||||||
Version: $CI_COMMIT_SHORT_SHA
|
|
||||||
Destination: dev
|
|
||||||
|
|
||||||
publish:cdn:test:
|
|
||||||
extends: .deploy-cdn
|
|
||||||
stage: publish
|
|
||||||
only:
|
|
||||||
- main
|
|
||||||
variables:
|
|
||||||
AssetsPath: ./build
|
|
||||||
Project: $PROJECT_NAME
|
|
||||||
Version: $CI_COMMIT_SHORT_SHA
|
|
||||||
Destination: test
|
|
||||||
|
|
||||||
publish:cdn:prod:
|
|
||||||
extends: .deploy-cdn
|
|
||||||
stage: publish
|
|
||||||
only:
|
|
||||||
- release
|
|
||||||
variables:
|
|
||||||
AssetsPath: ./build
|
|
||||||
Project: $PROJECT_NAME
|
|
||||||
Version: $CI_COMMIT_SHORT_SHA
|
|
||||||
Destination: prod
|
|
||||||
|
|
||||||
deploy:dev:
|
|
||||||
extends: .deploy-helm
|
|
||||||
stage: deploy
|
|
||||||
only:
|
|
||||||
- dev
|
|
||||||
needs:
|
|
||||||
- publish:cdn:dev
|
|
||||||
variables:
|
|
||||||
KubernetesCluster: dev
|
|
||||||
KubernetesNamespace: 'sf-test'
|
|
||||||
DockerTag: $CI_COMMIT_SHORT_SHA
|
|
||||||
ChartName: answer-web
|
|
||||||
InstallPolicy: replace
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
strict-peer-dependencies = true
|
||||||
|
auto-install-peers = true
|
|
@ -30,7 +30,7 @@ clone the repo locally and run following command in your terminal:
|
||||||
$ git clone git@github.com:answerdev/answer.git answer
|
$ git clone git@github.com:answerdev/answer.git answer
|
||||||
$ cd answer/ui
|
$ cd answer/ui
|
||||||
$ pnpm install
|
$ pnpm install
|
||||||
$ pnpm run start
|
$ pnpm start
|
||||||
```
|
```
|
||||||
|
|
||||||
now, your browser should already open automatically, and autoload `http://localhost:3000`.
|
now, your browser should already open automatically, and autoload `http://localhost:3000`.
|
||||||
|
@ -41,11 +41,9 @@ you can also manually visit it.
|
||||||
when cloning repo, and run `pnpm install` to init dependencies. you can use project commands below:
|
when cloning repo, and run `pnpm install` to init dependencies. you can use project commands below:
|
||||||
|
|
||||||
- `pnpm run start` run Answer web locally.
|
- `pnpm run start` run Answer web locally.
|
||||||
- `pnpm run build:dev` build code for environment `dev`
|
- `pnpm run build` build Answer for production
|
||||||
- `pnpm run build:test` build code for environment `test`
|
|
||||||
- `pnpm run build:prod` build code for environment `prod`
|
|
||||||
- `pnpm run lint` lint and fix the code style
|
- `pnpm run lint` lint and fix the code style
|
||||||
- `pnpm run cz` run `git commit` by `commitizen`
|
|
||||||
|
|
||||||
## 🖥 Environment Support
|
## 🖥 Environment Support
|
||||||
|
|
||||||
|
|
|
@ -8,12 +8,8 @@ const i18nPath = path.resolve(__dirname, "../i18n");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
webpack: function(config, env) {
|
webpack: function(config, env) {
|
||||||
if (env === "production") {
|
|
||||||
config.output.publicPath = process.env.REACT_APP_PUBLIC_PATH;
|
|
||||||
}
|
|
||||||
|
|
||||||
addWebpackAlias({
|
addWebpackAlias({
|
||||||
["@"]: path.resolve(__dirname, "src"),
|
"@": path.resolve(__dirname, "src"),
|
||||||
"@i18n": i18nPath
|
"@i18n": i18nPath
|
||||||
})(config);
|
})(config);
|
||||||
|
|
||||||
|
@ -34,16 +30,16 @@ module.exports = {
|
||||||
return function(proxy, allowedHost) {
|
return function(proxy, allowedHost) {
|
||||||
const config = configFunction(proxy, allowedHost);
|
const config = configFunction(proxy, allowedHost);
|
||||||
config.proxy = {
|
config.proxy = {
|
||||||
"/answer": {
|
'/answer': {
|
||||||
target: "http://10.0.10.98:2060",
|
target: process.env.REACT_APP_API_URL,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false
|
secure: false,
|
||||||
},
|
},
|
||||||
"/installation": {
|
'/installation': {
|
||||||
target: "http://10.0.10.98:2060",
|
target: process.env.REACT_APP_API_URL,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false
|
secure: false,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,26 +5,13 @@
|
||||||
"homepage": "/",
|
"homepage": "/",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-app-rewired start",
|
"start": "react-app-rewired start",
|
||||||
"build:dev": "env-cmd -f .env.development react-app-rewired build",
|
"build": "react-app-rewired build",
|
||||||
"build:test": "env-cmd -f .env.test react-app-rewired build",
|
|
||||||
"build:prod": "env-cmd -f .env.production react-app-rewired build",
|
|
||||||
"build": "env-cmd -f .env react-app-rewired build",
|
|
||||||
"test": "react-app-rewired test",
|
|
||||||
"lint": "eslint . --cache --fix --ext .ts,.tsx",
|
"lint": "eslint . --cache --fix --ext .ts,.tsx",
|
||||||
"prepare": "cd .. && husky install",
|
|
||||||
"cz": "cz",
|
|
||||||
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
|
|
||||||
"prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
|
"prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
|
||||||
|
"prepare": "cd .. && husky install",
|
||||||
"preinstall": "node ./scripts/preinstall.js"
|
"preinstall": "node ./scripts/preinstall.js"
|
||||||
},
|
},
|
||||||
"config": {
|
|
||||||
"commitizen": {
|
|
||||||
"path": "ui/node_modules/cz-conventional-changelog"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@testing-library/jest-dom": "^4.2.4",
|
|
||||||
"ahooks": "^3.7.0",
|
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"bootstrap": "^5.2.0",
|
"bootstrap": "^5.2.0",
|
||||||
"bootstrap-icons": "^1.9.1",
|
"bootstrap-icons": "^1.9.1",
|
||||||
|
@ -32,11 +19,7 @@
|
||||||
"codemirror": "5.65.0",
|
"codemirror": "5.65.0",
|
||||||
"copy-to-clipboard": "^3.3.2",
|
"copy-to-clipboard": "^3.3.2",
|
||||||
"dayjs": "^1.11.5",
|
"dayjs": "^1.11.5",
|
||||||
"highlight.js": "^11.6.0",
|
|
||||||
"i18next": "^21.9.0",
|
"i18next": "^21.9.0",
|
||||||
"i18next-chained-backend": "^3.0.2",
|
|
||||||
"i18next-http-backend": "^1.4.1",
|
|
||||||
"i18next-localstorage-backend": "^3.1.3",
|
|
||||||
"katex": "^0.16.2",
|
"katex": "^0.16.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"marked": "^4.0.19",
|
"marked": "^4.0.19",
|
||||||
|
@ -54,13 +37,10 @@
|
||||||
"zustand": "^4.1.1"
|
"zustand": "^4.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.18.10",
|
|
||||||
"@babel/plugin-syntax-flow": "^7.18.6",
|
|
||||||
"@babel/plugin-transform-react-jsx": "^7.14.9",
|
|
||||||
"@commitlint/cli": "^17.0.3",
|
"@commitlint/cli": "^17.0.3",
|
||||||
"@commitlint/config-conventional": "^17.0.3",
|
|
||||||
"@fullhuman/postcss-purgecss": "^4.1.3",
|
"@fullhuman/postcss-purgecss": "^4.1.3",
|
||||||
"@popperjs/core": "^2.11.5",
|
"purgecss-webpack-plugin": "^4.1.3",
|
||||||
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
"@testing-library/dom": "^8.17.1",
|
"@testing-library/dom": "^8.17.1",
|
||||||
"@testing-library/react": "^13.3.0",
|
"@testing-library/react": "^13.3.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
@ -74,11 +54,7 @@
|
||||||
"@types/react-helmet": "^6.1.5",
|
"@types/react-helmet": "^6.1.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||||
"@typescript-eslint/parser": "^5.33.0",
|
"@typescript-eslint/parser": "^5.33.0",
|
||||||
"commitizen": "^4.2.5",
|
|
||||||
"conventional-changelog-cli": "^2.2.2",
|
|
||||||
"customize-cra": "^1.0.0",
|
"customize-cra": "^1.0.0",
|
||||||
"cz-conventional-changelog": "^3.3.0",
|
|
||||||
"env-cmd": "^10.1.0",
|
|
||||||
"eslint": "^8.0.1",
|
"eslint": "^8.0.1",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||||
|
@ -95,13 +71,10 @@
|
||||||
"lint-staged": "^13.0.3",
|
"lint-staged": "^13.0.3",
|
||||||
"postcss": "^8.0.0",
|
"postcss": "^8.0.0",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"purgecss-webpack-plugin": "^4.1.3",
|
|
||||||
"react-app-rewired": "^2.2.1",
|
"react-app-rewired": "^2.2.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"sass": "^1.54.4",
|
"sass": "^1.54.4",
|
||||||
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
"typescript": "^4.8.3",
|
||||||
"typescript": "*",
|
|
||||||
"web-vitals": "^2.1.4",
|
|
||||||
"yaml-loader": "^0.8.0"
|
"yaml-loader": "^0.8.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@7.9.5",
|
"packageManager": "pnpm@7.9.5",
|
||||||
|
@ -110,4 +83,4 @@
|
||||||
"pnpm": ">=7"
|
"pnpm": ">=7"
|
||||||
},
|
},
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2,7 +2,6 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<!-- <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />-->
|
<!-- <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />-->
|
||||||
|
@ -24,7 +23,34 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root">
|
||||||
|
<style>
|
||||||
|
@keyframes _doc-spin {
|
||||||
|
to { transform: rotate(360deg) }
|
||||||
|
}
|
||||||
|
#doc-spinner {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
#doc-spinner .spinner {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-block;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
vertical-align: -.125em;
|
||||||
|
border: .25rem solid currentColor;
|
||||||
|
border-right-color: transparent;
|
||||||
|
color: rgba(108, 117, 125, .75);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: 0.75s linear infinite _doc-spin;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="doc-spinner">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!--
|
<!--
|
||||||
This HTML file is a template.
|
This HTML file is a template.
|
||||||
If you open it directly in the browser, you will see an empty page.
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
// There is a bug when using npm to install: the execution of preinstall is after install, so when this prompt is displayed, the dependent packages have already been installed.
|
// There is a bug when using npm to install: the execution of preinstall is after install, so when this prompt is displayed, the dependent packages have already been installed.
|
||||||
if (!/pnpm/.test(process.env.npm_execpath)) {
|
if (!/pnpm/.test(process.env.npm_execpath)) {
|
||||||
console.warn(`\u001b[33mThis repository requires using pnpm as the package manager for scripts to work properly.\u001b[39m\n`)
|
console.warn(
|
||||||
process.exit(1)
|
`\u001b[33mThis repository requires using pnpm as the package manager for scripts to work properly.\u001b[39m\n`,
|
||||||
}
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ export const ADMIN_NAV_MENUS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'contents',
|
name: 'contents',
|
||||||
child: [{ name: 'questions' }, { name: 'answers' }],
|
children: [{ name: 'questions' }, { name: 'answers' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'users',
|
name: 'users',
|
||||||
|
@ -54,10 +54,19 @@ export const ADMIN_NAV_MENUS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'settings',
|
name: 'settings',
|
||||||
child: [{ name: 'general' }, { name: 'interface' }, { name: 'smtp' }],
|
children: [
|
||||||
|
{ name: 'general' },
|
||||||
|
{ name: 'interface' },
|
||||||
|
{ name: 'branding' },
|
||||||
|
{ name: 'smtp' },
|
||||||
|
{ name: 'legal' },
|
||||||
|
{ name: 'write' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const ADMIN_LEGAL_MENUS = [{ name: 'tos' }, { name: 'privacy' }];
|
||||||
|
|
||||||
export const TIMEZONES = [
|
export const TIMEZONES = [
|
||||||
{
|
{
|
||||||
label: 'Africa',
|
label: 'Africa',
|
||||||
|
|
|
@ -24,6 +24,8 @@ export interface ReportParams {
|
||||||
export interface TagBase {
|
export interface TagBase {
|
||||||
display_name: string;
|
display_name: string;
|
||||||
slug_name: string;
|
slug_name: string;
|
||||||
|
recommend: boolean;
|
||||||
|
reserved: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Tag extends TagBase {
|
export interface Tag extends TagBase {
|
||||||
|
@ -126,7 +128,8 @@ export interface UserInfoRes extends UserInfoBase {
|
||||||
[prop: string]: any;
|
[prop: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AvatarUploadReq {
|
export type UploadType = 'post' | 'avatar' | 'branding';
|
||||||
|
export interface UploadReq {
|
||||||
file: FormData;
|
file: FormData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,7 +269,6 @@ export interface AdminSettingsGeneral {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminSettingsInterface {
|
export interface AdminSettingsInterface {
|
||||||
logo: string;
|
|
||||||
language: string;
|
language: string;
|
||||||
theme: string;
|
theme: string;
|
||||||
time_zone?: string;
|
time_zone?: string;
|
||||||
|
@ -285,10 +287,31 @@ export interface AdminSettingsSmtp {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SiteSettings {
|
export interface SiteSettings {
|
||||||
|
branding: AdmingSettingBranding;
|
||||||
general: AdminSettingsGeneral;
|
general: AdminSettingsGeneral;
|
||||||
interface: AdminSettingsInterface;
|
interface: AdminSettingsInterface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdmingSettingBranding {
|
||||||
|
logo: string;
|
||||||
|
square_icon: string;
|
||||||
|
mobile_logo?: string;
|
||||||
|
favicon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminSettingsLegal {
|
||||||
|
privacy_policy_original_text?: string;
|
||||||
|
privacy_policy_parsed_text?: string;
|
||||||
|
terms_of_service_original_text?: string;
|
||||||
|
terms_of_service_parsed_text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminSettingsWrite {
|
||||||
|
recommend_tags: string[];
|
||||||
|
required_tag: string;
|
||||||
|
reserved_tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description interface for Activity
|
* @description interface for Activity
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { useAccordionButton } from 'react-bootstrap/AccordionButton';
|
||||||
import { Icon } from '@/components';
|
import { Icon } from '@/components';
|
||||||
|
|
||||||
function MenuNode({ menu, callback, activeKey, isLeaf = false }) {
|
function MenuNode({ menu, callback, activeKey, isLeaf = false }) {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.nav_menus' });
|
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
|
||||||
const accordionClick = useAccordionButton(menu.name);
|
const accordionClick = useAccordionButton(menu.name);
|
||||||
const menuOnClick = (evt) => {
|
const menuOnClick = (evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
@ -44,30 +44,45 @@ function MenuNode({ menu, callback, activeKey, isLeaf = false }) {
|
||||||
|
|
||||||
interface AccordionProps {
|
interface AccordionProps {
|
||||||
menus: any[];
|
menus: any[];
|
||||||
|
path?: string;
|
||||||
}
|
}
|
||||||
const AccordionNav: FC<AccordionProps> = ({ menus }) => {
|
const AccordionNav: FC<AccordionProps> = ({ menus, path = '/' }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
let activeKey = menus[0].name;
|
const pathMatch = useMatch(`${path}*`);
|
||||||
const pathMatch = useMatch('/admin/*');
|
if (!menus.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// auto set menu fields
|
||||||
|
menus.forEach((m) => {
|
||||||
|
if (!Array.isArray(m.children)) {
|
||||||
|
m.children = [];
|
||||||
|
}
|
||||||
|
m.children.forEach((sm) => {
|
||||||
|
if (!Array.isArray(sm.children)) {
|
||||||
|
sm.children = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
const splat = pathMatch && pathMatch.params['*'];
|
const splat = pathMatch && pathMatch.params['*'];
|
||||||
|
let activeKey = menus[0].name;
|
||||||
if (splat) {
|
if (splat) {
|
||||||
activeKey = splat;
|
activeKey = splat;
|
||||||
}
|
}
|
||||||
const menuClick = (clickedMenu) => {
|
const menuClick = (clickedMenu) => {
|
||||||
const menuKey = clickedMenu.name;
|
const menuKey = clickedMenu.name;
|
||||||
if (Array.isArray(clickedMenu.child) && clickedMenu.child.length) {
|
if (clickedMenu.children.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (activeKey !== menuKey) {
|
if (activeKey !== menuKey) {
|
||||||
const routePath = `/admin/${menuKey}`;
|
const routePath = `${path}${menuKey}`;
|
||||||
navigate(routePath);
|
navigate(routePath);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let defaultOpenKey;
|
let defaultOpenKey;
|
||||||
menus.forEach((li) => {
|
menus.forEach((li) => {
|
||||||
if (Array.isArray(li.child) && li.child.length) {
|
if (li.children.length) {
|
||||||
const matchedChild = li.child.find((el) => {
|
const matchedChild = li.children.find((el) => {
|
||||||
return el.name === activeKey;
|
return el.name === activeKey;
|
||||||
});
|
});
|
||||||
if (matchedChild) {
|
if (matchedChild) {
|
||||||
|
@ -83,10 +98,10 @@ const AccordionNav: FC<AccordionProps> = ({ menus }) => {
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={li.name}>
|
<React.Fragment key={li.name}>
|
||||||
<MenuNode menu={li} callback={menuClick} activeKey={activeKey} />
|
<MenuNode menu={li} callback={menuClick} activeKey={activeKey} />
|
||||||
{Array.isArray(li.child) ? (
|
{li.children.length ? (
|
||||||
<Accordion.Collapse eventKey={li.name} className="ms-4">
|
<Accordion.Collapse eventKey={li.name} className="ms-4">
|
||||||
<Stack direction="vertical" gap={1}>
|
<Stack direction="vertical" gap={1}>
|
||||||
{li.child?.map((leaf) => {
|
{li.children.map((leaf) => {
|
||||||
return (
|
return (
|
||||||
<MenuNode
|
<MenuNode
|
||||||
menu={leaf}
|
menu={leaf}
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { FC, memo } from 'react';
|
|
||||||
import { Container } from 'react-bootstrap';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useMatch } from 'react-router-dom';
|
|
||||||
|
|
||||||
const Index: FC = () => {
|
|
||||||
const { t } = useTranslation('translation', {
|
|
||||||
keyPrefix: 'admin.admin_header',
|
|
||||||
});
|
|
||||||
const adminPathMatch = useMatch('/admin/*');
|
|
||||||
if (!adminPathMatch) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="bg-light py-2">
|
|
||||||
<Container className="py-1">
|
|
||||||
<h6 className="mb-0 fw-bold lh-base">{t('title')}</h6>
|
|
||||||
</Container>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(Index);
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { ButtonGroup, Button } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import { Icon, UploadImg } from '@/components';
|
||||||
|
import { UploadType } from '@/common/interface';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
type: UploadType;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
acceptType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Index: FC<Props> = ({ type = 'post', value, onChange, acceptType }) => {
|
||||||
|
const onUpload = (imgPath: string) => {
|
||||||
|
onChange(imgPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemove = () => {
|
||||||
|
onChange('');
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="d-flex">
|
||||||
|
<div className="bg-gray-300 upload-img-wrap me-2 d-flex align-items-center justify-content-center">
|
||||||
|
<img src={value} alt="" height={100} />
|
||||||
|
</div>
|
||||||
|
<ButtonGroup vertical className="fit-content">
|
||||||
|
<UploadImg
|
||||||
|
type={type}
|
||||||
|
uploadCallback={onUpload}
|
||||||
|
className="mb-0"
|
||||||
|
acceptType={acceptType}>
|
||||||
|
<Icon name="cloud-upload" />
|
||||||
|
</UploadImg>
|
||||||
|
|
||||||
|
<Button variant="outline-secondary" onClick={onRemove}>
|
||||||
|
<Icon name="trash" />
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -61,7 +61,7 @@ const Image: FC<IEditorContext> = ({ editor }) => {
|
||||||
files: FileList,
|
files: FileList,
|
||||||
): Promise<{ url: string; name: string }[]> => {
|
): Promise<{ url: string; name: string }[]> => {
|
||||||
const promises = Array.from(files).map(async (file) => {
|
const promises = Array.from(files).map(async (file) => {
|
||||||
const url = await uploadImage(file);
|
const url = await uploadImage({ file, type: 'post' });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: file.name,
|
name: file.name,
|
||||||
|
@ -209,7 +209,7 @@ const Image: FC<IEditorContext> = ({ editor }) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadImage(e.target.files[0]).then((url) => {
|
uploadImage({ file: e.target.files[0], type: 'post' }).then((url) => {
|
||||||
setLink({ ...link, value: url });
|
setLink({ ...link, value: url });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import type { Editor, Position } from 'codemirror';
|
import type { Editor, Position } from 'codemirror';
|
||||||
import type CodeMirror from 'codemirror';
|
import type CodeMirror from 'codemirror';
|
||||||
// import 'highlight.js/styles/github.css';
|
|
||||||
import 'katex/dist/katex.min.css';
|
import 'katex/dist/katex.min.css';
|
||||||
|
|
||||||
export function createEditorUtils(
|
export function createEditorUtils(
|
||||||
|
@ -114,9 +113,4 @@ export function htmlRender(el: HTMLElement | null) {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
// import('highlight.js').then(({ default: highlight }) => {
|
|
||||||
// el.querySelectorAll('pre code').forEach((code) => {
|
|
||||||
// highlight.highlightElement(code as HTMLElement);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
import { TagSelector, Tag } from '@/components';
|
import { TagSelector, Tag } from '@/components';
|
||||||
import { tryLoggedAndActicevated } from '@/utils/guard';
|
import { tryLoggedAndActivated } from '@/utils/guard';
|
||||||
import { useFollowingTags, followTags } from '@/services';
|
import { useFollowingTags, followTags } from '@/services';
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
|
@ -32,7 +32,7 @@ const Index: FC = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!tryLoggedAndActicevated().ok) {
|
if (!tryLoggedAndActivated().ok) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,11 +73,7 @@ const Index: FC = () => {
|
||||||
<>
|
<>
|
||||||
{followingTags.map((item) => {
|
{followingTags.map((item) => {
|
||||||
const slugName = item?.slug_name;
|
const slugName = item?.slug_name;
|
||||||
return (
|
return <Tag key={slugName} className="m-1" data={item} />;
|
||||||
<Tag key={slugName} className="m-1" href={`/tags/${slugName}`}>
|
|
||||||
{slugName}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
useLocation,
|
useLocation,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import { loggedUserInfoStore, siteInfoStore, interfaceStore } from '@/stores';
|
import { loggedUserInfoStore, siteInfoStore, brandingStore } from '@/stores';
|
||||||
import { logout, useQueryNotificationStatus } from '@/services';
|
import { logout, useQueryNotificationStatus } from '@/services';
|
||||||
import { RouteAlias } from '@/router/alias';
|
import { RouteAlias } from '@/router/alias';
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ const Header: FC = () => {
|
||||||
const q = urlSearch.get('q');
|
const q = urlSearch.get('q');
|
||||||
const [searchStr, setSearch] = useState('');
|
const [searchStr, setSearch] = useState('');
|
||||||
const siteInfo = siteInfoStore((state) => state.siteInfo);
|
const siteInfo = siteInfoStore((state) => state.siteInfo);
|
||||||
const { interface: interfaceInfo } = interfaceStore();
|
const brandingInfo = brandingStore((state) => state.branding);
|
||||||
const { data: redDot } = useQueryNotificationStatus();
|
const { data: redDot } = useQueryNotificationStatus();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const handleInput = (val) => {
|
const handleInput = (val) => {
|
||||||
|
@ -73,10 +73,10 @@ const Header: FC = () => {
|
||||||
|
|
||||||
<div className="d-flex justify-content-between align-items-center nav-grow flex-nowrap">
|
<div className="d-flex justify-content-between align-items-center nav-grow flex-nowrap">
|
||||||
<Navbar.Brand to="/" as={Link} className="lh-1 me-0 me-sm-3">
|
<Navbar.Brand to="/" as={Link} className="lh-1 me-0 me-sm-3">
|
||||||
{interfaceInfo.logo ? (
|
{brandingInfo.logo ? (
|
||||||
<img
|
<img
|
||||||
className="logo rounded-1 me-0"
|
className="logo rounded-1 me-0"
|
||||||
src={interfaceInfo.logo}
|
src={brandingInfo.logo}
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import React, { memo, FC } from 'react';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { labelStyle } from '@/utils';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Index: FC<IProps> = ({ className = '', children, color }) => {
|
||||||
|
// hover
|
||||||
|
const [hover, setHover] = React.useState(false);
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={classNames('badge-label rounded-1', className)}
|
||||||
|
onMouseEnter={() => setHover(true)}
|
||||||
|
onMouseLeave={() => setHover(false)}
|
||||||
|
style={labelStyle(color, hover)}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Index);
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Card, Dropdown, Form } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import Label from '../Label';
|
||||||
|
|
||||||
|
const Labels = ({ className }) => {
|
||||||
|
return (
|
||||||
|
<Card className={className}>
|
||||||
|
<Card.Header className="d-flex justify-content-between align-items-center">
|
||||||
|
<Card.Title className="mb-0">Labels</Card.Title>
|
||||||
|
|
||||||
|
<Dropdown align="end">
|
||||||
|
<Dropdown.Toggle variant="link" className="no-toggle">
|
||||||
|
Edit
|
||||||
|
</Dropdown.Toggle>
|
||||||
|
|
||||||
|
<Dropdown.Menu className="p-3">
|
||||||
|
<Form.Check className="mb-2" type="checkbox" label="featured" />
|
||||||
|
<Form.Check className="mb-2" type="checkbox" label="featured" />
|
||||||
|
<Form.Check className="mb-2" type="checkbox" label="featured" />
|
||||||
|
<Form.Check type="checkbox" label="featured" />
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Body>
|
||||||
|
<Label className="badge-label" color="#DC3545">
|
||||||
|
featured
|
||||||
|
</Label>
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Labels;
|
|
@ -158,14 +158,7 @@ const QuestionList: FC<Props> = ({ source }) => {
|
||||||
{Array.isArray(li.tags)
|
{Array.isArray(li.tags)
|
||||||
? li.tags.map((tag) => {
|
? li.tags.map((tag) => {
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag key={tag.slug_name} className="m-1" data={tag} />
|
||||||
key={tag.slug_name}
|
|
||||||
className="m-1"
|
|
||||||
href={`/tags/${
|
|
||||||
tag.main_tag_slug_name || tag.slug_name
|
|
||||||
}`}>
|
|
||||||
{tag.slug_name}
|
|
||||||
</Tag>
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
: null}
|
: null}
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
# SchemaForm User Guide
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
SchemaForm is a component that can be used to render a form based on a [JSON schema](https://json-schema.org/understanding-json-schema/index.html).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import React from 'react';
|
||||||
|
import { SchemaForm, initFormData, JSONSchema, UISchema } from '@/components';
|
||||||
|
|
||||||
|
const schema: JSONSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
title: 'Name',
|
||||||
|
},
|
||||||
|
age: {
|
||||||
|
type: 'number',
|
||||||
|
title: 'Age',
|
||||||
|
},
|
||||||
|
sex:{
|
||||||
|
type: 'boolean',
|
||||||
|
title: 'sex',
|
||||||
|
enum: [1, 2],
|
||||||
|
enumNames: ['male', 'female'],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const uiSchema: UISchema = {
|
||||||
|
name: {
|
||||||
|
'ui:widget': 'input',
|
||||||
|
},
|
||||||
|
age: {
|
||||||
|
'ui:widget': 'input',
|
||||||
|
'ui:options': {
|
||||||
|
type: 'number'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sex: {
|
||||||
|
'ui:widget': 'radio',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// form component
|
||||||
|
|
||||||
|
const Form = () => {
|
||||||
|
const [formData, setFormData] = useState(initFormData(schema));
|
||||||
|
return (
|
||||||
|
<SchemaForm
|
||||||
|
schema={schema}
|
||||||
|
uiSchema={uiSchema}
|
||||||
|
formData={formData}
|
||||||
|
onChange={console.log}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
| Property | Description | Type | Default |
|
||||||
|
| -------- | ---------------------------------------- | ----------------------------------------- | ------- |
|
||||||
|
| schema | JSON schema | [JSONSchema](index.tsx#L9) | - |
|
||||||
|
| uiSchema | UI schema | [UISchema](index.tsx#L24) | - |
|
||||||
|
| formData | Form data | [FormData](index.tsx#L66) | - |
|
||||||
|
| onChange | Callback function when form data changes | (data: [FormData](index.tsx#L66)) => void | - |
|
||||||
|
| onSubmit | Callback function when form is submitted | (data: React.FormEvent) => void | - |
|
||||||
|
|
||||||
|
## reference
|
||||||
|
|
||||||
|
- [json schema](https://json-schema.org/understanding-json-schema/index.html)
|
||||||
|
- [react-jsonschema-form](http://rjsf-team.github.io/react-jsonschema-form/)
|
||||||
|
- [vue-json-schema-form](https://github.com/lljj-x/vue-json-schema-form/)
|
|
@ -0,0 +1,415 @@
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { Form, Button, Stack } from 'react-bootstrap';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import BrandUpload from '../BrandUpload';
|
||||||
|
import TimeZonePicker from '../TimeZonePicker';
|
||||||
|
import type * as Type from '@/common/interface';
|
||||||
|
|
||||||
|
export interface JSONSchema {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
required?: string[];
|
||||||
|
properties: {
|
||||||
|
[key: string]: {
|
||||||
|
type: 'string' | 'boolean';
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
enum?: Array<string | boolean>;
|
||||||
|
enumNames?: string[];
|
||||||
|
default?: string | boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export interface UISchema {
|
||||||
|
[key: string]: {
|
||||||
|
'ui:widget'?:
|
||||||
|
| 'textarea'
|
||||||
|
| 'text'
|
||||||
|
| 'checkbox'
|
||||||
|
| 'radio'
|
||||||
|
| 'select'
|
||||||
|
| 'upload'
|
||||||
|
| 'timezone'
|
||||||
|
| 'switch';
|
||||||
|
'ui:options'?: {
|
||||||
|
rows?: number;
|
||||||
|
placeholder?: string;
|
||||||
|
type?:
|
||||||
|
| 'color'
|
||||||
|
| 'date'
|
||||||
|
| 'datetime-local'
|
||||||
|
| 'email'
|
||||||
|
| 'month'
|
||||||
|
| 'number'
|
||||||
|
| 'password'
|
||||||
|
| 'range'
|
||||||
|
| 'search'
|
||||||
|
| 'tel'
|
||||||
|
| 'text'
|
||||||
|
| 'time'
|
||||||
|
| 'url'
|
||||||
|
| 'week';
|
||||||
|
empty?: string;
|
||||||
|
validator?: (value) => Promise<string | true | void> | true | string;
|
||||||
|
textRender?: () => React.ReactElement;
|
||||||
|
imageType?: Type.UploadType;
|
||||||
|
acceptType?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
schema: JSONSchema;
|
||||||
|
uiSchema?: UISchema;
|
||||||
|
formData?: Type.FormDataType;
|
||||||
|
onChange?: (data: Type.FormDataType) => void;
|
||||||
|
onSubmit: (e: React.FormEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* json schema form
|
||||||
|
* @param schema json schema
|
||||||
|
* @param uiSchema ui schema
|
||||||
|
* @param formData form data
|
||||||
|
* @param onChange change event
|
||||||
|
* @param onSubmit submit event
|
||||||
|
*/
|
||||||
|
const SchemaForm: FC<IProps> = ({
|
||||||
|
schema,
|
||||||
|
uiSchema = {},
|
||||||
|
formData = {},
|
||||||
|
onChange,
|
||||||
|
onSubmit,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation('translation', {
|
||||||
|
keyPrefix: 'form',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { required = [], properties } = schema;
|
||||||
|
|
||||||
|
// check required field
|
||||||
|
const excludes = required.filter((key) => !properties[key]);
|
||||||
|
|
||||||
|
if (excludes.length > 0) {
|
||||||
|
console.error(t('not_found_props', { key: excludes.join(', ') }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = Object.keys(properties);
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
const data = {
|
||||||
|
...formData,
|
||||||
|
[name]: { ...formData[name], value, isInvalid: false },
|
||||||
|
};
|
||||||
|
if (onChange instanceof Function) {
|
||||||
|
onChange(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSwitchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, checked } = e.target;
|
||||||
|
const data = {
|
||||||
|
...formData,
|
||||||
|
[name]: { ...formData[name], value: checked, isInvalid: false },
|
||||||
|
};
|
||||||
|
if (onChange instanceof Function) {
|
||||||
|
onChange(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const requiredValidator = () => {
|
||||||
|
const errors: string[] = [];
|
||||||
|
required.forEach((key) => {
|
||||||
|
if (!formData[key] || !formData[key].value) {
|
||||||
|
errors.push(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncValidator = () => {
|
||||||
|
const errors: Array<{ key: string; msg: string }> = [];
|
||||||
|
const promises: Array<{
|
||||||
|
key: string;
|
||||||
|
promise;
|
||||||
|
}> = [];
|
||||||
|
keys.forEach((key) => {
|
||||||
|
const { validator } = uiSchema[key]?.['ui:options'] || {};
|
||||||
|
if (validator instanceof Function) {
|
||||||
|
const value = formData[key]?.value;
|
||||||
|
promises.push({
|
||||||
|
key,
|
||||||
|
promise: validator(value),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Promise.allSettled(promises.map((item) => item.promise)).then(
|
||||||
|
(results) => {
|
||||||
|
results.forEach((result, index) => {
|
||||||
|
const { key } = promises[index];
|
||||||
|
if (result.status === 'rejected') {
|
||||||
|
errors.push({
|
||||||
|
key,
|
||||||
|
msg: result.reason.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
const msg = result.value;
|
||||||
|
if (typeof msg === 'string') {
|
||||||
|
errors.push({
|
||||||
|
key,
|
||||||
|
msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return errors;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const errors = requiredValidator();
|
||||||
|
if (errors.length > 0) {
|
||||||
|
formData = errors.reduce((acc, cur) => {
|
||||||
|
console.log('schema.properties[cur]', cur);
|
||||||
|
acc[cur] = {
|
||||||
|
...formData[cur],
|
||||||
|
isInvalid: true,
|
||||||
|
errorMsg:
|
||||||
|
uiSchema[cur]?.['ui:options']?.empty ||
|
||||||
|
`${schema.properties[cur]?.title} ${t('empty')}`,
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
}, formData);
|
||||||
|
if (onChange instanceof Function) {
|
||||||
|
onChange({ ...formData });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const syncErrors = await syncValidator();
|
||||||
|
if (syncErrors.length > 0) {
|
||||||
|
formData = syncErrors.reduce((acc, cur) => {
|
||||||
|
acc[cur.key] = {
|
||||||
|
...formData[cur.key],
|
||||||
|
isInvalid: true,
|
||||||
|
errorMsg:
|
||||||
|
cur.msg || `${schema.properties[cur.key].title} ${t('invalid')}`,
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
}, formData);
|
||||||
|
if (onChange instanceof Function) {
|
||||||
|
onChange({ ...formData });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.keys(formData).forEach((key) => {
|
||||||
|
formData[key].isInvalid = false;
|
||||||
|
formData[key].errorMsg = '';
|
||||||
|
});
|
||||||
|
if (onChange instanceof Function) {
|
||||||
|
onChange(formData);
|
||||||
|
}
|
||||||
|
onSubmit(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUploadChange = (name: string, value: string) => {
|
||||||
|
const data = { ...formData, [name]: { ...formData[name], value } };
|
||||||
|
if (onChange instanceof Function) {
|
||||||
|
onChange(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
|
{keys.map((key) => {
|
||||||
|
const { title, description } = properties[key];
|
||||||
|
const { 'ui:widget': widget = 'input', 'ui:options': options = {} } =
|
||||||
|
uiSchema[key] || {};
|
||||||
|
if (widget === 'select') {
|
||||||
|
return (
|
||||||
|
<Form.Group key={title} controlId={key} className="mb-3">
|
||||||
|
<Form.Label>{title}</Form.Label>
|
||||||
|
<Form.Select
|
||||||
|
aria-label={description}
|
||||||
|
isInvalid={formData[key].isInvalid}>
|
||||||
|
{properties[key].enum?.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<option value={String(item)} key={String(item)}>
|
||||||
|
{properties[key].enumNames?.[index]}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Form.Select>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData[key]?.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
<Form.Text className="text-muted">{description}</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (widget === 'checkbox' || widget === 'radio') {
|
||||||
|
return (
|
||||||
|
<Form.Group key={title} className="mb-3" controlId={key}>
|
||||||
|
<Form.Label>{title}</Form.Label>
|
||||||
|
<Stack direction="horizontal">
|
||||||
|
{properties[key].enum?.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<Form.Check
|
||||||
|
key={String(item)}
|
||||||
|
inline
|
||||||
|
required
|
||||||
|
type={widget}
|
||||||
|
name={title}
|
||||||
|
id={String(item)}
|
||||||
|
label={properties[key].enumNames?.[index]}
|
||||||
|
checked={formData[key]?.value === item}
|
||||||
|
feedback={formData[key]?.errorMsg}
|
||||||
|
feedbackType="invalid"
|
||||||
|
isInvalid={formData[key].isInvalid}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData[key]?.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
<Form.Text className="text-muted">{description}</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget === 'switch') {
|
||||||
|
console.log(formData[key]?.value, 'switch=====');
|
||||||
|
return (
|
||||||
|
<Form.Group key={title} className="mb-3" controlId={key}>
|
||||||
|
<Form.Label>{title}</Form.Label>
|
||||||
|
<Form.Check
|
||||||
|
required
|
||||||
|
id={title}
|
||||||
|
name={key}
|
||||||
|
type="switch"
|
||||||
|
label={title}
|
||||||
|
checked={formData[key]?.value}
|
||||||
|
feedback={formData[key]?.errorMsg}
|
||||||
|
feedbackType="invalid"
|
||||||
|
isInvalid={formData[key].isInvalid}
|
||||||
|
onChange={handleSwitchChange}
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData[key]?.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
<Form.Text className="text-muted">{description}</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (widget === 'timezone') {
|
||||||
|
return (
|
||||||
|
<Form.Group key={title} className="mb-3" controlId={key}>
|
||||||
|
<Form.Label>{title}</Form.Label>
|
||||||
|
<TimeZonePicker
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget === 'upload') {
|
||||||
|
return (
|
||||||
|
<Form.Group key={title} className="mb-3" controlId={key}>
|
||||||
|
<Form.Label>{title}</Form.Label>
|
||||||
|
<BrandUpload
|
||||||
|
type={options.imageType || 'avatar'}
|
||||||
|
acceptType={options.acceptType || ''}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget === 'textarea') {
|
||||||
|
return (
|
||||||
|
<Form.Group controlId={key} key={key} className="mb-3">
|
||||||
|
<Form.Label>{title}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
as="textarea"
|
||||||
|
name={key}
|
||||||
|
placeholder={options?.placeholder || ''}
|
||||||
|
type={options?.type || 'text'}
|
||||||
|
value={formData[key]?.value}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
isInvalid={formData[key].isInvalid}
|
||||||
|
rows={options?.rows || 3}
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData[key]?.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
|
||||||
|
<Form.Text className="text-muted">{description}</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Form.Group controlId={key} key={key} className="mb-3">
|
||||||
|
<Form.Label>{title}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
name={key}
|
||||||
|
placeholder={options?.placeholder || ''}
|
||||||
|
type={options?.type || 'text'}
|
||||||
|
value={formData[key]?.value}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
style={options?.type === 'color' ? { width: '6rem' } : {}}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Button variant="primary" type="submit">
|
||||||
|
{t('btn_submit')}
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const initFormData = (schema: JSONSchema): Type.FormDataType => {
|
||||||
|
const formData: Type.FormDataType = {};
|
||||||
|
Object.keys(schema.properties).forEach((key) => {
|
||||||
|
formData[key] = {
|
||||||
|
value: '',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SchemaForm;
|
|
@ -2,17 +2,27 @@ import React, { memo, FC } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { Tag } from '@/common/interface';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
data: Tag;
|
||||||
|
href?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
children?: React.ReactNode;
|
|
||||||
href: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Index: FC<IProps> = ({ className = '', children, href }) => {
|
const Index: FC<IProps> = ({ className = '', href, data }) => {
|
||||||
href = href.toLowerCase();
|
href =
|
||||||
|
href || `/tags/${data.main_tag_slug_name || data.slug_name}`.toLowerCase();
|
||||||
return (
|
return (
|
||||||
<a href={href} className={classNames('badge-tag rounded-1', className)}>
|
<a
|
||||||
{children}
|
href={href}
|
||||||
|
className={classNames(
|
||||||
|
'badge-tag rounded-1',
|
||||||
|
data.reserved && 'badge-tag-reserved',
|
||||||
|
data.recommend && 'badge-tag-required',
|
||||||
|
className,
|
||||||
|
)}>
|
||||||
|
{data.slug_name}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
import { FC, useState, useEffect } from 'react';
|
import { FC, useState, useEffect } from 'react';
|
||||||
import { Dropdown, FormControl, Button, Form } from 'react-bootstrap';
|
import { Dropdown, FormControl, Button, Form } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
@ -95,17 +96,16 @@ const TagSelector: FC<IProps> = ({
|
||||||
}
|
}
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchTags = (str) => {
|
||||||
if (!tag) {
|
queryTags(str).then((res) => {
|
||||||
setTags(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
queryTags(tag).then((res) => {
|
|
||||||
const tagArray: Type.Tag[] = filterTags(res || []);
|
const tagArray: Type.Tag[] = filterTags(res || []);
|
||||||
setTags(tagArray);
|
setTags(tagArray);
|
||||||
});
|
});
|
||||||
}, [tag]);
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTags(tag);
|
||||||
|
}, [visibleMenu]);
|
||||||
|
|
||||||
const handleClick = (val: Type.Tag) => {
|
const handleClick = (val: Type.Tag) => {
|
||||||
const findIndex = initialValue.findIndex(
|
const findIndex = initialValue.findIndex(
|
||||||
|
@ -143,7 +143,9 @@ const TagSelector: FC<IProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleSearch = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setTag(e.currentTarget.value.replace(';', ''));
|
const searchStr = e.currentTarget.value.replace(';', '');
|
||||||
|
setTag(searchStr);
|
||||||
|
fetchTags(searchStr);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelect = (eventKey) => {
|
const handleSelect = (eventKey) => {
|
||||||
|
@ -186,7 +188,9 @@ const TagSelector: FC<IProps> = ({
|
||||||
'm-1 text-nowrap d-flex align-items-center',
|
'm-1 text-nowrap d-flex align-items-center',
|
||||||
index === repeatIndex && 'warning',
|
index === repeatIndex && 'warning',
|
||||||
)}
|
)}
|
||||||
variant="outline-secondary"
|
variant={`outline-${
|
||||||
|
item.reserved ? 'danger' : item.recommend ? 'dark' : 'secondary'
|
||||||
|
}`}
|
||||||
size="sm">
|
size="sm">
|
||||||
{item.slug_name}
|
{item.slug_name}
|
||||||
<span className="ms-1" onMouseUp={() => handleRemove(item)}>
|
<span className="ms-1" onMouseUp={() => handleRemove(item)}>
|
||||||
|
@ -220,6 +224,14 @@ const TagSelector: FC<IProps> = ({
|
||||||
</Form>
|
</Form>
|
||||||
</Dropdown.Header>
|
</Dropdown.Header>
|
||||||
)}
|
)}
|
||||||
|
{tags && tags.filter((v) => v.recommend)?.length > 0 && (
|
||||||
|
<Dropdown.Item
|
||||||
|
disabled
|
||||||
|
style={{ fontWeight: 500 }}
|
||||||
|
className="text-secondary">
|
||||||
|
Required tag (at least one)
|
||||||
|
</Dropdown.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
{tags?.map((item, index) => {
|
{tags?.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Form } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import { TIMEZONES } from '@/common/constants';
|
||||||
|
|
||||||
|
const TimeZonePicker = (props) => {
|
||||||
|
return (
|
||||||
|
<Form.Select {...props}>
|
||||||
|
{TIMEZONES?.map((item) => {
|
||||||
|
return (
|
||||||
|
<optgroup label={item.label} key={item.label}>
|
||||||
|
{item.options.map((option) => {
|
||||||
|
return (
|
||||||
|
<option value={option.value} key={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</optgroup>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Form.Select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TimeZonePicker;
|
|
@ -1,16 +1,29 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { uploadImage } from '@/services';
|
||||||
|
import * as Type from '@/common/interface';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
type: string;
|
type: Type.UploadType;
|
||||||
upload: (data: FormData) => Promise<any>;
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
acceptType?: string;
|
||||||
|
uploadCallback: (img: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Index: React.FC<IProps> = ({ type, upload }) => {
|
const Index: React.FC<IProps> = ({
|
||||||
|
type,
|
||||||
|
uploadCallback,
|
||||||
|
children,
|
||||||
|
acceptType = '',
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [status, setStatus] = useState(false);
|
const [status, setStatus] = useState(false);
|
||||||
|
|
||||||
const onChange = (e: any) => {
|
const onChange = (e: any) => {
|
||||||
|
console.log('uploading', e);
|
||||||
if (status) {
|
if (status) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -24,25 +37,25 @@ const Index: React.FC<IProps> = ({ type, upload }) => {
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
setStatus(true);
|
setStatus(true);
|
||||||
const data = new FormData();
|
console.log('uploading', e.target.files);
|
||||||
|
uploadImage({ file: e.target.files[0], type })
|
||||||
data.append('file', e.target.files[0]);
|
.then((res) => {
|
||||||
// do
|
uploadCallback(res);
|
||||||
upload(data).finally(() => {
|
})
|
||||||
setStatus(false);
|
.finally(() => {
|
||||||
});
|
setStatus(false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className="mb-2 btn btn-outline-secondary uploadBtn">
|
<label className={`btn btn-outline-secondary uploadBtn ${className}`}>
|
||||||
{status ? t('upload_img.loading') : t('upload_img.name')}
|
{children || (status ? t('upload_img.loading') : t('upload_img.name'))}
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
className="d-none"
|
className="d-none"
|
||||||
accept="image/jpeg,image/jpg,image/png,image/webp"
|
accept={`image/jpeg,image/jpg,image/png,image/webp${acceptType}`}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
id={type}
|
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,13 +18,15 @@ import TextArea from './TextArea';
|
||||||
import Mentions from './Mentions';
|
import Mentions from './Mentions';
|
||||||
import FormatTime from './FormatTime';
|
import FormatTime from './FormatTime';
|
||||||
import Toast from './Toast';
|
import Toast from './Toast';
|
||||||
import AdminHeader from './AdminHeader';
|
|
||||||
import AccordionNav from './AccordionNav';
|
import AccordionNav from './AccordionNav';
|
||||||
import PageTitle from './PageTitle';
|
import PageTitle from './PageTitle';
|
||||||
import Empty from './Empty';
|
import Empty from './Empty';
|
||||||
import BaseUserCard from './BaseUserCard';
|
import BaseUserCard from './BaseUserCard';
|
||||||
import FollowingTags from './FollowingTags';
|
import FollowingTags from './FollowingTags';
|
||||||
import QueryGroup from './QueryGroup';
|
import QueryGroup from './QueryGroup';
|
||||||
|
import BrandUpload from './BrandUpload';
|
||||||
|
import SchemaForm, { JSONSchema, UISchema, initFormData } from './SchemaForm';
|
||||||
|
import Labels from './LabelsCard';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Avatar,
|
Avatar,
|
||||||
|
@ -47,7 +49,6 @@ export {
|
||||||
Mentions,
|
Mentions,
|
||||||
FormatTime,
|
FormatTime,
|
||||||
Toast,
|
Toast,
|
||||||
AdminHeader,
|
|
||||||
AccordionNav,
|
AccordionNav,
|
||||||
PageTitle,
|
PageTitle,
|
||||||
Empty,
|
Empty,
|
||||||
|
@ -55,5 +56,9 @@ export {
|
||||||
FollowingTags,
|
FollowingTags,
|
||||||
htmlRender,
|
htmlRender,
|
||||||
QueryGroup,
|
QueryGroup,
|
||||||
|
BrandUpload,
|
||||||
|
SchemaForm,
|
||||||
|
initFormData,
|
||||||
|
Labels,
|
||||||
};
|
};
|
||||||
export type { EditorRef };
|
export type { EditorRef, JSONSchema, UISchema };
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState } from 'react';
|
import { useLayoutEffect, useState } from 'react';
|
||||||
import { Modal, Form, Button, FormCheck } from 'react-bootstrap';
|
import { Modal, Form, Button, FormCheck } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
@ -141,69 +141,70 @@ const useChangeModal = ({ callback }: Props) => {
|
||||||
setDefaultType(params.type);
|
setDefaultType(params.type);
|
||||||
setShow(true);
|
setShow(true);
|
||||||
};
|
};
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
root.render(
|
||||||
|
<Modal show={show} onHide={onClose}>
|
||||||
|
<Modal.Header closeButton>
|
||||||
|
<Modal.Title as="h5">{t('title')}</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<Form>
|
||||||
|
{list.map((item) => {
|
||||||
|
if (
|
||||||
|
defaultType === 'inactive' &&
|
||||||
|
(item.type === 'suspended' || item.type === 'deleted')
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
root.render(
|
if (defaultType === 'suspended' && item.type === 'inactive') {
|
||||||
<Modal show={show} onHide={onClose}>
|
return null;
|
||||||
<Modal.Header closeButton>
|
}
|
||||||
<Modal.Title as="h5">{t('title')}</Modal.Title>
|
return (
|
||||||
</Modal.Header>
|
<div key={item?.type}>
|
||||||
<Modal.Body>
|
<Form.Group
|
||||||
<Form>
|
controlId={item.type}
|
||||||
{list.map((item) => {
|
className={`${
|
||||||
if (
|
item.have_content && changeType === item.type
|
||||||
defaultType === 'inactive' &&
|
? 'mb-2'
|
||||||
(item.type === 'suspended' || item.type === 'deleted')
|
: 'mb-3'
|
||||||
) {
|
}`}>
|
||||||
return null;
|
<FormCheck>
|
||||||
}
|
<FormCheck.Input
|
||||||
|
id={item.type}
|
||||||
if (defaultType === 'suspended' && item.type === 'inactive') {
|
type="radio"
|
||||||
return null;
|
checked={changeType.type === item.type}
|
||||||
}
|
onChange={() => handleRadio(item)}
|
||||||
return (
|
isInvalid={isInvalid}
|
||||||
<div key={item?.type}>
|
/>
|
||||||
<Form.Group
|
<FormCheck.Label htmlFor={item.type}>
|
||||||
controlId={item.type}
|
<span className="fw-bold">{item?.name}</span>
|
||||||
className={`${
|
<br />
|
||||||
item.have_content && changeType === item.type
|
<span className="text-secondary">
|
||||||
? 'mb-2'
|
{item?.description}
|
||||||
: 'mb-3'
|
</span>
|
||||||
}`}>
|
</FormCheck.Label>
|
||||||
<FormCheck>
|
<Form.Control.Feedback type="invalid">
|
||||||
<FormCheck.Input
|
{t('msg.empty')}
|
||||||
id={item.type}
|
</Form.Control.Feedback>
|
||||||
type="radio"
|
</FormCheck>
|
||||||
checked={changeType.type === item.type}
|
</Form.Group>
|
||||||
onChange={() => handleRadio(item)}
|
</div>
|
||||||
isInvalid={isInvalid}
|
);
|
||||||
/>
|
})}
|
||||||
<FormCheck.Label htmlFor={item.type}>
|
</Form>
|
||||||
<span className="fw-bold">{item?.name}</span>
|
</Modal.Body>
|
||||||
<br />
|
<Modal.Footer>
|
||||||
<span className="text-secondary">
|
<Button variant="link" onClick={() => onClose()}>
|
||||||
{item?.description}
|
{t('btn_cancel')}
|
||||||
</span>
|
</Button>
|
||||||
</FormCheck.Label>
|
<Button variant="primary" onClick={handleSubmit}>
|
||||||
<Form.Control.Feedback type="invalid">
|
{t('btn_submit')}
|
||||||
{t('msg.empty')}
|
</Button>
|
||||||
</Form.Control.Feedback>
|
</Modal.Footer>
|
||||||
</FormCheck>
|
</Modal>,
|
||||||
</Form.Group>
|
);
|
||||||
</div>
|
});
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Form>
|
|
||||||
</Modal.Body>
|
|
||||||
<Modal.Footer>
|
|
||||||
<Button variant="link" onClick={() => onClose()}>
|
|
||||||
{t('btn_cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button variant="primary" onClick={handleSubmit}>
|
|
||||||
{t('btn_submit')}
|
|
||||||
</Button>
|
|
||||||
</Modal.Footer>
|
|
||||||
</Modal>,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onClose,
|
onClose,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState } from 'react';
|
import { useLayoutEffect, useState } from 'react';
|
||||||
import { Modal, Form, Button, FormCheck } from 'react-bootstrap';
|
import { Modal, Form, Button, FormCheck } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
@ -73,56 +73,57 @@ const useEditStatusModal = ({
|
||||||
setDefaultType(params.type);
|
setDefaultType(params.type);
|
||||||
setShow(true);
|
setShow(true);
|
||||||
};
|
};
|
||||||
|
useLayoutEffect(() => {
|
||||||
root.render(
|
root.render(
|
||||||
<Modal show={show} onHide={onClose}>
|
<Modal show={show} onHide={onClose}>
|
||||||
<Modal.Header closeButton>
|
<Modal.Header closeButton>
|
||||||
<Modal.Title as="h5">{t('title', { type: editType })}</Modal.Title>
|
<Modal.Title as="h5">{t('title', { type: editType })}</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<Form>
|
<Form>
|
||||||
{list.map((item) => {
|
{list.map((item) => {
|
||||||
if (editType === 'answer' && item.type === 'closed') {
|
if (editType === 'answer' && item.type === 'closed') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div key={item?.type}>
|
<div key={item?.type}>
|
||||||
<Form.Group controlId={item.type} className="mb-3">
|
<Form.Group controlId={item.type} className="mb-3">
|
||||||
<FormCheck>
|
<FormCheck>
|
||||||
<FormCheck.Input
|
<FormCheck.Input
|
||||||
id={item.type}
|
id={item.type}
|
||||||
type="radio"
|
type="radio"
|
||||||
checked={changeType === item.type}
|
checked={changeType === item.type}
|
||||||
onChange={() => handleRadio(item)}
|
onChange={() => handleRadio(item)}
|
||||||
isInvalid={isInvalid}
|
isInvalid={isInvalid}
|
||||||
/>
|
/>
|
||||||
<FormCheck.Label htmlFor={item.type}>
|
<FormCheck.Label htmlFor={item.type}>
|
||||||
<span className="fw-bold">{item.name}</span>
|
<span className="fw-bold">{item.name}</span>
|
||||||
<br />
|
<br />
|
||||||
<span className="fs-14 text-secondary">
|
<span className="fs-14 text-secondary">
|
||||||
{item.description}
|
{item.description}
|
||||||
</span>
|
</span>
|
||||||
</FormCheck.Label>
|
</FormCheck.Label>
|
||||||
<Form.Control.Feedback type="invalid">
|
<Form.Control.Feedback type="invalid">
|
||||||
{t('msg.empty')}
|
{t('msg.empty')}
|
||||||
</Form.Control.Feedback>
|
</Form.Control.Feedback>
|
||||||
</FormCheck>
|
</FormCheck>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Form>
|
</Form>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button variant="link" onClick={() => onClose()}>
|
<Button variant="link" onClick={() => onClose()}>
|
||||||
{t('btn_cancel')}
|
{t('btn_cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" onClick={handleSubmit}>
|
<Button variant="primary" onClick={handleSubmit}>
|
||||||
{changeType !== 'normal' ? t('btn_next') : t('btn_submit')}
|
{changeType !== 'normal' ? t('btn_next') : t('btn_submit')}
|
||||||
</Button>
|
</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>,
|
</Modal>,
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onClose,
|
onClose,
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
import { initReactI18next } from 'react-i18next';
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
|
||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import Backend from 'i18next-http-backend';
|
|
||||||
import en_US from '@i18n/en_US.yaml';
|
import en_US from '@i18n/en_US.yaml';
|
||||||
import zh_CN from '@i18n/zh_CN.yaml';
|
import zh_CN from '@i18n/zh_CN.yaml';
|
||||||
|
|
||||||
import { DEFAULT_LANG } from '@/common/constants';
|
import { DEFAULT_LANG } from '@/common/constants';
|
||||||
|
|
||||||
i18next
|
i18next
|
||||||
// load translation using http
|
|
||||||
.use(Backend)
|
|
||||||
// pass the i18n instance to react-i18next.
|
// pass the i18n instance to react-i18next.
|
||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
.init({
|
.init({
|
||||||
|
@ -31,12 +28,6 @@ i18next
|
||||||
// allow <br/> and simple html elements in translations
|
// allow <br/> and simple html elements in translations
|
||||||
transKeepBasicHtmlNodesFor: ['br', 'strong', 'i'],
|
transKeepBasicHtmlNodesFor: ['br', 'strong', 'i'],
|
||||||
},
|
},
|
||||||
// backend: {
|
|
||||||
// loadPath: (lngs, namespace) => {
|
|
||||||
// console.log(lngs, namespace);
|
|
||||||
// return 'https://cdn.jsdelivr.net/npm/echarts@4.8.0/map/js/china.js';
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default i18next;
|
export default i18next;
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
@import '~bootstrap/scss/bootstrap';
|
@import '~bootstrap/scss/bootstrap';
|
||||||
@import '~bootstrap-icons';
|
@import '~bootstrap-icons';
|
||||||
|
|
||||||
|
.bg-gray-300 {
|
||||||
|
background-color: $gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
.focus {
|
.focus {
|
||||||
color: $input-focus-color !important;
|
color: $input-focus-color !important;
|
||||||
background-color: $input-focus-bg !important;
|
background-color: $input-focus-bg !important;
|
||||||
|
@ -65,11 +69,32 @@ a {
|
||||||
padding: 1px 0.5rem 2px;
|
padding: 1px 0.5rem 2px;
|
||||||
color: $blue-700;
|
color: $blue-700;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
border: 1px solid rgba($blue-100, 0.5);
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba($blue-100, 1);
|
background: rgba($blue-100, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-tag-required {
|
||||||
|
background: rgba($gray-200, 0.5);
|
||||||
|
color: $gray-700;
|
||||||
|
border: 1px solid $gray-400;
|
||||||
|
&:hover {
|
||||||
|
color: $gray-700;
|
||||||
|
background: rgba($gray-400, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-tag-reserved {
|
||||||
|
background: rgba($orange-100, 0.5);
|
||||||
|
color: $orange-700;
|
||||||
|
border: 1px solid $orange-400;
|
||||||
|
&:hover {
|
||||||
|
color: $orange-700;
|
||||||
|
background: rgba($orange-400, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.divide-line {
|
.divide-line {
|
||||||
border-bottom: 1px solid rgba(33, 37, 41, 0.25);
|
border-bottom: 1px solid rgba(33, 37, 41, 0.25);
|
||||||
}
|
}
|
||||||
|
@ -143,6 +168,7 @@ a {
|
||||||
|
|
||||||
.fit-content {
|
.fit-content {
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
width: fit-content;
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,3 +236,16 @@ a {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-img-wrap {
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 128px;
|
||||||
|
}
|
||||||
|
.badge-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 1px 0.5rem 2px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
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 { brandingStore } from '@/stores';
|
||||||
|
import { useToast } from '@/hooks';
|
||||||
|
|
||||||
|
const uploadType = 'branding';
|
||||||
|
const Index: FC = () => {
|
||||||
|
const { t } = useTranslation('translation', {
|
||||||
|
keyPrefix: 'admin.branding',
|
||||||
|
});
|
||||||
|
const { branding: brandingInfo, update } = brandingStore();
|
||||||
|
const Toast = useToast();
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<FormDataType>({
|
||||||
|
logo: {
|
||||||
|
value: brandingInfo.logo,
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
mobile_logo: {
|
||||||
|
value: '',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
square_icon: {
|
||||||
|
value: '',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
favicon: {
|
||||||
|
value: '',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const schema: JSONSchema = {
|
||||||
|
title: t('page_title'),
|
||||||
|
properties: {
|
||||||
|
logo: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('logo.label'),
|
||||||
|
description: t('logo.text'),
|
||||||
|
},
|
||||||
|
mobile_logo: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('mobile_logo.label'),
|
||||||
|
description: t('mobile_logo.text'),
|
||||||
|
},
|
||||||
|
square_icon: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('square_icon.label'),
|
||||||
|
description: t('square_icon.text'),
|
||||||
|
},
|
||||||
|
favicon: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('favicon.label'),
|
||||||
|
description: t('favicon.text'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const uiSchema: UISchema = {
|
||||||
|
logo: {
|
||||||
|
'ui:widget': 'upload',
|
||||||
|
'ui:options': {
|
||||||
|
imageType: uploadType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mobile_logo: {
|
||||||
|
'ui:widget': 'upload',
|
||||||
|
'ui:options': {
|
||||||
|
imageType: uploadType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
square_icon: {
|
||||||
|
'ui:widget': 'upload',
|
||||||
|
'ui:options': {
|
||||||
|
imageType: uploadType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
favicon: {
|
||||||
|
'ui:widget': 'upload',
|
||||||
|
'ui:options': {
|
||||||
|
acceptType: ',image/x-icon,image/vnd.microsoft.icon',
|
||||||
|
imageType: uploadType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnChange = (data) => {
|
||||||
|
setFormData(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
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);
|
||||||
|
update(params);
|
||||||
|
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>
|
||||||
|
<SchemaForm
|
||||||
|
schema={schema}
|
||||||
|
uiSchema={uiSchema}
|
||||||
|
formData={formData}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Index);
|
|
@ -1,11 +1,12 @@
|
||||||
import React, { FC, useEffect, useState } from 'react';
|
import React, { FC, useEffect, useState } from 'react';
|
||||||
import { Form, Button } from 'react-bootstrap';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
|
||||||
import type * as Type from '@/common/interface';
|
import type * as Type from '@/common/interface';
|
||||||
import { useToast } from '@/hooks';
|
import { useToast } from '@/hooks';
|
||||||
import { siteInfoStore } from '@/stores';
|
import { siteInfoStore } from '@/stores';
|
||||||
import { useGeneralSetting, updateGeneralSetting } from '@/services';
|
import { useGeneralSetting, updateGeneralSetting } from '@/services';
|
||||||
|
import Pattern from '@/common/pattern';
|
||||||
|
|
||||||
import '../index.scss';
|
import '../index.scss';
|
||||||
|
|
||||||
|
@ -17,91 +18,77 @@ const General: FC = () => {
|
||||||
const updateSiteInfo = siteInfoStore((state) => state.update);
|
const updateSiteInfo = siteInfoStore((state) => state.update);
|
||||||
|
|
||||||
const { data: setting } = useGeneralSetting();
|
const { data: setting } = useGeneralSetting();
|
||||||
const [formData, setFormData] = useState<Type.FormDataType>({
|
const schema: JSONSchema = {
|
||||||
name: {
|
title: t('page_title'),
|
||||||
value: '',
|
required: ['name', 'site_url', 'contact_email'],
|
||||||
isInvalid: false,
|
properties: {
|
||||||
errorMsg: '',
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('name.label'),
|
||||||
|
description: t('name.text'),
|
||||||
|
},
|
||||||
|
site_url: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('site_url.label'),
|
||||||
|
description: t('site_url.text'),
|
||||||
|
},
|
||||||
|
short_description: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('short_description.label'),
|
||||||
|
description: t('short_description.text'),
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('description.label'),
|
||||||
|
description: t('description.text'),
|
||||||
|
},
|
||||||
|
contact_email: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('contact_email.label'),
|
||||||
|
description: t('contact_email.text'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
const uiSchema: UISchema = {
|
||||||
site_url: {
|
site_url: {
|
||||||
value: '',
|
'ui:options': {
|
||||||
isInvalid: false,
|
validator: (value) => {
|
||||||
errorMsg: '',
|
let url: URL | undefined;
|
||||||
},
|
try {
|
||||||
short_description: {
|
url = new URL(value);
|
||||||
value: '',
|
} catch (ex) {
|
||||||
isInvalid: false,
|
return t('site_url.validate');
|
||||||
errorMsg: '',
|
}
|
||||||
},
|
if (
|
||||||
description: {
|
!url ||
|
||||||
value: '',
|
/^https?:$/.test(url.protocol) === false ||
|
||||||
isInvalid: false,
|
url.pathname !== '/' ||
|
||||||
errorMsg: '',
|
url.search !== '' ||
|
||||||
|
url.hash !== ''
|
||||||
|
) {
|
||||||
|
return t('site_url.validate');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
contact_email: {
|
contact_email: {
|
||||||
value: '',
|
'ui:options': {
|
||||||
isInvalid: false,
|
validator: (value) => {
|
||||||
errorMsg: '',
|
if (!Pattern.email.test(value)) {
|
||||||
|
return t('contact_email.validate');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
|
||||||
const checkValidated = (): boolean => {
|
|
||||||
let ret = true;
|
|
||||||
const { name, site_url, contact_email } = formData;
|
|
||||||
if (!name.value) {
|
|
||||||
ret = false;
|
|
||||||
formData.name = {
|
|
||||||
value: '',
|
|
||||||
isInvalid: true,
|
|
||||||
errorMsg: t('name.msg'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!site_url.value) {
|
|
||||||
ret = false;
|
|
||||||
formData.site_url = {
|
|
||||||
value: '',
|
|
||||||
isInvalid: true,
|
|
||||||
errorMsg: t('site_url.msg'),
|
|
||||||
};
|
|
||||||
} else if (!/^(https?):\/\/([\w.]+\/?)\S*$/.test(site_url.value)) {
|
|
||||||
ret = false;
|
|
||||||
formData.site_url = {
|
|
||||||
value: formData.site_url.value,
|
|
||||||
isInvalid: true,
|
|
||||||
errorMsg: t('site_url.validate'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!contact_email.value) {
|
|
||||||
ret = false;
|
|
||||||
formData.contact_email = {
|
|
||||||
value: '',
|
|
||||||
isInvalid: true,
|
|
||||||
errorMsg: t('contact_email.msg'),
|
|
||||||
};
|
|
||||||
} else if (
|
|
||||||
!/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(
|
|
||||||
contact_email.value,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
ret = false;
|
|
||||||
formData.contact_email = {
|
|
||||||
value: formData.contact_email.value,
|
|
||||||
isInvalid: true,
|
|
||||||
errorMsg: t('contact_email.validate'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
});
|
|
||||||
return ret;
|
|
||||||
};
|
};
|
||||||
|
const [formData, setFormData] = useState(initFormData(schema));
|
||||||
|
|
||||||
const onSubmit = (evt) => {
|
const onSubmit = (evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
if (checkValidated() === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const reqParams: Type.AdminSettingsGeneral = {
|
const reqParams: Type.AdminSettingsGeneral = {
|
||||||
name: formData.name.value,
|
name: formData.name.value,
|
||||||
description: formData.description.value,
|
description: formData.description.value,
|
||||||
|
@ -126,19 +113,7 @@ const General: FC = () => {
|
||||||
setFormData({ ...formData });
|
setFormData({ ...formData });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const onFieldChange = (fieldName, fieldValue) => {
|
|
||||||
if (!formData[fieldName]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const fieldData: Type.FormDataType = {
|
|
||||||
[fieldName]: {
|
|
||||||
value: fieldValue,
|
|
||||||
isInvalid: false,
|
|
||||||
errorMsg: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
setFormData({ ...formData, ...fieldData });
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!setting) {
|
if (!setting) {
|
||||||
return;
|
return;
|
||||||
|
@ -149,87 +124,21 @@ const General: FC = () => {
|
||||||
});
|
});
|
||||||
setFormData({ ...formData, ...formMeta });
|
setFormData({ ...formData, ...formMeta });
|
||||||
}, [setting]);
|
}, [setting]);
|
||||||
|
|
||||||
|
const handleOnChange = (data) => {
|
||||||
|
setFormData(data);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3 className="mb-4">{t('page_title')}</h3>
|
<h3 className="mb-4">{t('page_title')}</h3>
|
||||||
<Form noValidate onSubmit={onSubmit}>
|
<SchemaForm
|
||||||
<Form.Group controlId="siteName" className="mb-3">
|
schema={schema}
|
||||||
<Form.Label>{t('name.label')}</Form.Label>
|
formData={formData}
|
||||||
<Form.Control
|
onSubmit={onSubmit}
|
||||||
required
|
uiSchema={uiSchema}
|
||||||
type="text"
|
onChange={handleOnChange}
|
||||||
value={formData.name.value}
|
/>
|
||||||
isInvalid={formData.name.isInvalid}
|
|
||||||
onChange={(evt) => onFieldChange('name', evt.target.value)}
|
|
||||||
/>
|
|
||||||
<Form.Text as="div">{t('name.text')}</Form.Text>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.name.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="siteUrl" className="mb-3">
|
|
||||||
<Form.Label>{t('site_url.label')}</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
required
|
|
||||||
type="text"
|
|
||||||
value={formData.site_url.value}
|
|
||||||
isInvalid={formData.site_url.isInvalid}
|
|
||||||
onChange={(evt) => onFieldChange('site_url', evt.target.value)}
|
|
||||||
/>
|
|
||||||
<Form.Text as="div">{t('site_url.text')}</Form.Text>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.site_url.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="shortDescription" className="mb-3">
|
|
||||||
<Form.Label>{t('short_description.label')}</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
required
|
|
||||||
type="text"
|
|
||||||
value={formData.short_description.value}
|
|
||||||
isInvalid={formData.short_description.isInvalid}
|
|
||||||
onChange={(evt) =>
|
|
||||||
onFieldChange('short_description', evt.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Form.Text as="div">{t('short_description.text')}</Form.Text>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.short_description.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="description" className="mb-3">
|
|
||||||
<Form.Label>{t('description.label')}</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
required
|
|
||||||
type="text"
|
|
||||||
value={formData.description.value}
|
|
||||||
isInvalid={formData.description.isInvalid}
|
|
||||||
onChange={(evt) => onFieldChange('description', evt.target.value)}
|
|
||||||
/>
|
|
||||||
<Form.Text as="div">{t('description.text')}</Form.Text>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.description.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="contact_email" className="mb-3">
|
|
||||||
<Form.Label>{t('contact_email.label')}</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
required
|
|
||||||
type="text"
|
|
||||||
value={formData.contact_email.value}
|
|
||||||
isInvalid={formData.contact_email.isInvalid}
|
|
||||||
onChange={(evt) => onFieldChange('contact_email', evt.target.value)}
|
|
||||||
/>
|
|
||||||
<Form.Text as="div">{t('contact_email.text')}</Form.Text>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.contact_email.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Button variant="primary" type="submit">
|
|
||||||
{t('save', { keyPrefix: 'btns' })}
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React, { FC, FormEvent, useEffect, useState } from 'react';
|
import { FC, FormEvent, useEffect, useState } from 'react';
|
||||||
import { Form, Button, Image, Stack } from 'react-bootstrap';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import { useToast } from '@/hooks';
|
import { useToast } from '@/hooks';
|
||||||
import {
|
import {
|
||||||
|
@ -9,10 +8,9 @@ import {
|
||||||
AdminSettingsInterface,
|
AdminSettingsInterface,
|
||||||
} from '@/common/interface';
|
} from '@/common/interface';
|
||||||
import { interfaceStore } from '@/stores';
|
import { interfaceStore } from '@/stores';
|
||||||
import { UploadImg } from '@/components';
|
import { JSONSchema, SchemaForm, UISchema } from '@/components';
|
||||||
import { TIMEZONES, DEFAULT_TIMEZONE } from '@/common/constants';
|
import { DEFAULT_TIMEZONE } from '@/common/constants';
|
||||||
import {
|
import {
|
||||||
uploadAvatar,
|
|
||||||
updateInterfaceSetting,
|
updateInterfaceSetting,
|
||||||
useInterfaceSetting,
|
useInterfaceSetting,
|
||||||
useThemeOptions,
|
useThemeOptions,
|
||||||
|
@ -33,12 +31,32 @@ const Interface: FC = () => {
|
||||||
const [langs, setLangs] = useState<LangsType[]>();
|
const [langs, setLangs] = useState<LangsType[]>();
|
||||||
const { data: setting } = useInterfaceSetting();
|
const { data: setting } = useInterfaceSetting();
|
||||||
|
|
||||||
const [formData, setFormData] = useState<FormDataType>({
|
const schema: JSONSchema = {
|
||||||
logo: {
|
title: t('page_title'),
|
||||||
value: setting?.logo || storeInterface.logo,
|
properties: {
|
||||||
isInvalid: false,
|
theme: {
|
||||||
errorMsg: '',
|
type: 'string',
|
||||||
|
title: t('theme.label'),
|
||||||
|
description: t('theme.text'),
|
||||||
|
enum: themes?.map((theme) => theme.value) || [],
|
||||||
|
enumNames: themes?.map((theme) => theme.label) || [],
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('language.label'),
|
||||||
|
description: t('language.text'),
|
||||||
|
enum: langs?.map((lang) => lang.value),
|
||||||
|
enumNames: langs?.map((lang) => lang.label),
|
||||||
|
},
|
||||||
|
time_zone: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('time_zone.label'),
|
||||||
|
description: t('time_zone.text'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<FormDataType>({
|
||||||
theme: {
|
theme: {
|
||||||
value: setting?.theme || storeInterface.theme,
|
value: setting?.theme || storeInterface.theme,
|
||||||
isInvalid: false,
|
isInvalid: false,
|
||||||
|
@ -55,6 +73,31 @@ const Interface: FC = () => {
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// const onChange = (fieldName, fieldValue) => {
|
||||||
|
// if (!formData[fieldName]) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// const fieldData: FormDataType = {
|
||||||
|
// [fieldName]: {
|
||||||
|
// value: fieldValue,
|
||||||
|
// isInvalid: false,
|
||||||
|
// errorMsg: '',
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// setFormData({ ...formData, ...fieldData });
|
||||||
|
// };
|
||||||
|
const uiSchema: UISchema = {
|
||||||
|
theme: {
|
||||||
|
'ui:widget': 'select',
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
'ui:widget': 'select',
|
||||||
|
},
|
||||||
|
time_zone: {
|
||||||
|
'ui:widget': 'timezone',
|
||||||
|
},
|
||||||
|
};
|
||||||
const getLangs = async () => {
|
const getLangs = async () => {
|
||||||
const res: LangsType[] = await loadLanguageOptions(true);
|
const res: LangsType[] = await loadLanguageOptions(true);
|
||||||
setLangs(res);
|
setLangs(res);
|
||||||
|
@ -103,7 +146,6 @@ const Interface: FC = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const reqParams: AdminSettingsInterface = {
|
const reqParams: AdminSettingsInterface = {
|
||||||
logo: formData.logo.value,
|
|
||||||
theme: formData.theme.value,
|
theme: formData.theme.value,
|
||||||
language: formData.language.value,
|
language: formData.language.value,
|
||||||
time_zone: formData.time_zone.value,
|
time_zone: formData.time_zone.value,
|
||||||
|
@ -111,13 +153,13 @@ const Interface: FC = () => {
|
||||||
|
|
||||||
updateInterfaceSetting(reqParams)
|
updateInterfaceSetting(reqParams)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
interfaceStore.getState().update(reqParams);
|
||||||
|
setupAppLanguage();
|
||||||
|
setupAppTimeZone();
|
||||||
Toast.onShow({
|
Toast.onShow({
|
||||||
msg: t('update', { keyPrefix: 'toast' }),
|
msg: t('update', { keyPrefix: 'toast' }),
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
});
|
});
|
||||||
interfaceStore.getState().update(reqParams);
|
|
||||||
setupAppLanguage();
|
|
||||||
setupAppTimeZone();
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (err.isError && err.key) {
|
if (err.isError && err.key) {
|
||||||
|
@ -127,34 +169,22 @@ const Interface: FC = () => {
|
||||||
setFormData({ ...formData });
|
setFormData({ ...formData });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const imgUpload = (file: any) => {
|
// const imgUpload = (file: any) => {
|
||||||
return new Promise((resolve) => {
|
// return new Promise((resolve) => {
|
||||||
uploadAvatar(file).then((res) => {
|
// uploadAvatar(file).then((res) => {
|
||||||
setFormData({
|
// setFormData({
|
||||||
...formData,
|
// ...formData,
|
||||||
logo: {
|
// logo: {
|
||||||
value: res,
|
// value: res,
|
||||||
isInvalid: false,
|
// isInvalid: false,
|
||||||
errorMsg: '',
|
// errorMsg: '',
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
resolve(true);
|
// resolve(true);
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
};
|
// };
|
||||||
const onChange = (fieldName, fieldValue) => {
|
|
||||||
if (!formData[fieldName]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const fieldData: FormDataType = {
|
|
||||||
[fieldName]: {
|
|
||||||
value: fieldValue,
|
|
||||||
isInvalid: false,
|
|
||||||
errorMsg: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
setFormData({ ...formData, ...fieldData });
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (setting) {
|
if (setting) {
|
||||||
const formMeta = {};
|
const formMeta = {};
|
||||||
|
@ -167,10 +197,21 @@ const Interface: FC = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getLangs();
|
getLangs();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleOnChange = (data) => {
|
||||||
|
setFormData(data);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3 className="mb-4">{t('page_title')}</h3>
|
<h3 className="mb-4">{t('page_title')}</h3>
|
||||||
<Form noValidate onSubmit={onSubmit}>
|
<SchemaForm
|
||||||
|
schema={schema}
|
||||||
|
uiSchema={uiSchema}
|
||||||
|
formData={formData}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
/>
|
||||||
|
{/* <Form noValidate onSubmit={onSubmit}>
|
||||||
<Form.Group controlId="logo" className="mb-3">
|
<Form.Group controlId="logo" className="mb-3">
|
||||||
<Form.Label>{t('logo.label')}</Form.Label>
|
<Form.Label>{t('logo.label')}</Form.Label>
|
||||||
<Stack gap={2}>
|
<Stack gap={2}>
|
||||||
|
@ -187,7 +228,7 @@ const Interface: FC = () => {
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="d-inline-flex">
|
<div className="d-inline-flex">
|
||||||
<UploadImg type="logo" upload={imgUpload} />
|
<UploadImg type="logo" upload={imgUpload} className="mb-2" />
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Form.Text as="div" className="text-muted">
|
<Form.Text as="div" className="text-muted">
|
||||||
|
@ -282,7 +323,7 @@ const Interface: FC = () => {
|
||||||
<Button variant="primary" type="submit">
|
<Button variant="primary" type="submit">
|
||||||
{t('save', { keyPrefix: 'btns' })}
|
{t('save', { keyPrefix: 'btns' })}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form> */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
import React, { FC, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
|
import type * as Type from '@/common/interface';
|
||||||
|
import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
|
||||||
|
import { useToast } from '@/hooks';
|
||||||
|
import { getLegalSetting, putLegalSetting } from '@/services';
|
||||||
|
import '../index.scss';
|
||||||
|
|
||||||
|
const Legal: FC = () => {
|
||||||
|
const { t } = useTranslation('translation', {
|
||||||
|
keyPrefix: 'admin.legal',
|
||||||
|
});
|
||||||
|
const Toast = useToast();
|
||||||
|
|
||||||
|
const schema: JSONSchema = {
|
||||||
|
title: t('page_title'),
|
||||||
|
required: ['terms_of_service', 'privacy_policy'],
|
||||||
|
properties: {
|
||||||
|
terms_of_service: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('terms_of_service.label'),
|
||||||
|
description: t('terms_of_service.text'),
|
||||||
|
},
|
||||||
|
privacy_policy: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('privacy_policy.label'),
|
||||||
|
description: t('privacy_policy.text'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const uiSchema: UISchema = {
|
||||||
|
terms_of_service: {
|
||||||
|
'ui:widget': 'textarea',
|
||||||
|
'ui:options': {
|
||||||
|
rows: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
privacy_policy: {
|
||||||
|
'ui:widget': 'textarea',
|
||||||
|
'ui:options': {
|
||||||
|
rows: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const [formData, setFormData] = useState(initFormData(schema));
|
||||||
|
|
||||||
|
const onSubmit = (evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
|
||||||
|
const reqParams: Type.AdminSettingsLegal = {
|
||||||
|
terms_of_service_original_text: formData.terms_of_service.value,
|
||||||
|
terms_of_service_parsed_text: marked.parse(
|
||||||
|
formData.terms_of_service.value,
|
||||||
|
),
|
||||||
|
privacy_policy_original_text: formData.privacy_policy.value,
|
||||||
|
privacy_policy_parsed_text: marked.parse(formData.privacy_policy.value),
|
||||||
|
};
|
||||||
|
|
||||||
|
putLegalSetting(reqParams)
|
||||||
|
.then(() => {
|
||||||
|
Toast.onShow({
|
||||||
|
msg: t('update', { keyPrefix: 'toast' }),
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.isError && err.key) {
|
||||||
|
formData[err.key].isInvalid = true;
|
||||||
|
formData[err.key].errorMsg = err.value;
|
||||||
|
}
|
||||||
|
setFormData({ ...formData });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getLegalSetting().then((setting) => {
|
||||||
|
const formMeta = { ...formData };
|
||||||
|
formMeta.terms_of_service.value = setting.terms_of_service_original_text;
|
||||||
|
formMeta.privacy_policy.value = setting.privacy_policy_original_text;
|
||||||
|
|
||||||
|
setFormData(formMeta);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOnChange = (data) => {
|
||||||
|
setFormData(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="mb-4">{t('page_title')}</h3>
|
||||||
|
<SchemaForm
|
||||||
|
schema={schema}
|
||||||
|
formData={formData}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
uiSchema={uiSchema}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Legal;
|
|
@ -15,11 +15,7 @@ import {
|
||||||
import { ADMIN_LIST_STATUS } from '@/common/constants';
|
import { ADMIN_LIST_STATUS } from '@/common/constants';
|
||||||
import { useEditStatusModal, useReportModal } from '@/hooks';
|
import { useEditStatusModal, useReportModal } from '@/hooks';
|
||||||
import * as Type from '@/common/interface';
|
import * as Type from '@/common/interface';
|
||||||
import {
|
import { useQuestionSearch, changeQuestionStatus } from '@/services';
|
||||||
useQuestionSearch,
|
|
||||||
changeQuestionStatus,
|
|
||||||
deleteQuestion,
|
|
||||||
} from '@/services';
|
|
||||||
|
|
||||||
import '../index.scss';
|
import '../index.scss';
|
||||||
|
|
||||||
|
@ -76,9 +72,7 @@ const Questions: FC = () => {
|
||||||
confirmBtnVariant: 'danger',
|
confirmBtnVariant: 'danger',
|
||||||
confirmText: t('delete', { keyPrefix: 'btns' }),
|
confirmText: t('delete', { keyPrefix: 'btns' }),
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
deleteQuestion({
|
changeQuestionStatus(id, 'deleted').then(() => {
|
||||||
id,
|
|
||||||
}).then(() => {
|
|
||||||
refreshList();
|
refreshList();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import React, { FC, useEffect, useState } from 'react';
|
import React, { FC, useEffect, useState } from 'react';
|
||||||
import { Form, Button, Stack } from 'react-bootstrap';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import type * as Type from '@/common/interface';
|
import type * as Type from '@/common/interface';
|
||||||
import { useToast } from '@/hooks';
|
import { useToast } from '@/hooks';
|
||||||
import { useSmtpSetting, updateSmtpSetting } from '@/services';
|
import { useSmtpSetting, updateSmtpSetting } from '@/services';
|
||||||
import pattern from '@/common/pattern';
|
import pattern from '@/common/pattern';
|
||||||
|
import { SchemaForm, JSONSchema, UISchema } from '@/components';
|
||||||
|
import { initFormData } from '../../../components/SchemaForm/index';
|
||||||
|
|
||||||
const Smtp: FC = () => {
|
const Smtp: FC = () => {
|
||||||
const { t } = useTranslation('translation', {
|
const { t } = useTranslation('translation', {
|
||||||
|
@ -13,90 +14,100 @@ const Smtp: FC = () => {
|
||||||
});
|
});
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const { data: setting } = useSmtpSetting();
|
const { data: setting } = useSmtpSetting();
|
||||||
const [formData, setFormData] = useState<Type.FormDataType>({
|
const schema: JSONSchema = {
|
||||||
from_email: {
|
title: t('page_title'),
|
||||||
value: '',
|
properties: {
|
||||||
isInvalid: false,
|
from_email: {
|
||||||
errorMsg: '',
|
type: 'string',
|
||||||
},
|
title: t('from_email.label'),
|
||||||
from_name: {
|
description: t('from_email.text'),
|
||||||
value: '',
|
},
|
||||||
isInvalid: false,
|
from_name: {
|
||||||
errorMsg: '',
|
type: 'string',
|
||||||
},
|
title: t('from_name.label'),
|
||||||
smtp_host: {
|
description: t('from_name.text'),
|
||||||
value: '',
|
},
|
||||||
isInvalid: false,
|
smtp_host: {
|
||||||
errorMsg: '',
|
type: 'string',
|
||||||
|
title: t('smtp_host.label'),
|
||||||
|
description: t('smtp_host.text'),
|
||||||
|
},
|
||||||
|
encryption: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: t('encryption.label'),
|
||||||
|
description: t('encryption.text'),
|
||||||
|
enum: [true, false],
|
||||||
|
enumNames: ['SSL', ''],
|
||||||
|
},
|
||||||
|
smtp_port: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('smtp_port.label'),
|
||||||
|
description: t('smtp_port.text'),
|
||||||
|
},
|
||||||
|
smtp_authentication: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: t('smtp_authentication.label'),
|
||||||
|
enum: [true, false],
|
||||||
|
enumNames: [t('smtp_authentication.yes'), t('smtp_authentication.no')],
|
||||||
|
},
|
||||||
|
smtp_username: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('smtp_username.label'),
|
||||||
|
description: t('smtp_username.text'),
|
||||||
|
},
|
||||||
|
smtp_password: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('smtp_password.label'),
|
||||||
|
description: t('smtp_password.text'),
|
||||||
|
},
|
||||||
|
test_email_recipient: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('test_email_recipient.label'),
|
||||||
|
description: t('test_email_recipient.text'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
const uiSchema: UISchema = {
|
||||||
encryption: {
|
encryption: {
|
||||||
value: '',
|
'ui:widget': 'radio',
|
||||||
isInvalid: false,
|
|
||||||
errorMsg: '',
|
|
||||||
},
|
|
||||||
smtp_port: {
|
|
||||||
value: '',
|
|
||||||
isInvalid: false,
|
|
||||||
errorMsg: '',
|
|
||||||
},
|
|
||||||
smtp_authentication: {
|
|
||||||
value: 'yes',
|
|
||||||
isInvalid: false,
|
|
||||||
errorMsg: '',
|
|
||||||
},
|
|
||||||
smtp_username: {
|
|
||||||
value: '',
|
|
||||||
isInvalid: false,
|
|
||||||
errorMsg: '',
|
|
||||||
},
|
},
|
||||||
smtp_password: {
|
smtp_password: {
|
||||||
value: '',
|
'ui:options': {
|
||||||
isInvalid: false,
|
type: 'password',
|
||||||
errorMsg: '',
|
},
|
||||||
|
},
|
||||||
|
smtp_authentication: {
|
||||||
|
'ui:widget': 'radio',
|
||||||
|
},
|
||||||
|
smtp_port: {
|
||||||
|
'ui:options': {
|
||||||
|
validator: (value) => {
|
||||||
|
if (!/^[1-9][0-9]*$/.test(value) || Number(value) > 65535) {
|
||||||
|
return t('smtp_port.msg');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
test_email_recipient: {
|
test_email_recipient: {
|
||||||
value: '',
|
'ui:options': {
|
||||||
isInvalid: false,
|
validator: (value) => {
|
||||||
errorMsg: '',
|
if (value && !pattern.email.test(value)) {
|
||||||
|
return t('test_email_recipient.msg');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
|
||||||
const checkValidated = (): boolean => {
|
|
||||||
let ret = true;
|
|
||||||
const { smtp_port, test_email_recipient } = formData;
|
|
||||||
if (
|
|
||||||
!/^[1-9][0-9]*$/.test(smtp_port.value) ||
|
|
||||||
Number(smtp_port.value) > 65535
|
|
||||||
) {
|
|
||||||
ret = false;
|
|
||||||
formData.smtp_port = {
|
|
||||||
value: smtp_port.value,
|
|
||||||
isInvalid: true,
|
|
||||||
errorMsg: t('smtp_port.msg'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
test_email_recipient.value &&
|
|
||||||
!pattern.email.test(test_email_recipient.value)
|
|
||||||
) {
|
|
||||||
ret = false;
|
|
||||||
formData.test_email_recipient = {
|
|
||||||
value: test_email_recipient.value,
|
|
||||||
isInvalid: true,
|
|
||||||
errorMsg: t('test_email_recipient.msg'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
});
|
|
||||||
return ret;
|
|
||||||
};
|
};
|
||||||
|
const [formData, setFormData] = useState<Type.FormDataType>(
|
||||||
|
initFormData(schema),
|
||||||
|
);
|
||||||
|
|
||||||
const onSubmit = (evt) => {
|
const onSubmit = (evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
if (!checkValidated()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const reqParams: Type.AdminSettingsSmtp = {
|
const reqParams: Type.AdminSettingsSmtp = {
|
||||||
from_email: formData.from_email.value,
|
from_email: formData.from_email.value,
|
||||||
from_name: formData.from_name.value,
|
from_name: formData.from_name.value,
|
||||||
|
@ -124,19 +135,7 @@ const Smtp: FC = () => {
|
||||||
setFormData({ ...formData });
|
setFormData({ ...formData });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const onFieldChange = (fieldName, fieldValue) => {
|
|
||||||
if (!formData[fieldName]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const fieldData: Type.FormDataType = {
|
|
||||||
[fieldName]: {
|
|
||||||
value: fieldValue,
|
|
||||||
isInvalid: false,
|
|
||||||
errorMsg: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
setFormData({ ...formData, ...fieldData });
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!setting) {
|
if (!setting) {
|
||||||
return;
|
return;
|
||||||
|
@ -152,166 +151,19 @@ const Smtp: FC = () => {
|
||||||
setFormData(formState);
|
setFormData(formState);
|
||||||
}, [setting]);
|
}, [setting]);
|
||||||
|
|
||||||
|
const handleOnChange = (data) => {
|
||||||
|
setFormData(data);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3 className="mb-4">{t('page_title')}</h3>
|
<h3 className="mb-4">{t('page_title')}</h3>
|
||||||
<Form noValidate onSubmit={onSubmit}>
|
<SchemaForm
|
||||||
<Form.Group controlId="fromEmail" className="mb-3">
|
schema={schema}
|
||||||
<Form.Label>{t('from_email.label')}</Form.Label>
|
uiSchema={uiSchema}
|
||||||
<Form.Control
|
formData={formData}
|
||||||
required
|
onChange={handleOnChange}
|
||||||
type="text"
|
onSubmit={onSubmit}
|
||||||
value={formData.from_email.value}
|
/>
|
||||||
isInvalid={formData.from_email.isInvalid}
|
|
||||||
onChange={(evt) => onFieldChange('from_email', evt.target.value)}
|
|
||||||
/>
|
|
||||||
<Form.Text as="div">{t('from_email.text')}</Form.Text>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.from_email.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="fromName" className="mb-3">
|
|
||||||
<Form.Label>{t('from_name.label')}</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
required
|
|
||||||
type="text"
|
|
||||||
value={formData.from_name.value}
|
|
||||||
isInvalid={formData.from_name.isInvalid}
|
|
||||||
onChange={(evt) => onFieldChange('from_name', evt.target.value)}
|
|
||||||
/>
|
|
||||||
<Form.Text as="div">{t('from_name.text')}</Form.Text>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.from_name.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="smtpHost" className="mb-3">
|
|
||||||
<Form.Label>{t('smtp_host.label')}</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
required
|
|
||||||
type="text"
|
|
||||||
value={formData.smtp_host.value}
|
|
||||||
isInvalid={formData.smtp_host.isInvalid}
|
|
||||||
onChange={(evt) => onFieldChange('smtp_host', evt.target.value)}
|
|
||||||
/>
|
|
||||||
<Form.Text as="div">{t('smtp_host.text')}</Form.Text>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.smtp_host.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="encryption" className="mb-3">
|
|
||||||
<Form.Label>{t('encryption.label')}</Form.Label>
|
|
||||||
<Stack direction="horizontal">
|
|
||||||
<Form.Check
|
|
||||||
inline
|
|
||||||
label={t('encryption.ssl')}
|
|
||||||
name="smtp_encryption"
|
|
||||||
id="smtp_encryption_ssl"
|
|
||||||
checked={formData.encryption.value === 'SSL'}
|
|
||||||
onChange={() => onFieldChange('encryption', 'SSL')}
|
|
||||||
type="radio"
|
|
||||||
/>
|
|
||||||
<Form.Check
|
|
||||||
inline
|
|
||||||
label={t('encryption.none')}
|
|
||||||
name="smtp_encryption"
|
|
||||||
id="smtp_encryption_none"
|
|
||||||
checked={!formData.encryption.value}
|
|
||||||
onChange={() => onFieldChange('encryption', '')}
|
|
||||||
type="radio"
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<Form.Text as="div">{t('encryption.text')}</Form.Text>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.encryption.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="smtpPort" className="mb-3">
|
|
||||||
<Form.Label>{t('smtp_port.label')}</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
required
|
|
||||||
type="text"
|
|
||||||
value={formData.smtp_port.value}
|
|
||||||
isInvalid={formData.smtp_port.isInvalid}
|
|
||||||
onChange={(evt) => onFieldChange('smtp_port', evt.target.value)}
|
|
||||||
/>
|
|
||||||
<Form.Text as="div">{t('smtp_port.text')}</Form.Text>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.smtp_port.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="smtpAuthentication" className="mb-3">
|
|
||||||
<Form.Label>{t('smtp_authentication.label')}</Form.Label>
|
|
||||||
<Stack direction="horizontal">
|
|
||||||
<Form.Check
|
|
||||||
inline
|
|
||||||
label={t('smtp_authentication.yes')}
|
|
||||||
name="smtp_authentication"
|
|
||||||
id="smtp_authentication_yes"
|
|
||||||
checked={!!formData.smtp_authentication.value}
|
|
||||||
onChange={() => onFieldChange('smtp_authentication', true)}
|
|
||||||
type="radio"
|
|
||||||
/>
|
|
||||||
<Form.Check
|
|
||||||
inline
|
|
||||||
label={t('smtp_authentication.no')}
|
|
||||||
name="smtp_authentication"
|
|
||||||
id="smtp_authentication_no"
|
|
||||||
checked={!formData.smtp_authentication.value}
|
|
||||||
onChange={() => onFieldChange('smtp_authentication', false)}
|
|
||||||
type="radio"
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.smtp_authentication.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="smtpUsername" className="mb-3">
|
|
||||||
<Form.Label>{t('smtp_username.label')}</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
required
|
|
||||||
type="text"
|
|
||||||
value={formData.smtp_username.value}
|
|
||||||
isInvalid={formData.smtp_username.isInvalid}
|
|
||||||
onChange={(evt) => onFieldChange('smtp_username', evt.target.value)}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.smtp_username.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="smtpPassword" className="mb-3">
|
|
||||||
<Form.Label>{t('smtp_password.label')}</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
required
|
|
||||||
type="password"
|
|
||||||
value={formData.smtp_password.value}
|
|
||||||
isInvalid={formData.smtp_password.isInvalid}
|
|
||||||
onChange={(evt) => onFieldChange('smtp_password', evt.target.value)}
|
|
||||||
/>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.smtp_password.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="testEmailRecipient" className="mb-3">
|
|
||||||
<Form.Label>{t('test_email_recipient.label')}</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
required
|
|
||||||
type="text"
|
|
||||||
value={formData.test_email_recipient.value}
|
|
||||||
isInvalid={formData.test_email_recipient.isInvalid}
|
|
||||||
onChange={(evt) =>
|
|
||||||
onFieldChange('test_email_recipient', evt.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Form.Text as="div">{t('test_email_recipient.text')}</Form.Text>
|
|
||||||
<Form.Control.Feedback type="invalid">
|
|
||||||
{formData.test_email_recipient.errorMsg}
|
|
||||||
</Form.Control.Feedback>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Button variant="primary" type="submit">
|
|
||||||
{t('save', { keyPrefix: 'btns' })}
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
import React, { FC, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
|
||||||
|
import type * as Type from '@/common/interface';
|
||||||
|
import { useToast } from '@/hooks';
|
||||||
|
import {
|
||||||
|
getRequireAndReservedTag,
|
||||||
|
postRequireAndReservedTag,
|
||||||
|
} from '@/services';
|
||||||
|
|
||||||
|
import '../index.scss';
|
||||||
|
|
||||||
|
const Legal: FC = () => {
|
||||||
|
const { t } = useTranslation('translation', {
|
||||||
|
keyPrefix: 'admin.write',
|
||||||
|
});
|
||||||
|
const Toast = useToast();
|
||||||
|
// const updateSiteInfo = siteInfoStore((state) => state.update);
|
||||||
|
|
||||||
|
const schema: JSONSchema = {
|
||||||
|
title: t('page_title'),
|
||||||
|
properties: {
|
||||||
|
recommend_tags: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('recommend_tags.label'),
|
||||||
|
description: t('recommend_tags.text'),
|
||||||
|
},
|
||||||
|
required_tag: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: t('required_tag.label'),
|
||||||
|
description: t('required_tag.text'),
|
||||||
|
},
|
||||||
|
reserved_tags: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('reserved_tags.label'),
|
||||||
|
description: t('reserved_tags.text'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const uiSchema: UISchema = {
|
||||||
|
recommend_tags: {
|
||||||
|
'ui:widget': 'textarea',
|
||||||
|
'ui:options': {
|
||||||
|
rows: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required_tag: {
|
||||||
|
'ui:widget': 'switch',
|
||||||
|
},
|
||||||
|
reserved_tags: {
|
||||||
|
'ui:widget': 'textarea',
|
||||||
|
'ui:options': {
|
||||||
|
rows: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const [formData, setFormData] = useState(initFormData(schema));
|
||||||
|
|
||||||
|
const onSubmit = (evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
|
||||||
|
const reqParams: Type.AdminSettingsWrite = {
|
||||||
|
recommend_tags: formData.recommend_tags.value.trim().split('\n'),
|
||||||
|
required_tag: formData.required_tag.value,
|
||||||
|
reserved_tags: formData.reserved_tags.value.trim().split('\n'),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(reqParams);
|
||||||
|
postRequireAndReservedTag(reqParams)
|
||||||
|
.then(() => {
|
||||||
|
Toast.onShow({
|
||||||
|
msg: t('update', { keyPrefix: 'toast' }),
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.isError && err.key) {
|
||||||
|
formData[err.key].isInvalid = true;
|
||||||
|
formData[err.key].errorMsg = err.value;
|
||||||
|
}
|
||||||
|
setFormData({ ...formData });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const initData = () => {
|
||||||
|
getRequireAndReservedTag().then((res) => {
|
||||||
|
formData.recommend_tags.value = res.recommend_tags.join('\n');
|
||||||
|
formData.required_tag.value = res.required_tag;
|
||||||
|
formData.reserved_tags.value = res.reserved_tags.join('\n');
|
||||||
|
setFormData({ ...formData });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOnChange = (data) => {
|
||||||
|
setFormData(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="mb-4">{t('page_title')}</h3>
|
||||||
|
<SchemaForm
|
||||||
|
schema={schema}
|
||||||
|
formData={formData}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
uiSchema={uiSchema}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Legal;
|
|
@ -1,25 +1,42 @@
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { Container, Row, Col } from 'react-bootstrap';
|
import { Container, Row, Col } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { AccordionNav, AdminHeader, PageTitle } from '@/components';
|
import { AccordionNav, PageTitle } from '@/components';
|
||||||
import { ADMIN_NAV_MENUS } from '@/common/constants';
|
import { ADMIN_NAV_MENUS } from '@/common/constants';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
const Dashboard: FC = () => {
|
const formPaths = [
|
||||||
|
'general',
|
||||||
|
'smtp',
|
||||||
|
'interface',
|
||||||
|
'branding',
|
||||||
|
'legal',
|
||||||
|
'write',
|
||||||
|
];
|
||||||
|
|
||||||
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
|
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle title={t('admin')} />
|
<PageTitle title={t('admin')} />
|
||||||
<AdminHeader />
|
<div className="bg-light py-2">
|
||||||
|
<Container className="py-1">
|
||||||
|
<h6 className="mb-0 fw-bold lh-base">
|
||||||
|
{t('title', { keyPrefix: 'admin.admin_header' })}
|
||||||
|
</h6>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
<Container className="admin-container">
|
<Container className="admin-container">
|
||||||
<Row>
|
<Row>
|
||||||
<Col lg={2}>
|
<Col lg={2}>
|
||||||
<AccordionNav menus={ADMIN_NAV_MENUS} />
|
<AccordionNav menus={ADMIN_NAV_MENUS} path="/admin/" />
|
||||||
</Col>
|
</Col>
|
||||||
<Col lg={10}>
|
<Col lg={formPaths.find((v) => pathname.includes(v)) ? 6 : 10}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -28,4 +45,4 @@ const Dashboard: FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Dashboard;
|
export default Index;
|
||||||
|
|
|
@ -4,12 +4,13 @@ import { Helmet, HelmetProvider } from 'react-helmet-async';
|
||||||
|
|
||||||
import { SWRConfig } from 'swr';
|
import { SWRConfig } from 'swr';
|
||||||
|
|
||||||
import { siteInfoStore, toastStore } from '@/stores';
|
import { siteInfoStore, toastStore, brandingStore } from '@/stores';
|
||||||
import { Header, Footer, Toast } from '@/components';
|
import { Header, Footer, Toast } from '@/components';
|
||||||
|
|
||||||
const Layout: FC = () => {
|
const Layout: FC = () => {
|
||||||
const { msg: toastMsg, variant, clear: toastClear } = toastStore();
|
const { msg: toastMsg, variant, clear: toastClear } = toastStore();
|
||||||
const { siteInfo } = siteInfoStore.getState();
|
const { siteInfo } = siteInfoStore.getState();
|
||||||
|
const { favicon } = brandingStore((state) => state.branding);
|
||||||
const closeToast = () => {
|
const closeToast = () => {
|
||||||
toastClear();
|
toastClear();
|
||||||
};
|
};
|
||||||
|
@ -17,6 +18,7 @@ const Layout: FC = () => {
|
||||||
return (
|
return (
|
||||||
<HelmetProvider>
|
<HelmetProvider>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
<link rel="icon" href={favicon || '/favicon.ico'} />
|
||||||
{siteInfo && <meta name="description" content={siteInfo.description} />}
|
{siteInfo && <meta name="description" content={siteInfo.description} />}
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<SWRConfig
|
<SWRConfig
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
import { useLegalPrivacy } from '@/services';
|
||||||
|
|
||||||
|
const Index: FC = () => {
|
||||||
|
const { data: privacy } = useLegalPrivacy();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fmt fs-14"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: privacy?.privacy_policy_parsed_text || '',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
import { useLegalTos } from '@/services';
|
||||||
|
|
||||||
|
const Index: FC = () => {
|
||||||
|
const { data: tos } = useLegalTos();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fmt fs-14"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: tos?.terms_of_service_parsed_text || '',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -0,0 +1,4 @@
|
||||||
|
.sub-container {
|
||||||
|
padding-top: 2rem;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { Container, Row, Col } from 'react-bootstrap';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { AccordionNav } from '@/components';
|
||||||
|
import { ADMIN_LEGAL_MENUS } from '@/common/constants';
|
||||||
|
|
||||||
|
import './index.scss';
|
||||||
|
|
||||||
|
const Index: FC = () => {
|
||||||
|
return (
|
||||||
|
<Container className="sub-container">
|
||||||
|
<Row>
|
||||||
|
<Col lg={2}>
|
||||||
|
<AccordionNav menus={ADMIN_LEGAL_MENUS} />
|
||||||
|
</Col>
|
||||||
|
<Col lg={6}>
|
||||||
|
<Outlet />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -123,7 +123,7 @@ const Ask = () => {
|
||||||
isInvalid: true,
|
isInvalid: true,
|
||||||
errorMsg: t('form.fields.title.msg.empty'),
|
errorMsg: t('form.fields.title.msg.empty'),
|
||||||
};
|
};
|
||||||
} else if ([...title.value].length > 150) {
|
} else if (Array.from(title.value).length > 150) {
|
||||||
bol = false;
|
bol = false;
|
||||||
formData.title = {
|
formData.title = {
|
||||||
value: title.value,
|
value: title.value,
|
||||||
|
|
|
@ -93,14 +93,7 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="m-n1">
|
<div className="m-n1">
|
||||||
{data?.tags?.map((item: any) => {
|
{data?.tags?.map((item: any) => {
|
||||||
return (
|
return <Tag className="m-1" key={item.slug_name} data={item} />;
|
||||||
<Tag
|
|
||||||
className="m-1"
|
|
||||||
href={`/tags/${item.main_tag_slug_name || item.slug_name}`}
|
|
||||||
key={item.slug_name}>
|
|
||||||
{item.slug_name}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<article
|
<article
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
||||||
import { Container, Row, Col } from 'react-bootstrap';
|
import { Container, Row, Col } from 'react-bootstrap';
|
||||||
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
|
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { Pagination, PageTitle } from '@/components';
|
import { Pagination, PageTitle, Labels } from '@/components';
|
||||||
import { loggedUserInfoStore } from '@/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { scrollTop } from '@/utils';
|
import { scrollTop } from '@/utils';
|
||||||
import { usePageUsers } from '@/hooks';
|
import { usePageUsers } from '@/hooks';
|
||||||
|
@ -167,6 +167,7 @@ const Index = () => {
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||||
|
<Labels className="mb-4" />
|
||||||
<RelatedQuestions id={question?.id || ''} />
|
<RelatedQuestions id={question?.id || ''} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -67,11 +67,7 @@ const Index: FC<Props> = ({ data }) => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{data.object?.tags?.map((item) => {
|
{data.object?.tags?.map((item) => {
|
||||||
return (
|
return <Tag key={item.slug_name} className="me-1" data={item} />;
|
||||||
<Tag href={`/tags/${item.slug_name}`} className="me-1">
|
|
||||||
{item.slug_name}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
);
|
);
|
||||||
|
|
|
@ -152,9 +152,17 @@ const TagIntroduction = () => {
|
||||||
<>
|
<>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
{t('synonyms.text')}{' '}
|
{t('synonyms.text')}{' '}
|
||||||
<Tag className="me-2 mb-2" href="#">
|
<Tag
|
||||||
{tagName}
|
className="me-2 mb-2"
|
||||||
</Tag>
|
href="#"
|
||||||
|
data={{
|
||||||
|
slug_name: tagName || '',
|
||||||
|
main_tag_slug_name: '',
|
||||||
|
display_name: '',
|
||||||
|
recommend: false,
|
||||||
|
reserved: false,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<TagSelector
|
<TagSelector
|
||||||
value={synonymsTags}
|
value={synonymsTags}
|
||||||
|
@ -170,9 +178,8 @@ const TagIntroduction = () => {
|
||||||
<Tag
|
<Tag
|
||||||
key={item.tag_id}
|
key={item.tag_id}
|
||||||
className="me-2 mb-2"
|
className="me-2 mb-2"
|
||||||
href={`/tags/${item.slug_name}`}>
|
data={item}
|
||||||
{item.slug_name}
|
/>
|
||||||
</Tag>
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -77,9 +77,8 @@ const Tags = () => {
|
||||||
className="mb-4">
|
className="mb-4">
|
||||||
<Card className="h-100">
|
<Card className="h-100">
|
||||||
<Card.Body className="d-flex flex-column align-items-start">
|
<Card.Body className="d-flex flex-column align-items-start">
|
||||||
<Tag className="mb-3" href={`/tags/${tag.slug_name}`}>
|
<Tag className="mb-3" data={tag} />
|
||||||
{tag.slug_name}
|
|
||||||
</Tag>
|
|
||||||
<p className="fs-14 flex-fill text-break text-wrap text-truncate-4">
|
<p className="fs-14 flex-fill text-break text-wrap text-truncate-4">
|
||||||
{tag.original_text}
|
{tag.original_text}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
import { FC, memo, useEffect } from 'react';
|
import { FC, memo, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { loggedUserInfoStore } from '@/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { getQueryString } from '@/utils';
|
|
||||||
import { activateAccount } from '@/services';
|
import { activateAccount } from '@/services';
|
||||||
import { PageTitle } from '@/components';
|
import { PageTitle } from '@/components';
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
|
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const updateUser = loggedUserInfoStore((state) => state.update);
|
const updateUser = loggedUserInfoStore((state) => state.update);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const code = getQueryString('code');
|
const code = searchParams.get('code');
|
||||||
|
|
||||||
if (code) {
|
if (code) {
|
||||||
activateAccount(encodeURIComponent(code)).then((res) => {
|
activateAccount(encodeURIComponent(code)).then((res) => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { FormEvent, useState, useEffect } from 'react';
|
import React, { FormEvent, useState, useEffect } from 'react';
|
||||||
import { Container, Form, Button, Col } from 'react-bootstrap';
|
import { Container, Form, Button, Col } from 'react-bootstrap';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
@ -10,7 +10,7 @@ import type {
|
||||||
} from '@/common/interface';
|
} from '@/common/interface';
|
||||||
import { PageTitle, Unactivate } from '@/components';
|
import { PageTitle, Unactivate } from '@/components';
|
||||||
import { loggedUserInfoStore } from '@/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { getQueryString, guard, floppyNavigation } from '@/utils';
|
import { guard, floppyNavigation } from '@/utils';
|
||||||
import { login, checkImgCode } from '@/services';
|
import { login, checkImgCode } from '@/services';
|
||||||
import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants';
|
import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants';
|
||||||
import { RouteAlias } from '@/router/alias';
|
import { RouteAlias } from '@/router/alias';
|
||||||
|
@ -20,6 +20,7 @@ import Storage from '@/utils/storage';
|
||||||
const Index: React.FC = () => {
|
const Index: React.FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'login' });
|
const { t } = useTranslation('translation', { keyPrefix: 'login' });
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const [refresh, setRefresh] = useState(0);
|
const [refresh, setRefresh] = useState(0);
|
||||||
const updateUser = loggedUserInfoStore((state) => state.update);
|
const updateUser = loggedUserInfoStore((state) => state.update);
|
||||||
const storeUser = loggedUserInfoStore((state) => state.user);
|
const storeUser = loggedUserInfoStore((state) => state.user);
|
||||||
|
@ -154,7 +155,7 @@ const Index: React.FC = () => {
|
||||||
}, [refresh]);
|
}, [refresh]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isInactive = getQueryString('status');
|
const isInactive = searchParams.get('status');
|
||||||
|
|
||||||
if ((storeUser.id && storeUser.mail_status === 2) || isInactive) {
|
if ((storeUser.id && storeUser.mail_status === 2) || isInactive) {
|
||||||
setStep(2);
|
setStep(2);
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
import React, { FormEvent, useState } from 'react';
|
import React, { FormEvent, useState } from 'react';
|
||||||
import { Container, Col, Form, Button } from 'react-bootstrap';
|
import { Container, Col, Form, Button } from 'react-bootstrap';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { loggedUserInfoStore } from '@/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { getQueryString } from '@/utils';
|
|
||||||
import type { FormDataType } from '@/common/interface';
|
import type { FormDataType } from '@/common/interface';
|
||||||
import { replacementPassword } from '@/services';
|
import { replacementPassword } from '@/services';
|
||||||
import { PageTitle } from '@/components';
|
import { PageTitle } from '@/components';
|
||||||
|
|
||||||
const Index: React.FC = () => {
|
const Index: React.FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'password_reset' });
|
const { t } = useTranslation('translation', { keyPrefix: 'password_reset' });
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
const clearUser = loggedUserInfoStore((state) => state.clear);
|
const clearUser = loggedUserInfoStore((state) => state.clear);
|
||||||
const [formData, setFormData] = useState<FormDataType>({
|
const [formData, setFormData] = useState<FormDataType>({
|
||||||
|
@ -91,7 +90,7 @@ const Index: React.FC = () => {
|
||||||
if (checkValidated() === false) {
|
if (checkValidated() === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const code = getQueryString('code');
|
const code = searchParams.get('code');
|
||||||
if (!code) {
|
if (!code) {
|
||||||
console.error('code is required');
|
console.error('code is required');
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -46,14 +46,7 @@ const Index: FC<Props> = ({ visible, data }) => {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{item.question_info?.tags?.map((tag) => {
|
{item.question_info?.tags?.map((tag) => {
|
||||||
return (
|
return <Tag key={tag.slug_name} className="me-1" data={tag} />;
|
||||||
<Tag
|
|
||||||
href={`/t/${tag.main_tag_slug_name || tag.slug_name}`}
|
|
||||||
key={tag.slug_name}
|
|
||||||
className="me-1">
|
|
||||||
{tag.slug_name}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
|
|
|
@ -73,14 +73,7 @@ const Index: FC<Props> = ({ visible, tabName, data }) => {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{item.tags?.map((tag) => {
|
{item.tags?.map((tag) => {
|
||||||
return (
|
return <Tag className="me-1" key={tag.slug_name} data={tag} />;
|
||||||
<Tag
|
|
||||||
href={`/t/${tag.main_tag_slug_name || tag.slug_name}`}
|
|
||||||
className="me-1"
|
|
||||||
key={tag.slug_name}>
|
|
||||||
{tag.slug_name}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import React, { FormEvent, useState } from 'react';
|
import React, { FormEvent, MouseEvent, useState } from 'react';
|
||||||
import { Form, Button, Col } from 'react-bootstrap';
|
import { Form, Button, Col } from 'react-bootstrap';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import type { FormDataType } from '@/common/interface';
|
import type { FormDataType } from '@/common/interface';
|
||||||
import { register } from '@/services';
|
import { register, useLegalTos, useLegalPrivacy } from '@/services';
|
||||||
import userStore from '@/stores/userInfo';
|
import userStore from '@/stores/userInfo';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -82,6 +82,26 @@ const Index: React.FC<Props> = ({ callback }) => {
|
||||||
});
|
});
|
||||||
return bol;
|
return bol;
|
||||||
};
|
};
|
||||||
|
const { data: tos } = useLegalTos();
|
||||||
|
const { data: privacy } = useLegalPrivacy();
|
||||||
|
const argumentClick = (evt: MouseEvent, type: 'tos' | 'privacy') => {
|
||||||
|
evt.stopPropagation();
|
||||||
|
const contentText =
|
||||||
|
type === 'tos'
|
||||||
|
? tos?.terms_of_service_original_text
|
||||||
|
: privacy?.privacy_policy_original_text;
|
||||||
|
let matchUrl: URL | undefined;
|
||||||
|
try {
|
||||||
|
if (contentText) {
|
||||||
|
matchUrl = new URL(contentText);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} catch (ex) {}
|
||||||
|
if (matchUrl) {
|
||||||
|
evt.preventDefault();
|
||||||
|
window.open(matchUrl.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (event: FormEvent) => {
|
const handleSubmit = async (event: FormEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -185,7 +205,29 @@ const Index: React.FC<Props> = ({ callback }) => {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
<div className="text-center fs-14 mt-3">
|
||||||
|
<Trans i18nKey="login.agreements" ns="translation">
|
||||||
|
By registering, you agree to the
|
||||||
|
<Link
|
||||||
|
to="/privacy"
|
||||||
|
onClick={(evt) => {
|
||||||
|
argumentClick(evt, 'privacy');
|
||||||
|
}}
|
||||||
|
target="_blank">
|
||||||
|
privacy policy
|
||||||
|
</Link>
|
||||||
|
and
|
||||||
|
<Link
|
||||||
|
to="/tos"
|
||||||
|
onClick={(evt) => {
|
||||||
|
argumentClick(evt, 'tos');
|
||||||
|
}}
|
||||||
|
target="_blank">
|
||||||
|
terms of service
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
<div className="text-center mt-5">
|
<div className="text-center mt-5">
|
||||||
<Trans i18nKey="login.info_login" ns="translation">
|
<Trans i18nKey="login.info_login" ns="translation">
|
||||||
Already have an account? <Link to="/users/login">Log in</Link>
|
Already have an account? <Link to="/users/login">Log in</Link>
|
||||||
|
|
|
@ -9,7 +9,7 @@ import type { FormDataType } from '@/common/interface';
|
||||||
import { UploadImg, Avatar } from '@/components';
|
import { UploadImg, Avatar } from '@/components';
|
||||||
import { loggedUserInfoStore } from '@/stores';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { useToast } from '@/hooks';
|
import { useToast } from '@/hooks';
|
||||||
import { modifyUserInfo, uploadAvatar, getLoggedUserInfo } from '@/services';
|
import { modifyUserInfo, getLoggedUserInfo } from '@/services';
|
||||||
|
|
||||||
const Index: React.FC = () => {
|
const Index: React.FC = () => {
|
||||||
const { t } = useTranslation('translation', {
|
const { t } = useTranslation('translation', {
|
||||||
|
@ -60,21 +60,16 @@ const Index: React.FC = () => {
|
||||||
setFormData({ ...formData, ...params });
|
setFormData({ ...formData, ...params });
|
||||||
};
|
};
|
||||||
|
|
||||||
const avatarUpload = (file: any) => {
|
const avatarUpload = (path: string) => {
|
||||||
return new Promise((resolve) => {
|
setFormData({
|
||||||
uploadAvatar(file).then((res) => {
|
...formData,
|
||||||
setFormData({
|
avatar: {
|
||||||
...formData,
|
...formData.avatar,
|
||||||
avatar: {
|
type: 'custom',
|
||||||
...formData.avatar,
|
custom: path,
|
||||||
type: 'custom',
|
isInvalid: false,
|
||||||
custom: res,
|
errorMsg: '',
|
||||||
isInvalid: false,
|
},
|
||||||
errorMsg: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
resolve(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -364,7 +359,11 @@ const Index: React.FC = () => {
|
||||||
className="me-3 rounded"
|
className="me-3 rounded"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<UploadImg type="avatar" upload={avatarUpload} />
|
<UploadImg
|
||||||
|
type="avatar"
|
||||||
|
uploadCallback={avatarUpload}
|
||||||
|
className="mb-2"
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Form.Text className="text-muted mt-0">
|
<Form.Text className="text-muted mt-0">
|
||||||
<Trans i18nKey="settings.profile.avatar.text">
|
<Trans i18nKey="settings.profile.avatar.text">
|
||||||
|
|
|
@ -1,2 +1,4 @@
|
||||||
/// <reference types="react-scripts" />
|
/// <reference types="react-scripts" />
|
||||||
declare module '*.yaml';
|
declare module '*.yaml';
|
||||||
|
|
||||||
|
declare module '*.ico';
|
||||||
|
|
|
@ -10,7 +10,7 @@ const routes: RouteObject[] = [];
|
||||||
|
|
||||||
const routeWrapper = (routeNodes: RouteNode[], root: RouteObject[]) => {
|
const routeWrapper = (routeNodes: RouteNode[], root: RouteObject[]) => {
|
||||||
routeNodes.forEach((rn) => {
|
routeNodes.forEach((rn) => {
|
||||||
if (rn.path === '/') {
|
if (rn.page === 'pages/Layout') {
|
||||||
rn.element = <Layout />;
|
rn.element = <Layout />;
|
||||||
rn.errorElement = <ErrorBoundary />;
|
rn.errorElement = <ErrorBoundary />;
|
||||||
} else {
|
} else {
|
||||||
|
@ -31,7 +31,7 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteObject[]) => {
|
||||||
const refLoader = rn.loader;
|
const refLoader = rn.loader;
|
||||||
const refGuard = rn.guard;
|
const refGuard = rn.guard;
|
||||||
rn.loader = async (args) => {
|
rn.loader = async (args) => {
|
||||||
const gr = await refGuard();
|
const gr = await refGuard(args);
|
||||||
if (gr?.redirect && floppyNavigation.differentCurrent(gr.redirect)) {
|
if (gr?.redirect && floppyNavigation.differentCurrent(gr.redirect)) {
|
||||||
return redirect(gr.redirect);
|
return redirect(gr.redirect);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { RouteObject } from 'react-router-dom';
|
import { LoaderFunctionArgs, RouteObject } from 'react-router-dom';
|
||||||
|
|
||||||
import { guard } from '@/utils';
|
import { guard } from '@/utils';
|
||||||
import type { TGuardResult } from '@/utils/guard';
|
import type { TGuardResult } from '@/utils/guard';
|
||||||
|
@ -13,7 +13,7 @@ export interface RouteNode extends RouteObject {
|
||||||
* if guard returned the `TGuardResult` has `redirect` field,
|
* if guard returned the `TGuardResult` has `redirect` field,
|
||||||
* then auto redirect route to the `redirect` target.
|
* then auto redirect route to the `redirect` target.
|
||||||
*/
|
*/
|
||||||
guard?: () => Promise<TGuardResult>;
|
guard?: (args: LoaderFunctionArgs) => Promise<TGuardResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes: RouteNode[] = [
|
const routes: RouteNode[] = [
|
||||||
|
@ -31,7 +31,6 @@ const routes: RouteNode[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'questions',
|
path: 'questions',
|
||||||
index: true,
|
|
||||||
page: 'pages/Questions',
|
page: 'pages/Questions',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -251,6 +250,18 @@ const routes: RouteNode[] = [
|
||||||
path: 'smtp',
|
path: 'smtp',
|
||||||
page: 'pages/Admin/Smtp',
|
page: 'pages/Admin/Smtp',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'branding',
|
||||||
|
page: 'pages/Admin/Branding',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'legal',
|
||||||
|
page: 'pages/Admin/Legal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'write',
|
||||||
|
page: 'pages/Admin/Write',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -263,6 +274,25 @@ const routes: RouteNode[] = [
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
page: 'pages/Layout',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
page: 'pages/Legal',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'tos',
|
||||||
|
page: 'pages/Legal/Tos',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'privacy',
|
||||||
|
page: 'pages/Legal/Privacy',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/install',
|
path: '/install',
|
||||||
page: 'pages/Install',
|
page: 'pages/Install',
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import * as Type from '@/common/interface';
|
||||||
|
import request from '@/utils/request';
|
||||||
|
|
||||||
|
export const useDashBoard = () => {
|
||||||
|
const apiUrl = `/answer/admin/api/dashboard`;
|
||||||
|
const { data, error } = useSWR<Type.AdminDashboard, Error>(
|
||||||
|
[apiUrl],
|
||||||
|
request.instance.get,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
isLoading: !data && !error,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
|
@ -2,3 +2,5 @@ export * from './answer';
|
||||||
export * from './flag';
|
export * from './flag';
|
||||||
export * from './question';
|
export * from './question';
|
||||||
export * from './settings';
|
export * from './settings';
|
||||||
|
export * from './users';
|
||||||
|
export * from './dashboard';
|
||||||
|
|
|
@ -4,24 +4,6 @@ import useSWR from 'swr';
|
||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type * as Type from '@/common/interface';
|
import type * as Type from '@/common/interface';
|
||||||
|
|
||||||
export const changeUserStatus = (params) => {
|
|
||||||
return request.put('/answer/admin/api/user/status', params);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useQueryUsers = (params) => {
|
|
||||||
const apiUrl = `/answer/admin/api/users/page?${qs.stringify(params)}`;
|
|
||||||
const { data, error, mutate } = useSWR<Type.ListResult, Error>(
|
|
||||||
apiUrl,
|
|
||||||
request.instance.get,
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
isLoading: !data && !error,
|
|
||||||
error,
|
|
||||||
mutate,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useQuestionSearch = (params: Type.AdminContentsReq) => {
|
export const useQuestionSearch = (params: Type.AdminContentsReq) => {
|
||||||
const apiUrl = `/answer/admin/api/question/page?${qs.stringify(params)}`;
|
const apiUrl = `/answer/admin/api/question/page?${qs.stringify(params)}`;
|
||||||
const { data, error, mutate } = useSWR<Type.ListResult, Error>(
|
const { data, error, mutate } = useSWR<Type.ListResult, Error>(
|
||||||
|
|
|
@ -71,20 +71,33 @@ export const updateSmtpSetting = (params: Type.AdminSettingsSmtp) => {
|
||||||
return request.put(apiUrl, params);
|
return request.put(apiUrl, params);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useDashBoard = () => {
|
|
||||||
const apiUrl = `/answer/admin/api/dashboard`;
|
|
||||||
const { data, error } = useSWR<Type.AdminDashboard, Error>(
|
|
||||||
[apiUrl],
|
|
||||||
request.instance.get,
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
isLoading: !data && !error,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAdminLanguageOptions = () => {
|
export const getAdminLanguageOptions = () => {
|
||||||
const apiUrl = `/answer/admin/api/language/options`;
|
const apiUrl = `/answer/admin/api/language/options`;
|
||||||
return request.get<Type.LangsType[]>(apiUrl);
|
return request.get<Type.LangsType[]>(apiUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getBrandSetting = () => {
|
||||||
|
return request.get('/answer/admin/api/siteinfo/branding');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const brandSetting = (params: Type.AdmingSettingBranding) => {
|
||||||
|
return request.put('/answer/admin/api/siteinfo/branding', params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRequireAndReservedTag = () => {
|
||||||
|
return request.get('/answer/admin/api/siteinfo/write');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const postRequireAndReservedTag = (params) => {
|
||||||
|
return request.put('/answer/admin/api/siteinfo/write', params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLegalSetting = () => {
|
||||||
|
return request.get<Type.AdminSettingsLegal>(
|
||||||
|
'/answer/admin/api/siteinfo/legal',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const putLegalSetting = (params: Type.AdminSettingsLegal) => {
|
||||||
|
return request.put('/answer/admin/api/siteinfo/legal', params);
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import qs from 'qs';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import request from '@/utils/request';
|
||||||
|
import type * as Type from '@/common/interface';
|
||||||
|
|
||||||
|
export const changeUserStatus = (params) => {
|
||||||
|
return request.put('/answer/admin/api/user/status', params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useQueryUsers = (params) => {
|
||||||
|
const apiUrl = `/answer/admin/api/users/page?${qs.stringify(params)}`;
|
||||||
|
const { data, error, mutate } = useSWR<Type.ListResult, Error>(
|
||||||
|
apiUrl,
|
||||||
|
request.instance.get,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
isLoading: !data && !error,
|
||||||
|
error,
|
||||||
|
mutate,
|
||||||
|
};
|
||||||
|
};
|
|
@ -5,3 +5,4 @@ export * from './question';
|
||||||
export * from './search';
|
export * from './search';
|
||||||
export * from './tag';
|
export * from './tag';
|
||||||
export * from './settings';
|
export * from './settings';
|
||||||
|
export * from './legal';
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import request from '@/utils/request';
|
||||||
|
import type * as Type from '@/common/interface';
|
||||||
|
|
||||||
|
export const useLegalTos = () => {
|
||||||
|
const apiUrl = '/answer/api/v1/siteinfo/legal?info_type=tos';
|
||||||
|
const { data, error, mutate } = useSWR<Type.AdminSettingsLegal, Error>(
|
||||||
|
[apiUrl],
|
||||||
|
request.instance.get,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
isLoading: !data && !error,
|
||||||
|
error,
|
||||||
|
mutate,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLegalPrivacy = () => {
|
||||||
|
const apiUrl = '/answer/api/v1/siteinfo/legal?info_type=privacy';
|
||||||
|
const { data, error, mutate } = useSWR<Type.AdminSettingsLegal, Error>(
|
||||||
|
[apiUrl],
|
||||||
|
request.instance.get,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
isLoading: !data && !error,
|
||||||
|
error,
|
||||||
|
mutate,
|
||||||
|
};
|
||||||
|
};
|
|
@ -3,7 +3,7 @@ import qs from 'qs';
|
||||||
|
|
||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type * as Type from '@/common/interface';
|
import type * as Type from '@/common/interface';
|
||||||
import { tryLoggedAndActicevated } from '@/utils/guard';
|
import { tryLoggedAndActivated } from '@/utils/guard';
|
||||||
|
|
||||||
export const useQueryNotifications = (params) => {
|
export const useQueryNotifications = (params) => {
|
||||||
const apiUrl = `/answer/api/v1/notification/page?${qs.stringify(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';
|
const apiUrl = '/answer/api/v1/notification/status';
|
||||||
|
|
||||||
return useSWR<{ inbox: number; achievement: number }>(
|
return useSWR<{ inbox: number; achievement: number }>(
|
||||||
tryLoggedAndActicevated().ok ? apiUrl : null,
|
tryLoggedAndActivated().ok ? apiUrl : null,
|
||||||
request.instance.get,
|
request.instance.get,
|
||||||
{
|
{
|
||||||
refreshInterval: 3000,
|
refreshInterval: 3000,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import useSWR from 'swr';
|
||||||
|
|
||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type * as Type from '@/common/interface';
|
import type * as Type from '@/common/interface';
|
||||||
import { tryLoggedAndActicevated } from '@/utils/guard';
|
import { tryLoggedAndActivated } from '@/utils/guard';
|
||||||
|
|
||||||
export const deleteTag = (id) => {
|
export const deleteTag = (id) => {
|
||||||
return request.delete('/answer/api/v1/tag', {
|
return request.delete('/answer/api/v1/tag', {
|
||||||
|
@ -24,7 +24,7 @@ export const saveSynonymsTags = (params) => {
|
||||||
|
|
||||||
export const useFollowingTags = () => {
|
export const useFollowingTags = () => {
|
||||||
let apiUrl = '';
|
let apiUrl = '';
|
||||||
if (tryLoggedAndActicevated().ok) {
|
if (tryLoggedAndActivated().ok) {
|
||||||
apiUrl = '/answer/api/v1/tags/following';
|
apiUrl = '/answer/api/v1/tags/following';
|
||||||
}
|
}
|
||||||
const { data, error, mutate } = useSWR<any[]>(apiUrl, request.instance.get);
|
const { data, error, mutate } = useSWR<any[]>(apiUrl, request.instance.get);
|
||||||
|
|
|
@ -4,12 +4,13 @@ import useSWR from 'swr';
|
||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type * as Type from '@/common/interface';
|
import type * as Type from '@/common/interface';
|
||||||
|
|
||||||
export const uploadImage = (file) => {
|
export const uploadImage = (params: { file: File; type: Type.UploadType }) => {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
|
form.append('source', String(params.type));
|
||||||
form.append('file', file);
|
form.append('file', params.file);
|
||||||
return request.post('/answer/api/v1/user/post/file', form);
|
return request.post('/answer/api/v1/file', form);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useQueryQuestionByTitle = (title) => {
|
export const useQueryQuestionByTitle = (title) => {
|
||||||
return useSWR<Record<string, any>>(
|
return useSWR<Record<string, any>>(
|
||||||
title ? `/answer/api/v1/question/similar?title=${title}` : '',
|
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);
|
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) => {
|
export const resetPassword = (params: Type.PasswordResetReq) => {
|
||||||
return request.post('/answer/api/v1/user/password/reset', params);
|
return request.post('/answer/api/v1/user/password/reset', params);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import create from 'zustand';
|
||||||
|
|
||||||
|
import { AdmingSettingBranding } from '@/common/interface';
|
||||||
|
import { DEFAULT_LANG } from '@/common/constants';
|
||||||
|
|
||||||
|
interface InterfaceType {
|
||||||
|
branding: AdmingSettingBranding;
|
||||||
|
update: (params: AdmingSettingBranding) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const interfaceSetting = create<InterfaceType>((set) => ({
|
||||||
|
branding: {
|
||||||
|
logo: '',
|
||||||
|
square_icon: '',
|
||||||
|
mobile_logo: '',
|
||||||
|
favicon: '',
|
||||||
|
},
|
||||||
|
interface: {
|
||||||
|
theme: '',
|
||||||
|
language: DEFAULT_LANG,
|
||||||
|
time_zone: '',
|
||||||
|
},
|
||||||
|
update: (params) =>
|
||||||
|
set(() => {
|
||||||
|
return {
|
||||||
|
branding: params,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default interfaceSetting;
|
|
@ -3,6 +3,7 @@ import loggedUserInfoStore from './userInfo';
|
||||||
import globalStore from './global';
|
import globalStore from './global';
|
||||||
import siteInfoStore from './siteInfo';
|
import siteInfoStore from './siteInfo';
|
||||||
import interfaceStore from './interface';
|
import interfaceStore from './interface';
|
||||||
|
import brandingStore from './branding';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
toastStore,
|
toastStore,
|
||||||
|
@ -10,4 +11,5 @@ export {
|
||||||
globalStore,
|
globalStore,
|
||||||
siteInfoStore,
|
siteInfoStore,
|
||||||
interfaceStore,
|
interfaceStore,
|
||||||
|
brandingStore,
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,7 +10,6 @@ interface InterfaceType {
|
||||||
|
|
||||||
const interfaceSetting = create<InterfaceType>((set) => ({
|
const interfaceSetting = create<InterfaceType>((set) => ({
|
||||||
interface: {
|
interface: {
|
||||||
logo: '',
|
|
||||||
theme: '',
|
theme: '',
|
||||||
language: DEFAULT_LANG,
|
language: DEFAULT_LANG,
|
||||||
time_zone: '',
|
time_zone: '',
|
||||||
|
|
|
@ -1,12 +1,5 @@
|
||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
|
|
||||||
function getQueryString(name: string): string {
|
|
||||||
const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`);
|
|
||||||
const r = window.location.search.substr(1).match(reg);
|
|
||||||
if (r != null) return unescape(r[2]);
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function thousandthDivision(num) {
|
function thousandthDivision(num) {
|
||||||
const reg = /\d{1,3}(?=(\d{3})+$)/g;
|
const reg = /\d{1,3}(?=(\d{3})+$)/g;
|
||||||
return `${num}`.replace(reg, '$&,');
|
return `${num}`.replace(reg, '$&,');
|
||||||
|
@ -101,9 +94,63 @@ function escapeRemove(str) {
|
||||||
return arrEntities[t];
|
return arrEntities[t];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function mixColor(color_1, color_2, weight) {
|
||||||
|
function d2h(d) {
|
||||||
|
return d.toString(16);
|
||||||
|
}
|
||||||
|
function h2d(h) {
|
||||||
|
return parseInt(h, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
weight = typeof weight !== 'undefined' ? weight : 50;
|
||||||
|
let color = '#';
|
||||||
|
|
||||||
|
for (let i = 0; i <= 5; i += 2) {
|
||||||
|
const v1 = h2d(color_1.substr(i, 2));
|
||||||
|
const v2 = h2d(color_2.substr(i, 2));
|
||||||
|
let val = d2h(Math.floor(v2 + (v1 - v2) * (weight / 100.0)));
|
||||||
|
|
||||||
|
while (val.length < 2) {
|
||||||
|
val = `0${val}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
color += val;
|
||||||
|
}
|
||||||
|
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
function colorRgb(sColor) {
|
||||||
|
sColor = sColor.toLowerCase();
|
||||||
|
const reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
|
||||||
|
if (sColor && reg.test(sColor)) {
|
||||||
|
if (sColor.length === 4) {
|
||||||
|
let sColorNew = '#';
|
||||||
|
for (let i = 1; i < 4; i += 1) {
|
||||||
|
sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1));
|
||||||
|
}
|
||||||
|
sColor = sColorNew;
|
||||||
|
}
|
||||||
|
const sColorChange: number[] = [];
|
||||||
|
for (let i = 1; i < 7; i += 2) {
|
||||||
|
sColorChange.push(parseInt(`0x${sColor.slice(i, i + 2)}`, 16));
|
||||||
|
}
|
||||||
|
return sColorChange.join(',');
|
||||||
|
}
|
||||||
|
return sColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
function labelStyle(color, hover) {
|
||||||
|
const textColor = mixColor('000000', color.replace('#', ''), 40);
|
||||||
|
const backgroundColor = mixColor('ffffff', color.replace('#', ''), 80);
|
||||||
|
const rgbBackgroundColor = colorRgb(backgroundColor);
|
||||||
|
return {
|
||||||
|
color: textColor,
|
||||||
|
backgroundColor: `rgba(${colorRgb(rgbBackgroundColor)},${hover ? 1 : 0.5})`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getQueryString,
|
|
||||||
thousandthDivision,
|
thousandthDivision,
|
||||||
formatCount,
|
formatCount,
|
||||||
scrollTop,
|
scrollTop,
|
||||||
|
@ -111,4 +158,7 @@ export {
|
||||||
parseUserInfo,
|
parseUserInfo,
|
||||||
formatUptime,
|
formatUptime,
|
||||||
escapeRemove,
|
escapeRemove,
|
||||||
|
mixColor,
|
||||||
|
colorRgb,
|
||||||
|
labelStyle,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import { getLoggedUserInfo, getAppSettings } from '@/services';
|
import { getLoggedUserInfo, getAppSettings } from '@/services';
|
||||||
import { loggedUserInfoStore, siteInfoStore, interfaceStore } from '@/stores';
|
import {
|
||||||
|
loggedUserInfoStore,
|
||||||
|
siteInfoStore,
|
||||||
|
interfaceStore,
|
||||||
|
brandingStore,
|
||||||
|
} from '@/stores';
|
||||||
import { RouteAlias } from '@/router/alias';
|
import { RouteAlias } from '@/router/alias';
|
||||||
import Storage from '@/utils/storage';
|
import Storage from '@/utils/storage';
|
||||||
import { LOGGED_USER_STORAGE_KEY } from '@/common/constants';
|
import { LOGGED_USER_STORAGE_KEY } from '@/common/constants';
|
||||||
|
@ -182,9 +187,10 @@ export const tryNormalLogged = (canNavigate: boolean = false) => {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tryLoggedAndActicevated = () => {
|
export const tryLoggedAndActivated = () => {
|
||||||
const gr: TGuardResult = { ok: true };
|
const gr: TGuardResult = { ok: true };
|
||||||
const us = deriveLoginState();
|
const us = deriveLoginState();
|
||||||
|
|
||||||
if (!us.isLogged || !us.isActivated) {
|
if (!us.isLogged || !us.isActivated) {
|
||||||
gr.ok = false;
|
gr.ok = false;
|
||||||
}
|
}
|
||||||
|
@ -196,6 +202,7 @@ export const initAppSettingsStore = async () => {
|
||||||
if (appSettings) {
|
if (appSettings) {
|
||||||
siteInfoStore.getState().update(appSettings.general);
|
siteInfoStore.getState().update(appSettings.general);
|
||||||
interfaceStore.getState().update(appSettings.interface);
|
interfaceStore.getState().update(appSettings.interface);
|
||||||
|
brandingStore.getState().update(appSettings.branding);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -85,6 +85,7 @@ export const getCurrentLang = () => {
|
||||||
|
|
||||||
export const setupAppLanguage = async () => {
|
export const setupAppLanguage = async () => {
|
||||||
const lang = getCurrentLang();
|
const lang = getCurrentLang();
|
||||||
|
console.log(lang);
|
||||||
if (!i18next.getDataByLanguage(lang)) {
|
if (!i18next.getDataByLanguage(lang)) {
|
||||||
await addI18nResource(lang);
|
await addI18nResource(lang);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,16 +10,8 @@ import { getCurrentLang } from '@/utils/localize';
|
||||||
import Storage from './storage';
|
import Storage from './storage';
|
||||||
import { floppyNavigation } from './floppyNavigation';
|
import { floppyNavigation } from './floppyNavigation';
|
||||||
|
|
||||||
const API = {
|
|
||||||
development: '',
|
|
||||||
production: '',
|
|
||||||
test: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const baseApiUrl = process.env.REACT_APP_API_URL || API[process.env.NODE_ENV];
|
|
||||||
|
|
||||||
const baseConfig = {
|
const baseConfig = {
|
||||||
baseUrl: baseApiUrl,
|
baseUrl: process.env.REACT_APP_API_URL || '',
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
};
|
};
|
||||||
|
@ -56,8 +48,8 @@ class Request {
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
const { status, data: respData, msg: respMsg } = error.response;
|
const { status, data: respData, msg: respMsg } = error.response || {};
|
||||||
const { data, msg = '' } = respData;
|
const { data = {}, msg = '' } = respData || {};
|
||||||
if (status === 400) {
|
if (status === 400) {
|
||||||
// show error message
|
// show error message
|
||||||
if (data instanceof Object && data.err_type) {
|
if (data instanceof Object && data.err_type) {
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"downlevelIteration": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
|
@ -24,5 +23,5 @@
|
||||||
"@i18n/*": ["../i18n/*"]
|
"@i18n/*": ["../i18n/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src", "node_modules/@testing-library/jest-dom" ]
|
"include": ["src", "node_modules/@testing-library/jest-dom", "scripts" ]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue