feat: member manage section in setting dialog

This commit is contained in:
boojack 2022-05-16 22:19:39 +08:00
parent fbf4afff8e
commit c492317ffe
24 changed files with 421 additions and 344 deletions

View File

@ -27,9 +27,10 @@ type User struct {
type UserCreate struct {
// Domain specific fields
Email string
Role Role
Name string
Email string `json:"email"`
Role Role `json:"role"`
Name string `json:"name"`
Password string `json:"password"`
PasswordHash string
OpenID string
}

View File

@ -81,24 +81,10 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
if len(signup.Email) < 6 {
return echo.NewHTTPError(http.StatusBadRequest, "Email is too short, minimum length is 6.")
}
if len(signup.Name) < 6 {
return echo.NewHTTPError(http.StatusBadRequest, "Username is too short, minimum length is 6.")
}
if len(signup.Password) < 6 {
return echo.NewHTTPError(http.StatusBadRequest, "Password is too short, minimum length is 6.")
}
userFind := &api.UserFind{
Email: &signup.Email,
}
user, err := s.Store.FindUser(userFind)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by email %s", signup.Email)).SetInternal(err)
}
if user != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Existed user found: %s", signup.Email))
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(signup.Password), bcrypt.DefaultCost)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
@ -111,7 +97,7 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
PasswordHash: string(passwordHash),
OpenID: common.GenUUID(),
}
user, err = s.Store.CreateUser(userCreate)
user, err := s.Store.CreateUser(userCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
}

View File

