Commit c492317f authored by boojack's avatar boojack

feat: member manage section in setting dialog

parent fbf4afff
...@@ -27,9 +27,10 @@ type User struct { ...@@ -27,9 +27,10 @@ type User struct {
type UserCreate struct { type UserCreate struct {
// Domain specific fields // Domain specific fields
Email string Email string `json:"email"`
Role Role Role Role `json:"role"`
Name string Name string `json:"name"`
Password string `json:"password"`
PasswordHash string PasswordHash string
OpenID string OpenID string
} }
......
...@@ -81,24 +81,10 @@ func (s *Server) registerAuthRoutes(g *echo.Group) { ...@@ -81,24 +81,10 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
if len(signup.Email) < 6 { if len(signup.Email) < 6 {
return echo.NewHTTPError(http.StatusBadRequest, "Email is too short, minimum length is 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 { if len(signup.Password) < 6 {
return echo.NewHTTPError(http.StatusBadRequest, "Password is too short, minimum length is 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) passwordHash, err := bcrypt.GenerateFromPassword([]byte(signup.Password), bcrypt.DefaultCost)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
...@@ -111,7 +97,7 @@ func (s *Server) registerAuthRoutes(g *echo.Group) { ...@@ -111,7 +97,7 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
PasswordHash: string(passwordHash), PasswordHash: string(passwordHash),
OpenID: common.GenUUID(), OpenID: common.GenUUID(),
} }
user, err = s.Store.CreateUser(userCreate) user, err := s.Store.CreateUser(userCreate)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
} }
......
...@@ -12,6 +12,45 @@ import ( ...@@ -12,6 +12,45 @@ import (
) )
func (s *Server) registerUserRoutes(g *echo.Group) { 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. // GET /api/user/me is used to check if the user is logged in.
g.GET("/user/me", func(c echo.Context) error { g.GET("/user/me", func(c echo.Context) error {
userSessionID := c.Get(getUserIDContextKey()) userSessionID := c.Get(getUserIDContextKey())
......
-- user -- user
CREATE TABLE user ( CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT, 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', role TEXT NOT NULL CHECK (role IN ('OWNER', 'USER')) DEFAULT 'USER',
name TEXT NOT NULL, name TEXT NOT NULL,
password_hash 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')), created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
UNIQUE(`email`, `open_id`)
); );
INSERT INTO INSERT INTO
......
...@@ -25,6 +25,15 @@ func (s *Store) PatchUser(patch *api.UserPatch) (*api.User, error) { ...@@ -25,6 +25,15 @@ func (s *Store) PatchUser(patch *api.UserPatch) (*api.User, error) {
return user, nil 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) { func (s *Store) FindUser(find *api.UserFind) (*api.User, error) {
list, err := findUserList(s.db, find) list, err := findUserList(s.db, find)
if err != nil { if err != nil {
......
...@@ -246,7 +246,7 @@ const MemoEditor: React.FC<Props> = () => { ...@@ -246,7 +246,7 @@ const MemoEditor: React.FC<Props> = () => {
const file = inputEl.files[0]; const file = inputEl.files[0];
const url = await handleUploadFile(file); const url = await handleUploadFile(file);
if (url) { if (url) {
editorRef.current?.insertText(url); editorRef.current?.insertText(url + " ");
} }
}; };
inputEl.click(); inputEl.click();
...@@ -259,7 +259,7 @@ const MemoEditor: React.FC<Props> = () => { ...@@ -259,7 +259,7 @@ const MemoEditor: React.FC<Props> = () => {
} }
}, []); }, []);
const showEditStatus = Boolean(globalState.editMemoId); const isEditing = Boolean(globalState.editMemoId);
const editorConfig = useMemo( const editorConfig = useMemo(
() => ({ () => ({
...@@ -267,17 +267,17 @@ const MemoEditor: React.FC<Props> = () => { ...@@ -267,17 +267,17 @@ const MemoEditor: React.FC<Props> = () => {
initialContent: getEditorContentCache(), initialContent: getEditorContentCache(),
placeholder: "Any thoughts...", placeholder: "Any thoughts...",
showConfirmBtn: true, showConfirmBtn: true,
showCancelBtn: showEditStatus, showCancelBtn: isEditing,
onConfirmBtnClick: handleSaveBtnClick, onConfirmBtnClick: handleSaveBtnClick,
onCancelBtnClick: handleCancelBtnClick, onCancelBtnClick: handleCancelBtnClick,
onContentChange: handleContentChange, onContentChange: handleContentChange,
}), }),
[showEditStatus] [isEditing]
); );
return ( return (
<div className={"memo-editor-container " + (showEditStatus ? "edit-ing" : "")}> <div className={"memo-editor-container " + (isEditing ? "edit-ing" : "")}>
<p className={"tip-text " + (showEditStatus ? "" : "hidden")}>Editting...</p> <p className={"tip-text " + (isEditing ? "" : "hidden")}>Editting...</p>
<Editor <Editor
ref={editorRef} ref={editorRef}
{...editorConfig} {...editorConfig}
......
import { useState } from "react"; import { useContext, useState } from "react";
import appContext from "../stores/appContext";
import { showDialog } from "./Dialog"; import { showDialog } from "./Dialog";
import MyAccountSection from "./MyAccountSection"; import MyAccountSection from "./Settings/MyAccountSection";
import PreferencesSection from "./PreferencesSection"; import PreferencesSection from "./Settings/PreferencesSection";
import MemberSection from "./Settings/MemberSection";
import "../less/setting-dialog.less"; import "../less/setting-dialog.less";
interface Props extends DialogProps {} interface Props extends DialogProps {}
type SettingSection = "my-account" | "preferences"; type SettingSection = "my-account" | "preferences" | "member";
interface State { interface State {
selectedSection: SettingSection; selectedSection: SettingSection;
} }
const SettingDialog: React.FC<Props> = (props: Props) => { const SettingDialog: React.FC<Props> = (props: Props) => {
const {
userState: { user },
} = useContext(appContext);
const { destroy } = props; const { destroy } = props;
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
selectedSection: "my-account", selectedSection: "my-account",
...@@ -30,6 +35,7 @@ const SettingDialog: React.FC<Props> = (props: Props) => { ...@@ -30,6 +35,7 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
<img className="icon-img" src="/icons/close.svg" /> <img className="icon-img" src="/icons/close.svg" />
</button> </button>
<div className="section-selector-container"> <div className="section-selector-container">
<span className="section-title">Basic</span>
<span <span
onClick={() => handleSectionSelectorItemClick("my-account")} onClick={() => handleSectionSelectorItemClick("my-account")}
className={`section-item ${state.selectedSection === "my-account" ? "selected" : ""}`} className={`section-item ${state.selectedSection === "my-account" ? "selected" : ""}`}
...@@ -42,12 +48,25 @@ const SettingDialog: React.FC<Props> = (props: Props) => { ...@@ -42,12 +48,25 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
> >
Preferences Preferences
</span> </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>
<div className="section-content-container"> <div className="section-content-container">
{state.selectedSection === "my-account" ? ( {state.selectedSection === "my-account" ? (
<MyAccountSection /> <MyAccountSection />
) : state.selectedSection === "preferences" ? ( ) : state.selectedSection === "preferences" ? (
<PreferencesSection /> <PreferencesSection />
) : state.selectedSection === "member" ? (
<MemberSection />
) : null} ) : null}
</div> </div>
</div> </div>
......
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;
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import appContext from "../stores/appContext"; import appContext from "../../stores/appContext";
import { userService } from "../services"; import { userService } from "../../services";
import utils from "../helpers/utils"; import utils from "../../helpers/utils";
import { validate, ValidatorConfig } from "../helpers/validator"; import { validate, ValidatorConfig } from "../../helpers/validator";
import toastHelper from "./Toast"; import toastHelper from "../Toast";
import showChangePasswordDialog from "./ChangePasswordDialog"; import showChangePasswordDialog from "../ChangePasswordDialog";
import showConfirmResetOpenIdDialog from "./ConfirmResetOpenIdDialog"; import showConfirmResetOpenIdDialog from "../ConfirmResetOpenIdDialog";
import "../less/my-account-section.less"; import "../../less/settings/my-account-section.less";
const validateConfig: ValidatorConfig = { const validateConfig: ValidatorConfig = {
minLength: 4, minLength: 4,
......
import { useContext } from "react"; import { memoService } from "../../services";
import appContext from "../stores/appContext"; import utils from "../../helpers/utils";
import { globalStateService, memoService } from "../services"; import toastHelper from "../Toast";
import utils from "../helpers/utils"; import "../../less/settings/preferences-section.less";
import { formatMemoContent } from "./Memo";
import toastHelper from "./Toast";
import "../less/preferences-section.less";
interface Props {} interface Props {}
const PreferencesSection: React.FC<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 handleExportBtnClick = async () => {
const formatedMemos = memoService.getState().memos.map((m) => { const formatedMemos = memoService.getState().memos.map((m) => {
return { return {
...@@ -87,38 +61,17 @@ const PreferencesSection: React.FC<Props> = () => { ...@@ -87,38 +61,17 @@ const PreferencesSection: React.FC<Props> = () => {
}; };
return ( return (
<> <div className="section-container preferences-section-container">
<div className="section-container preferences-section-container"> <p className="title-text">Others</p>
<p className="title-text">Memo Display</p> <div className="btns-container">
<div <button className="btn" onClick={handleExportBtnClick}>
className="demo-content-container memo-content-text" Export data as JSON
dangerouslySetInnerHTML={{ __html: formatMemoContent(demoMemoContent) }} </button>
></div> <button className="btn" onClick={handleImportBtnClick}>
<label className="form-label checkbox-form-label hidden" onClick={handleSplitWordsValueChanged}> Import from JSON
<span className="normal-text">Auto-space in English and Chinese</span> </button>
<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> </div>
</> </div>
); );
}; };
......
...@@ -48,10 +48,18 @@ namespace api { ...@@ -48,10 +48,18 @@ namespace api {
}); });
} }
export function getUserInfo() { export function getUserList() {
return request<Model.User>({ return request<Model.User[]>({
method: "GET", 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 { ...@@ -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>({ return request<Model.User>({
method: "POST", method: "POST",
url: "/api/auth/signup", url: "/api/auth/signup",
data: { data: {
email, email,
role,
name,
password, password,
role,
name: email,
}, },
}); });
} }
...@@ -86,23 +94,10 @@ namespace api { ...@@ -86,23 +94,10 @@ namespace api {
}); });
} }
export function checkUsernameUsable(name: string) { export function getUserInfo() {
return request<boolean>({ return request<Model.User>({
method: "POST", method: "GET",
url: "/api/user/rename_check", url: "/api/user/me",
data: {
name,
},
});
}
export function checkPasswordValid(password: string) {
return request<boolean>({
method: "POST",
url: "/api/user/password_check",
data: {
password,
},
}); });
} }
......
...@@ -29,15 +29,11 @@ ...@@ -29,15 +29,11 @@
.btn { .btn {
.flex(column, center, center); .flex(column, center, center);
@apply w-6 h-6 rounded; @apply w-6 h-6 rounded hover:bg-gray-200 hover:shadow;
> .icon-img { > .icon-img {
@apply w-5 h-5; @apply w-5 h-5;
} }
&:hover {
@apply bg-gray-200;
}
} }
} }
......
...@@ -66,10 +66,6 @@ a { ...@@ -66,10 +66,6 @@ a {
.btn { .btn {
@apply select-none cursor-pointer text-center; @apply select-none cursor-pointer text-center;
border: unset;
background-color: unset;
text-align: unset;
font-size: unset;
} }
.hidden { .hidden {
......
@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;
}
}
}
}
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.setting-dialog { .setting-dialog {
> .dialog-container { > .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 { > .dialog-content-container {
.flex(column, flex-start, flex-start); .flex(column, flex-start, flex-start);
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
> .close-btn { > .close-btn {
.flex(column, center, center); .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 { > .icon-img {
@apply w-5 h-5; @apply w-5 h-5;
...@@ -20,10 +20,14 @@ ...@@ -20,10 +20,14 @@
} }
> .section-selector-container { > .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 { > .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 { &.selected {
@apply font-bold hover:opacity-100; @apply font-bold hover:opacity-100;
...@@ -32,20 +36,19 @@ ...@@ -32,20 +36,19 @@
} }
> .section-content-container { > .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 { > .section-container {
.flex(column, flex-start, flex-start); .flex(column, flex-start, flex-start);
@apply w-full my-2; @apply w-full my-2;
> .title-text { > .title-text {
@apply text-base font-bold mb-2; @apply text-sm mb-3 font-mono text-gray-500;
color: @text-black;
} }
> .form-label { > .form-label {
.flex(row, flex-start, center); .flex(row, flex-start, center);
@apply w-full text-sm mb-2; @apply w-full mb-2;
> .normal-text { > .normal-text {
@apply shrink-0 select-text; @apply shrink-0 select-text;
......
@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;
}
}
}
}
@import "./mixin.less"; @import "../mixin.less";
.account-section-container { .account-section-container {
> .form-label { > .form-label {
min-height: 28px; min-height: 28px;
> .normal-text { > .normal-text {
@apply first:mr-2; @apply first:mr-2 text-base;
} }
&.username-label { &.username-label {
> input { > input {
flex-grow: 0; @apply grow-0 shadow-inner w-auto px-2 py-1 text-base border rounded leading-6 bg-transparent focus:border-black;
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 { > .btns-container {
.flex(row, flex-start, center); .flex(row, flex-start, center);
margin-left: 8px; @apply ml-2 shrink-0;
flex-shrink: 0;
> .btn { > .btn {
font-size: 12px; @apply text-sm shadow px-4 py-1 leading-6 rounded border hover:opacity-80 bg-gray-50;
padding: 0 16px;
border-radius: 4px;
line-height: 28px;
margin-right: 8px;
background-color: lightgray;
&:hover {
opacity: 0.8;
}
&.cancel-btn { &.cancel-btn {
background-color: unset; @apply shadow-none bg-transparent;
} }
&.confirm-btn { &.confirm-btn {
background-color: @text-green; @apply bg-green-600 text-white;
color: white;
} }
} }
} }
...@@ -63,17 +41,11 @@ ...@@ -63,17 +41,11 @@
.openapi-section-container { .openapi-section-container {
> .value-text { > .value-text {
width: 100%; @apply w-full font-mono text-sm shadow-inner border py-2 px-3 rounded leading-6 break-all whitespace-pre-wrap;
border: 1px solid lightgray;
padding: 4px 6px;
border-radius: 4px;
line-height: 1.6;
word-break: break-all;
white-space: pre-wrap;
} }
> .reset-btn { > .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; @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 { > .usage-guide-container {
...@@ -85,7 +57,7 @@ ...@@ -85,7 +57,7 @@
} }
> pre { > pre {
@apply w-full bg-gray-50 py-2 px-3 text-sm rounded whitespace-pre-wrap; @apply w-full bg-gray-100 shadow-inner py-2 px-3 text-sm rounded font-mono break-all whitespace-pre-wrap;
} }
} }
} }
@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;
}
}
}
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
} }
> .tag { > .tag {
@apply text-xs px-1 bg-blue-500 rounded text-white; @apply text-xs px-1 bg-blue-600 rounded text-white shadow;
} }
} }
......
...@@ -89,11 +89,9 @@ const Signin: React.FC<Props> = () => { ...@@ -89,11 +89,9 @@ const Signin: React.FC<Props> = () => {
return; return;
} }
const name = email.split("@")[0];
try { try {
actionBtnLoadingState.setLoading(); actionBtnLoadingState.setLoading();
await api.signup(email, "OWNER", name, password); await api.signup(email, password, "OWNER");
const user = await userService.doSignIn(); const user = await userService.doSignIn();
if (user) { if (user) {
locationService.replaceHistory("/"); locationService.replaceHistory("/");
......
...@@ -2,4 +2,11 @@ declare namespace API { ...@@ -2,4 +2,11 @@ declare namespace API {
interface SystemStatus { interface SystemStatus {
owner: Model.User; owner: Model.User;
} }
interface UserCreate {
email: string;
password: string;
name: string;
role: UserRole;
}
} }
...@@ -16,6 +16,7 @@ module.exports = { ...@@ -16,6 +16,7 @@ module.exports = {
spacing: { spacing: {
128: "32rem", 128: "32rem",
168: "42rem", 168: "42rem",
176: "44rem",
200: "50rem", 200: "50rem",
}, },
zIndex: { zIndex: {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment