diff --git a/api/user.go b/api/user.go index 708c6267..bb867272 100644 --- a/api/user.go +++ b/api/user.go @@ -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 } diff --git a/server/auth.go b/server/auth.go index c65849ea..928540e0 100644 --- a/server/auth.go +++ b/server/auth.go @@ -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) } diff --git a/server/user.go b/server/user.go index 0c73d84c..354318e4 100644 --- a/server/user.go +++ b/server/user.go @@ -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()) diff --git a/store/migration/10001__schema.sql b/store/migration/10001__schema.sql index 0823e7da..501086ed 100644 --- a/store/migration/10001__schema.sql +++ b/store/migration/10001__schema.sql @@ -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 diff --git a/store/user.go b/store/user.go index 5793796a..5b9834f8 100644 --- a/store/user.go +++ b/store/user.go @@ -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 { diff --git a/web/src/components/MemoEditor.tsx b/web/src/components/MemoEditor.tsx index e5fd5cf7..f7692ccc 100644 --- a/web/src/components/MemoEditor.tsx +++ b/web/src/components/MemoEditor.tsx @@ -246,7 +246,7 @@ const MemoEditor: React.FC = () => { 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 = () => { } }, []); - const showEditStatus = Boolean(globalState.editMemoId); + const isEditing = Boolean(globalState.editMemoId); const editorConfig = useMemo( () => ({ @@ -267,17 +267,17 @@ const MemoEditor: React.FC = () => { initialContent: getEditorContentCache(), placeholder: "Any thoughts...", showConfirmBtn: true, - showCancelBtn: showEditStatus, + showCancelBtn: isEditing, onConfirmBtnClick: handleSaveBtnClick, onCancelBtnClick: handleCancelBtnClick, onContentChange: handleContentChange, }), - [showEditStatus] + [isEditing] ); return ( -
-

Editting...

+
+

Editting...

= () => { - 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 ( - <> -
-

Memo Display

-
- - - -
-
-

Others

-
- - -
-
- - ); -}; - -export default PreferencesSection; diff --git a/web/src/components/SettingDialog.tsx b/web/src/components/SettingDialog.tsx index cfb600f8..142d0555 100644 --- a/web/src/components/SettingDialog.tsx +++ b/web/src/components/SettingDialog.tsx @@ -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) => { + const { + userState: { user }, + } = useContext(appContext); const { destroy } = props; const [state, setState] = useState({ selectedSection: "my-account", @@ -30,6 +35,7 @@ const SettingDialog: React.FC = (props: Props) => {
+ Basic handleSectionSelectorItemClick("my-account")} className={`section-item ${state.selectedSection === "my-account" ? "selected" : ""}`} @@ -42,12 +48,25 @@ const SettingDialog: React.FC = (props: Props) => { > Preferences + {user?.role === "OWNER" ? ( + <> + Admin + handleSectionSelectorItemClick("member")} + className={`section-item ${state.selectedSection === "member" ? "selected" : ""}`} + > + Member + + + ) : null}
{state.selectedSection === "my-account" ? ( ) : state.selectedSection === "preferences" ? ( + ) : state.selectedSection === "member" ? ( + ) : null}
diff --git a/web/src/components/Settings/MemberSection.tsx b/web/src/components/Settings/MemberSection.tsx new file mode 100644 index 00000000..2b54a753 --- /dev/null +++ b/web/src/components/Settings/MemberSection.tsx @@ -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 = () => { + const [state, setState] = useState({ + createUserEmail: "", + createUserPassword: "", + }); + const [userList, setUserList] = useState([]); + + useEffect(() => { + fetchUserList(); + }, []); + + const fetchUserList = async () => { + const data = await api.getUserList(); + setUserList(data); + }; + + const handleEmailInputChange = (event: React.ChangeEvent) => { + setState({ + ...state, + createUserEmail: event.target.value, + }); + }; + + const handlePasswordInputChange = (event: React.ChangeEvent) => { + 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 ( +
+

Create a member

+
+
+ Email + +
+
+ Password + +
+
+ +
+
+

Member list

+ {userList.map((user) => ( +
+ {user.id} + {user.email} +
+ ))} +
+ ); +}; + +export default PreferencesSection; diff --git a/web/src/components/MyAccountSection.tsx b/web/src/components/Settings/MyAccountSection.tsx similarity index 89% rename from web/src/components/MyAccountSection.tsx rename to web/src/components/Settings/MyAccountSection.tsx index 066a73b2..c2230241 100644 --- a/web/src/components/MyAccountSection.tsx +++ b/web/src/components/Settings/MyAccountSection.tsx @@ -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, diff --git a/web/src/components/Settings/PreferencesSection.tsx b/web/src/components/Settings/PreferencesSection.tsx new file mode 100644 index 00000000..187c5c67 --- /dev/null +++ b/web/src/components/Settings/PreferencesSection.tsx @@ -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 = () => { + 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 ( +
+

Others

+
+ + +
+
+ ); +}; + +export default PreferencesSection; diff --git a/web/src/helpers/api.ts b/web/src/helpers/api.ts index 5e9a9c6c..43b395f1 100644 --- a/web/src/helpers/api.ts +++ b/web/src/helpers/api.ts @@ -48,10 +48,18 @@ namespace api { }); } - export function getUserInfo() { - return request({ + export function getUserList() { + return request({ method: "GET", - url: "/api/user/me", + url: "/api/user", + }); + } + + export function createUser(userCreate: API.UserCreate) { + return request({ + 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({ 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({ - method: "POST", - url: "/api/user/rename_check", - data: { - name, - }, - }); - } - - export function checkPasswordValid(password: string) { - return request({ - method: "POST", - url: "/api/user/password_check", - data: { - password, - }, + export function getUserInfo() { + return request({ + method: "GET", + url: "/api/user/me", }); } diff --git a/web/src/less/dialog.less b/web/src/less/dialog.less index 80114503..07634827 100644 --- a/web/src/less/dialog.less +++ b/web/src/less/dialog.less @@ -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; - } } } diff --git a/web/src/less/global.less b/web/src/less/global.less index 72facbaa..2f33b13c 100644 --- a/web/src/less/global.less +++ b/web/src/less/global.less @@ -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 { diff --git a/web/src/less/my-account-section.less b/web/src/less/my-account-section.less deleted file mode 100644 index b2de21e7..00000000 --- a/web/src/less/my-account-section.less +++ /dev/null @@ -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; - } - } -} diff --git a/web/src/less/preferences-section.less b/web/src/less/preferences-section.less deleted file mode 100644 index bc76823d..00000000 --- a/web/src/less/preferences-section.less +++ /dev/null @@ -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; - } - } - } -} diff --git a/web/src/less/setting-dialog.less b/web/src/less/setting-dialog.less index d194de2c..41990c6e 100644 --- a/web/src/less/setting-dialog.less +++ b/web/src/less/setting-dialog.less @@ -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; diff --git a/web/src/less/settings/member-section.less b/web/src/less/settings/member-section.less new file mode 100644 index 00000000..bad99134 --- /dev/null +++ b/web/src/less/settings/member-section.less @@ -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; + } + } + } +} diff --git a/web/src/less/settings/my-account-section.less b/web/src/less/settings/my-account-section.less new file mode 100644 index 00000000..56af4d33 --- /dev/null +++ b/web/src/less/settings/my-account-section.less @@ -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; + } + } +} diff --git a/web/src/less/settings/preferences-section.less b/web/src/less/settings/preferences-section.less new file mode 100644 index 00000000..aff3a6a1 --- /dev/null +++ b/web/src/less/settings/preferences-section.less @@ -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; + } + } +} diff --git a/web/src/less/user-banner.less b/web/src/less/user-banner.less index c0ce978e..ce4a6515 100644 --- a/web/src/less/user-banner.less +++ b/web/src/less/user-banner.less @@ -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; } } diff --git a/web/src/pages/Signin.tsx b/web/src/pages/Signin.tsx index 26d24a86..10ae6304 100644 --- a/web/src/pages/Signin.tsx +++ b/web/src/pages/Signin.tsx @@ -89,11 +89,9 @@ const Signin: React.FC = () => { 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("/"); diff --git a/web/src/types/api.d.ts b/web/src/types/api.d.ts index 10884c05..1b765692 100644 --- a/web/src/types/api.d.ts +++ b/web/src/types/api.d.ts @@ -2,4 +2,11 @@ declare namespace API { interface SystemStatus { owner: Model.User; } + + interface UserCreate { + email: string; + password: string; + name: string; + role: UserRole; + } } diff --git a/web/tailwind.config.js b/web/tailwind.config.js index a574b203..5279c358 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -16,6 +16,7 @@ module.exports = { spacing: { 128: "32rem", 168: "42rem", + 176: "44rem", 200: "50rem", }, zIndex: {