@ -12,6 +12,45 @@ import (
)
func (s *Server) registerUserRoutes(g *echo.Group) {
g.POST("/user", func(c echo.Context) error {
userCreate := &api.UserCreate{}
if err := json.NewDecoder(c.Request().Body).Decode(userCreate); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user request").SetInternal(err)
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(userCreate.Password), bcrypt.DefaultCost)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
}
userCreate.PasswordHash = string(passwordHash)
user, err := s.Store.CreateUser(userCreate)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
}
return nil
})
g.GET("/user", func(c echo.Context) error {
userList, err := s.Store.FindUserList(&api.UserFind{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user list").SetInternal(err)
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(userList)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user list response").SetInternal(err)
}
return nil
})
// GET /api/user/me is used to check if the user is logged in.
g.GET("/user/me", func(c echo.Context) error {
userSessionID := c.Get(getUserIDContextKey())

View File

@ -1,14 +1,13 @@
-- user
CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
role TEXT NOT NULL CHECK (role IN ('OWNER', 'USER')) DEFAULT 'USER',
name TEXT NOT NULL,
password_hash TEXT NOT NULL,
open_id TEXT NOT NULL,
open_id TEXT NOT NULL UNIQUE,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(`email`, `open_id`)
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
);
INSERT INTO

View File

@ -25,6 +25,15 @@ func (s *Store) PatchUser(patch *api.UserPatch) (*api.User, error) {
return user, nil
}
func (s *Store) FindUserList(find *api.UserFind) ([]*api.User, error) {
list, err := findUserList(s.db, find)
if err != nil {
return nil, err
}
return list, nil
}
func (s *Store) FindUser(find *api.UserFind) (*api.User, error) {
list, err := findUserList(s.db, find)
if err != nil {

View File

@ -246,7 +246,7 @@ const MemoEditor: React.FC<Props> = () => {
const file = inputEl.files[0];
const url = await handleUploadFile(file);
if (url) {
editorRef.current?.insertText(url);
editorRef.current?.insertText(url + " ");
}
};
inputEl.click();
@ -259,7 +259,7 @@ const MemoEditor: React.FC<Props> = () => {
}
}, []);
const showEditStatus = Boolean(globalState.editMemoId);
const isEditing = Boolean(globalState.editMemoId);
const editorConfig = useMemo(
() => ({
@ -267,17 +267,17 @@ const MemoEditor: React.FC<Props> = () => {
initialContent: getEditorContentCache(),
placeholder: "Any thoughts...",
showConfirmBtn: true,
showCancelBtn: showEditStatus,
showCancelBtn: isEditing,
onConfirmBtnClick: handleSaveBtnClick,
onCancelBtnClick: handleCancelBtnClick,
onContentChange: handleContentChange,
}),
[showEditStatus]
[isEditing]
);
return (
<div className={"memo-editor-container " + (showEditStatus ? "edit-ing" : "")}>
<p className={"tip-text " + (showEditStatus ? "" : "hidden")}>Editting...</p>
<div className={"memo-editor-container " + (isEditing ? "edit-ing" : "")}>
<p className={"tip-text " + (isEditing ? "" : "hidden")}>Editting...</p>
<Editor
ref={editorRef}
{...editorConfig}

View File

@ -1,125 +0,0 @@
import { useContext } from "react";
import appContext from "../stores/appContext";
import { globalStateService, memoService } from "../services";
import utils from "../helpers/utils";
import { formatMemoContent } from "./Memo";
import toastHelper from "./Toast";
import "../less/preferences-section.less";
interface Props {}
const PreferencesSection: React.FC<Props> = () => {
const { globalState } = useContext(appContext);
const { shouldHideImageUrl, shouldSplitMemoWord, shouldUseMarkdownParser } = globalState;
const demoMemoContent = "👋 Hiya, welcome to memos!\n* ✨ **Open source project**;\n* 😋 What do you think;\n* 📑 Tell me something plz;";
const handleSplitWordsValueChanged = () => {
globalStateService.setAppSetting({
shouldSplitMemoWord: !shouldSplitMemoWord,
});
};
const handleHideImageUrlValueChanged = () => {
globalStateService.setAppSetting({
shouldHideImageUrl: !shouldHideImageUrl,
});
};
const handleUseMarkdownParserChanged = () => {
globalStateService.setAppSetting({
shouldUseMarkdownParser: !shouldUseMarkdownParser,
});
};
const handleExportBtnClick = async () => {
const formatedMemos = memoService.getState().memos.map((m) => {
return {
content: m.content,
createdAt: m.createdAt,
};
});
const jsonStr = JSON.stringify(formatedMemos);
const element = document.createElement("a");
element.setAttribute("href", "data:text/json;charset=utf-8," + encodeURIComponent(jsonStr));
element.setAttribute("download", `memos-${utils.getDateTimeString(Date.now())}.json`);
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
const handleImportBtnClick = async () => {
const fileInputEl = document.createElement("input");
fileInputEl.type = "file";
fileInputEl.accept = "application/JSON";
fileInputEl.onchange = () => {
if (fileInputEl.files?.length && fileInputEl.files.length > 0) {
const reader = new FileReader();
reader.readAsText(fileInputEl.files[0]);
reader.onload = async (event) => {
const memoList = JSON.parse(event.target?.result as string) as Model.Memo[];
if (!Array.isArray(memoList)) {
toastHelper.error("Unexpected data type.");
}
let succeedAmount = 0;
for (const memo of memoList) {
const content = memo.content || "";
const createdAt = memo.createdAt || utils.getDateTimeString(Date.now());
try {
await memoService.importMemo(content, createdAt);
succeedAmount++;
} catch (error) {
// do nth
}
}
await memoService.fetchAllMemos();
toastHelper.success(`${succeedAmount} memos successfully imported.`);
};
}
};
fileInputEl.click();
};
return (
<>
<div className="section-container preferences-section-container">
<p className="title-text">Memo Display</p>
<div
className="demo-content-container memo-content-text"
dangerouslySetInnerHTML={{ __html: formatMemoContent(demoMemoContent) }}
></div>
<label className="form-label checkbox-form-label hidden" onClick={handleSplitWordsValueChanged}>
<span className="normal-text">Auto-space in English and Chinese</span>
<img className="icon-img" src={shouldSplitMemoWord ? "/icons/checkbox-active.svg" : "/icons/checkbox.svg"} />
</label>
<label className="form-label checkbox-form-label" onClick={handleUseMarkdownParserChanged}>
<span className="normal-text">Partial markdown format parsing</span>
<img className="icon-img" src={shouldUseMarkdownParser ? "/icons/checkbox-active.svg" : "/icons/checkbox.svg"} />
</label>
<label className="form-label checkbox-form-label" onClick={handleHideImageUrlValueChanged}>
<span className="normal-text">Hide image url</span>
<img className="icon-img" src={shouldHideImageUrl ? "/icons/checkbox-active.svg" : "/icons/checkbox.svg"} />
</label>
</div>
<div className="section-container">
<p className="title-text">Others</p>
<div className="w-full flex flex-row justify-start items-center">
<button className="px-2 py-1 border rounded text-base hover:opacity-80" onClick={handleExportBtnClick}>
Export data as JSON
</button>
<button className="ml-2 px-2 py-1 border rounded text-base hover:opacity-80" onClick={handleImportBtnClick}>
Import from JSON
</button>
</div>
</div>
</>
);
};
export default PreferencesSection;

View File

@ -1,18 +1,23 @@
import { useState } from "react";
import { useContext, useState } from "react";
import appContext from "../stores/appContext";
import { showDialog } from "./Dialog";
import MyAccountSection from "./MyAccountSection";
import PreferencesSection from "./PreferencesSection";
import MyAccountSection from "./Settings/MyAccountSection";
import PreferencesSection from "./Settings/PreferencesSection";
import MemberSection from "./Settings/MemberSection";
import "../less/setting-dialog.less";
interface Props extends DialogProps {}
type SettingSection = "my-account" | "preferences";
type SettingSection = "my-account" | "preferences" | "member";
interface State {
selectedSection: SettingSection;
}
const SettingDialog: React.FC<Props> = (props: Props) => {
const {
userState: { user },
} = useContext(appContext);
const { destroy } = props;
const [state, setState] = useState<State>({
selectedSection: "my-account",
@ -30,6 +35,7 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
<img className="icon-img" src="/icons/close.svg" />
</button>
<div className="section-selector-container">
<span className="section-title">Basic</span>
<span
onClick={() => handleSectionSelectorItemClick("my-account")}
className={`section-item ${state.selectedSection === "my-account" ? "selected" : ""}`}
@ -42,12 +48,25 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
>
Preferences
</span>
{user?.role === "OWNER" ? (
<>
<span className="section-title">Admin</span>
<span
onClick={() => handleSectionSelectorItemClick("member")}
className={`section-item ${state.selectedSection === "member" ? "selected" : ""}`}
>
Member
</span>
</>
) : null}
</div>
<div className="section-content-container">
{state.selectedSection === "my-account" ? (
<MyAccountSection />
) : state.selectedSection === "preferences" ? (
<PreferencesSection />
) : state.selectedSection === "member" ? (
<MemberSection />
) : null}
</div>
</div>

View File

@ -0,0 +1,96 @@
import React, { useEffect, useState } from "react";
import { isEmpty } from "lodash-es";
import api from "../../helpers/api";
import toastHelper from "../Toast";
import "../../less/settings/member-section.less";
interface Props {}
interface State {
createUserEmail: string;
createUserPassword: string;
}
const PreferencesSection: React.FC<Props> = () => {
const [state, setState] = useState<State>({
createUserEmail: "",
createUserPassword: "",
});
const [userList, setUserList] = useState<Model.User[]>([]);
useEffect(() => {
fetchUserList();
}, []);
const fetchUserList = async () => {
const data = await api.getUserList();
setUserList(data);
};
const handleEmailInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setState({
...state,
createUserEmail: event.target.value,
});
};
const handlePasswordInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setState({
...state,
createUserPassword: event.target.value,
});
};
const handleCreateUserBtnClick = async () => {
if (isEmpty(state.createUserEmail) || isEmpty(state.createUserPassword)) {
toastHelper.error("Please fill out this form");
return;
}
const userCreate: API.UserCreate = {
email: state.createUserEmail,
password: state.createUserPassword,
role: "USER",
name: state.createUserEmail,
};
try {
await api.createUser(userCreate);
} catch (error: any) {
toastHelper.error(error.message);
}
await fetchUserList();
setState({
createUserEmail: "",
createUserPassword: "",
});
};
return (
<div className="section-container member-section-container">
<p className="title-text">Create a member</p>
<div className="create-member-container">
<div className="input-form-container">
<span className="field-text">Email</span>
<input type="email" placeholder="Email" value={state.createUserEmail} onChange={handleEmailInputChange} />
</div>
<div className="input-form-container">
<span className="field-text">Password</span>
<input type="text" placeholder="Password" value={state.createUserPassword} onChange={handlePasswordInputChange} />
</div>
<div className="btns-container">
<button onClick={handleCreateUserBtnClick}>Create</button>
</div>
</div>
<p className="title-text">Member list</p>
{userList.map((user) => (
<div key={user.id} className="user-container">
<span className="field-text id-text">{user.id}</span>
<span className="field-text">{user.email}</span>
</div>
))}
</div>
);
};
export default PreferencesSection;

View File

@ -1,12 +1,12 @@
import { useContext, useState } from "react";
import appContext from "../stores/appContext";
import { userService } from "../services";
import utils from "../helpers/utils";
import { validate, ValidatorConfig } from "../helpers/validator";
import toastHelper from "./Toast";
import showChangePasswordDialog from "./ChangePasswordDialog";
import showConfirmResetOpenIdDialog from "./ConfirmResetOpenIdDialog";
import "../less/my-account-section.less";
import appContext from "../../stores/appContext";
import { userService } from "../../services";
import utils from "../../helpers/utils";
import { validate, ValidatorConfig } from "../../helpers/validator";
import toastHelper from "../Toast";
import showChangePasswordDialog from "../ChangePasswordDialog";
import showConfirmResetOpenIdDialog from "../ConfirmResetOpenIdDialog";
import "../../less/settings/my-account-section.less";
const validateConfig: ValidatorConfig = {
minLength: 4,

View File

@ -0,0 +1,78 @@
import { memoService } from "../../services";
import utils from "../../helpers/utils";
import toastHelper from "../Toast";
import "../../less/settings/preferences-section.less";
interface Props {}
const PreferencesSection: React.FC<Props> = () => {
const handleExportBtnClick = async () => {
const formatedMemos = memoService.getState().memos.map((m) => {
return {
content: m.content,
createdAt: m.createdAt,
};
});
const jsonStr = JSON.stringify(formatedMemos);
const element = document.createElement("a");
element.setAttribute("href", "data:text/json;charset=utf-8," + encodeURIComponent(jsonStr));
element.setAttribute("download", `memos-${utils.getDateTimeString(Date.now())}.json`);
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
const handleImportBtnClick = async () => {
const fileInputEl = document.createElement("input");
fileInputEl.type = "file";
fileInputEl.accept = "application/JSON";
fileInputEl.onchange = () => {
if (fileInputEl.files?.length && fileInputEl.files.length > 0) {
const reader = new FileReader();
reader.readAsText(fileInputEl.files[0]);
reader.onload = async (event) => {
const memoList = JSON.parse(event.target?.result as string) as Model.Memo[];
if (!Array.isArray(memoList)) {
toastHelper.error("Unexpected data type.");
}
let succeedAmount = 0;
for (const memo of memoList) {
const content = memo.content || "";
const createdAt = memo.createdAt || utils.getDateTimeString(Date.now());
try {
await memoService.importMemo(content, createdAt);
succeedAmount++;
} catch (error) {
// do nth
}
}
await memoService.fetchAllMemos();
toastHelper.success(`${succeedAmount} memos successfully imported.`);
};
}
};
fileInputEl.click();
};
return (
<div className="section-container preferences-section-container">
<p className="title-text">Others</p>
<div className="btns-container">
<button className="btn" onClick={handleExportBtnClick}>
Export data as JSON
</button>
<button className="btn" onClick={handleImportBtnClick}>
Import from JSON
</button>
</div>
</div>
);
};
export default PreferencesSection;

View File

@ -48,10 +48,18 @@ namespace api {
});
}
export function getUserInfo() {
return request<Model.User>({
export function getUserList() {
return request<Model.User[]>({
method: "GET",
url: "/api/user/me",
url: "/api/user",
});
}
export function createUser(userCreate: API.UserCreate) {
return request<Model.User[]>({
method: "POST",
url: "/api/user",
data: userCreate,
});
}
@ -66,15 +74,15 @@ namespace api {
});
}
export function signup(email: string, role: UserRole, name: string, password: string) {
export function signup(email: string, password: string, role: UserRole) {
return request<Model.User>({
method: "POST",
url: "/api/auth/signup",
data: {
email,
role,
name,
password,
role,
name: email,
},
});
}
@ -86,23 +94,10 @@ namespace api {
});
}
export function checkUsernameUsable(name: string) {
return request<boolean>({
method: "POST",
url: "/api/user/rename_check",
data: {
name,
},
});
}
export function checkPasswordValid(password: string) {
return request<boolean>({
method: "POST",
url: "/api/user/password_check",
data: {
password,
},
export function getUserInfo() {
return request<Model.User>({
method: "GET",
url: "/api/user/me",
});
}

View File

@ -29,15 +29,11 @@
.btn {
.flex(column, center, center);
@apply w-6 h-6 rounded;
@apply w-6 h-6 rounded hover:bg-gray-200 hover:shadow;
> .icon-img {
@apply w-5 h-5;
}
&:hover {
@apply bg-gray-200;
}
}
}

View File

@ -66,10 +66,6 @@ a {
.btn {
@apply select-none cursor-pointer text-center;
border: unset;
background-color: unset;
text-align: unset;
font-size: unset;
}
.hidden {

View File

@ -1,91 +0,0 @@
@import "./mixin.less";
.account-section-container {
> .form-label {
min-height: 28px;
> .normal-text {
@apply first:mr-2;
}
&.username-label {
> input {
flex-grow: 0;
width: 128px;
padding: 0 8px;
font-size: 14px;
border: 1px solid lightgray;
border-radius: 4px;
line-height: 26px;
background-color: transparent;
&:focus {
border-color: black;
}
}
> .btns-container {
.flex(row, flex-start, center);
margin-left: 8px;
flex-shrink: 0;
> .btn {
font-size: 12px;
padding: 0 16px;
border-radius: 4px;
line-height: 28px;
margin-right: 8px;
background-color: lightgray;
&:hover {
opacity: 0.8;
}
&.cancel-btn {
background-color: unset;
}
&.confirm-btn {
background-color: @text-green;
color: white;
}
}
}
}
&.password-label {
> .btn {
@apply text-blue-600 ml-1 cursor-pointer hover:opacity-80;
}
}
}
}
.openapi-section-container {
> .value-text {
width: 100%;
border: 1px solid lightgray;
padding: 4px 6px;
border-radius: 4px;
line-height: 1.6;
word-break: break-all;
white-space: pre-wrap;
}
> .reset-btn {
@apply mt-2 py-1 px-2 bg-red-50 border border-red-500 text-red-600 rounded leading-4 cursor-pointer text-xs select-none hover:opacity-80;
}
> .usage-guide-container {
.flex(column, flex-start, flex-start);
@apply mt-2 w-full;
> .title-text {
@apply my-2 text-sm;
}
> pre {
@apply w-full bg-gray-50 py-2 px-3 text-sm rounded whitespace-pre-wrap;
}
}
}

View File

@ -1,44 +0,0 @@
@import "./mixin.less";
.preferences-section-container {
> .demo-content-container {
padding: 16px;
border-radius: 8px;
border: 2px solid @bg-gray;
margin: 12px 0;
}
> .form-label {
min-height: 28px;
cursor: pointer;
> .icon-img {
width: 16px;
height: 16px;
margin: 0 8px;
}
&:hover {
opacity: 0.8;
}
}
> .btn-container {
.flex(row, flex-start, center);
width: 100%;
margin: 4px 0;
.btn {
height: 28px;
padding: 0 12px;
margin-right: 8px;
border: 1px solid gray;
border-radius: 8px;
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
}
}

View File

@ -3,7 +3,7 @@
.setting-dialog {
> .dialog-container {
@apply w-168 max-w-full mb-8 p-0;
@apply w-176 max-w-full mb-8 p-0;
> .dialog-content-container {
.flex(column, flex-start, flex-start);
@ -12,7 +12,7 @@
> .close-btn {
.flex(column, center, center);
@apply absolute top-4 right-4 w-6 h-6 rounded hover:bg-gray-200;
@apply absolute top-4 right-4 w-6 h-6 rounded hover:bg-gray-200 hover:shadow;
> .icon-img {
@apply w-5 h-5;
@ -20,10 +20,14 @@
}
> .section-selector-container {
@apply w-40 h-full shrink-0 rounded-l-lg p-4 bg-gray-100 flex flex-col justify-start items-start;
@apply w-40 h-full shrink-0 rounded-l-lg p-4 border-r bg-gray-100 flex flex-col justify-start items-start;
> .section-title {
@apply text-sm mt-4 first:mt-3 mb-1 font-mono text-gray-400;
}
> .section-item {
@apply text-base left-6 mt-2 mb-1 cursor-pointer hover:opacity-80;
@apply text-base left-6 mt-2 text-gray-700 cursor-pointer hover:opacity-80;
&.selected {
@apply font-bold hover:opacity-100;
@ -32,20 +36,19 @@
}
> .section-content-container {
@apply w-auto p-4 grow flex flex-col justify-start items-start;
@apply w-auto p-4 px-6 grow flex flex-col justify-start items-start h-128 overflow-y-scroll;
> .section-container {
.flex(column, flex-start, flex-start);
@apply w-full my-2;
> .title-text {
@apply text-base font-bold mb-2;
color: @text-black;
@apply text-sm mb-3 font-mono text-gray-500;
}
> .form-label {
.flex(row, flex-start, center);
@apply w-full text-sm mb-2;
@apply w-full mb-2;
> .normal-text {
@apply shrink-0 select-text;

View File

@ -0,0 +1,39 @@
@import "../mixin.less";
.member-section-container {
> .create-member-container {
@apply w-full flex flex-col justify-start items-start;
> .input-form-container {
@apply w-full mb-2 flex flex-row justify-start items-center;
> .field-text {
@apply text-sm text-gray-600 w-20 text-right pr-2;
}
> input {
@apply border rounded text-sm leading-6 shadow-inner py-1 px-2;
}
}
> .btns-container {
@apply w-full mb-6 pl-20 flex flex-row justify-start items-center;
> button {
@apply border text-sm py-1 px-3 rounded leading-6 shadow hover:opacity-80;
}
}
}
> .user-container {
@apply w-full mb-4 grid grid-cols-5;
> .field-text {
@apply text-base mr-4 w-16;
&.id-text {
@apply font-mono;
}
}
}
}

View File

@ -0,0 +1,63 @@
@import "../mixin.less";
.account-section-container {
> .form-label {
min-height: 28px;
> .normal-text {
@apply first:mr-2 text-base;
}
&.username-label {
> input {
@apply grow-0 shadow-inner w-auto px-2 py-1 text-base border rounded leading-6 bg-transparent focus:border-black;
}
> .btns-container {
.flex(row, flex-start, center);
@apply ml-2 shrink-0;
> .btn {
@apply text-sm shadow px-4 py-1 leading-6 rounded border hover:opacity-80 bg-gray-50;
&.cancel-btn {
@apply shadow-none bg-transparent;
}
&.confirm-btn {
@apply bg-green-600 text-white;
}
}
}
}
&.password-label {
> .btn {
@apply text-blue-600 ml-1 cursor-pointer hover:opacity-80;
}
}
}
}
.openapi-section-container {
> .value-text {
@apply w-full font-mono text-sm shadow-inner border py-2 px-3 rounded leading-6 break-all whitespace-pre-wrap;
}
> .reset-btn {
@apply mt-2 py-1 px-2 text-sm shadow bg-red-50 border border-red-500 text-red-600 rounded cursor-pointer select-none hover:opacity-80;
}
> .usage-guide-container {
.flex(column, flex-start, flex-start);
@apply mt-2 w-full;
> .title-text {
@apply my-2 text-sm;
}
> pre {
@apply w-full bg-gray-100 shadow-inner py-2 px-3 text-sm rounded font-mono break-all whitespace-pre-wrap;
}
}
}

View File

@ -0,0 +1,12 @@
@import "../mixin.less";
.preferences-section-container {
> .btns-container {
.flex(row, flex-start, center);
@apply w-full;
> .btn {
@apply border text-sm py-1 px-3 mr-2 rounded leading-6 shadow hover:opacity-80;
}
}
}

View File

@ -12,7 +12,7 @@
}
> .tag {
@apply text-xs px-1 bg-blue-500 rounded text-white;
@apply text-xs px-1 bg-blue-600 rounded text-white shadow;
}
}

View File

@ -89,11 +89,9 @@ const Signin: React.FC<Props> = () => {
return;
}
const name = email.split("@")[0];
try {
actionBtnLoadingState.setLoading();
await api.signup(email, "OWNER", name, password);
await api.signup(email, password, "OWNER");
const user = await userService.doSignIn();
if (user) {
locationService.replaceHistory("/");

View File

@ -2,4 +2,11 @@ declare namespace API {
interface SystemStatus {
owner: Model.User;
}
interface UserCreate {
email: string;
password: string;
name: string;
role: UserRole;
}
}

View File

@ -16,6 +16,7 @@ module.exports = {
spacing: {
128: "32rem",
168: "42rem",
176: "44rem",
200: "50rem",
},
zIndex: {