mirror of
https://github.com/aykhans/slash-e.git
synced 2025-04-16 12:23:12 +00:00
chore: add user detail page
This commit is contained in:
parent
90639322b7
commit
156218b40f
@ -54,8 +54,8 @@ type UserPatch struct {
|
|||||||
Email *string `json:"email"`
|
Email *string `json:"email"`
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
Password *string `json:"password"`
|
Password *string `json:"password"`
|
||||||
|
ResetOpenID *bool `json:"resetOpenId"`
|
||||||
PasswordHash *string
|
PasswordHash *string
|
||||||
ResetOpenID *bool `json:"resetOpenId"`
|
|
||||||
OpenID *string
|
OpenID *string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import Only from "./components/common/OnlyWhen";
|
|||||||
import Auth from "./pages/Auth";
|
import Auth from "./pages/Auth";
|
||||||
import Home from "./pages/Home";
|
import Home from "./pages/Home";
|
||||||
import WorkspaceDetail from "./pages/WorkspaceDetail";
|
import WorkspaceDetail from "./pages/WorkspaceDetail";
|
||||||
|
import UserDetail from "./pages/UserDetail";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -30,6 +31,7 @@ function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route index element={<Home />} />
|
<Route index element={<Home />} />
|
||||||
<Route path="/auth" element={<Auth />} />
|
<Route path="/auth" element={<Auth />} />
|
||||||
|
<Route path="/user/:userId" element={<UserDetail />} />
|
||||||
<Route path="/workspace/:workspaceId" element={<WorkspaceDetail />} />
|
<Route path="/workspace/:workspaceId" element={<WorkspaceDetail />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Only>
|
</Only>
|
||||||
|
@ -16,7 +16,9 @@ const Header: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="w-full bg-amber-50">
|
<div className="w-full bg-amber-50">
|
||||||
<div className="w-full max-w-4xl mx-auto px-3 py-4 flex flex-row justify-between items-center">
|
<div className="w-full max-w-4xl mx-auto px-3 py-4 flex flex-row justify-between items-center">
|
||||||
<span className="text-xl font-mono font-medium">Corgi</span>
|
<span className="text-xl font-mono font-medium cursor-pointer" onClick={() => navigate("/")}>
|
||||||
|
Corgi
|
||||||
|
</span>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
trigger={
|
trigger={
|
||||||
@ -27,11 +29,21 @@ const Header: React.FC = () => {
|
|||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<span className="w-full px-2 leading-8 cursor-pointer rounded hover:bg-gray-100" onClick={() => handleSignOutButtonClick()}>
|
<span
|
||||||
|
className="w-full px-3 leading-8 cursor-pointer rounded whitespace-nowrap hover:bg-gray-100"
|
||||||
|
onClick={() => navigate(`/user/${user?.id}`)}
|
||||||
|
>
|
||||||
|
My information
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="w-full px-3 leading-8 cursor-pointer rounded whitespace-nowrap hover:bg-gray-100"
|
||||||
|
onClick={() => handleSignOutButtonClick()}
|
||||||
|
>
|
||||||
Sign out
|
Sign out
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
actionsClassName="!w-36"
|
||||||
></Dropdown>
|
></Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,6 +45,7 @@ const ShortcutListView: React.FC<Props> = (props: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
actionsClassName="!w-24"
|
||||||
></Dropdown>
|
></Dropdown>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -49,6 +49,7 @@ const WorkspaceListView: React.FC<Props> = (props: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
actionsClassName="!w-24"
|
||||||
></Dropdown>
|
></Dropdown>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import { ReactNode, useEffect, useRef } from "react";
|
import { ReactNode, useEffect, useRef } from "react";
|
||||||
import useToggle from "../../hooks/useToggle";
|
import useToggle from "../../hooks/useToggle";
|
||||||
import Icon from "../Icon";
|
import Icon from "../Icon";
|
||||||
import "../../less/common/dropdown.less";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
trigger?: ReactNode;
|
trigger?: ReactNode;
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
actionsClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Dropdown: React.FC<Props> = (props: Props) => {
|
const Dropdown: React.FC<Props> = (props: Props) => {
|
||||||
const { trigger, actions, className } = props;
|
const { trigger, actions, className, actionsClassName } = props;
|
||||||
const [dropdownStatus, toggleDropdownStatus] = useToggle(false);
|
const [dropdownStatus, toggleDropdownStatus] = useToggle(false);
|
||||||
const dropdownWrapperRef = useRef<HTMLDivElement>(null);
|
const dropdownWrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@ -29,15 +29,25 @@ const Dropdown: React.FC<Props> = (props: Props) => {
|
|||||||
}, [dropdownStatus]);
|
}, [dropdownStatus]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={dropdownWrapperRef} className={`dropdown-wrapper ${className ?? ""}`} onClick={() => toggleDropdownStatus()}>
|
<div
|
||||||
|
ref={dropdownWrapperRef}
|
||||||
|
className={`relative flex flex-col justify-start items-start select-none ${className ?? ""}`}
|
||||||
|
onClick={() => toggleDropdownStatus()}
|
||||||
|
>
|
||||||
{trigger ? (
|
{trigger ? (
|
||||||
trigger
|
trigger
|
||||||
) : (
|
) : (
|
||||||
<span className="trigger-button">
|
<span className="flex flex-row justify-center items-center border p-1 rounded shadow text-gray-600 cursor-pointer hover:opacity-80">
|
||||||
<Icon.MoreHorizontal className="icon-img" />
|
<Icon.MoreHorizontal className="w-4 h-auto" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div className={`action-buttons-container ${dropdownStatus ? "" : "!hidden"}`}>{actions}</div>
|
<div
|
||||||
|
className={`w-auto mt-1 absolute top-full right-0 flex flex-col justify-start items-start bg-white z-1 border p-1 rounded-md shadow ${
|
||||||
|
actionsClassName ?? ""
|
||||||
|
} ${dropdownStatus ? "" : "!hidden"}`}
|
||||||
|
>
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,95 +0,0 @@
|
|||||||
import { memo, useEffect, useRef } from "react";
|
|
||||||
import useToggle from "../../hooks/useToggle";
|
|
||||||
import Icon from "../Icon";
|
|
||||||
import "../../less/common/selector.less";
|
|
||||||
|
|
||||||
interface SelectorItem {
|
|
||||||
text: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
className?: string;
|
|
||||||
value: string;
|
|
||||||
dataSource: SelectorItem[];
|
|
||||||
handleValueChanged?: (value: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nullItem = {
|
|
||||||
text: "Select",
|
|
||||||
value: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
const Selector: React.FC<Props> = (props: Props) => {
|
|
||||||
const { className, dataSource, handleValueChanged, value } = props;
|
|
||||||
const [showSelector, toggleSelectorStatus] = useToggle(false);
|
|
||||||
|
|
||||||
const seletorElRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
let currentItem = nullItem;
|
|
||||||
for (const d of dataSource) {
|
|
||||||
if (d.value === value) {
|
|
||||||
currentItem = d;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (showSelector) {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (!seletorElRef.current?.contains(event.target as Node)) {
|
|
||||||
toggleSelectorStatus(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener("click", handleClickOutside, {
|
|
||||||
capture: true,
|
|
||||||
once: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [showSelector]);
|
|
||||||
|
|
||||||
const handleItemClick = (item: SelectorItem) => {
|
|
||||||
if (handleValueChanged) {
|
|
||||||
handleValueChanged(item.value);
|
|
||||||
}
|
|
||||||
toggleSelectorStatus(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCurrentValueClick = (event: React.MouseEvent) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
toggleSelectorStatus();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`selector-wrapper ${className ?? ""}`} ref={seletorElRef}>
|
|
||||||
<div className={`current-value-container ${showSelector ? "active" : ""}`} onClick={handleCurrentValueClick}>
|
|
||||||
<span className="value-text">{currentItem.text}</span>
|
|
||||||
<span className="arrow-text">
|
|
||||||
<Icon.ChevronDown className="icon-img" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`items-wrapper ${showSelector ? "" : "!hidden"}`}>
|
|
||||||
{dataSource.length > 0 ? (
|
|
||||||
dataSource.map((d) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`item-container ${d.value === value ? "selected" : ""}`}
|
|
||||||
key={d.value}
|
|
||||||
onClick={() => {
|
|
||||||
handleItemClick(d);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{d.text}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<p className="tip-text">Null</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(Selector);
|
|
@ -1,15 +0,0 @@
|
|||||||
import { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
const useRefresh = () => {
|
|
||||||
const [, setBoolean] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const refresh = useCallback(() => {
|
|
||||||
setBoolean((ps) => {
|
|
||||||
return !ps;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return refresh;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useRefresh;
|
|
@ -1,19 +0,0 @@
|
|||||||
.dropdown-wrapper {
|
|
||||||
@apply relative flex flex-col justify-start items-start select-none;
|
|
||||||
|
|
||||||
> .trigger-button {
|
|
||||||
@apply flex flex-row justify-center items-center border p-1 rounded shadow text-gray-600 cursor-pointer hover:opacity-80;
|
|
||||||
|
|
||||||
> .icon-img {
|
|
||||||
@apply w-4 h-auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .action-buttons-container {
|
|
||||||
@apply w-28 mt-1 absolute top-full right-0 flex flex-col justify-start items-start bg-white z-1 border p-1 rounded shadow;
|
|
||||||
|
|
||||||
> button {
|
|
||||||
@apply w-full text-left px-2 text-sm leading-7 rounded hover:bg-gray-100;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
.selector-wrapper {
|
|
||||||
@apply flex flex-col justify-start items-start relative h-8;
|
|
||||||
|
|
||||||
> .current-value-container {
|
|
||||||
@apply flex flex-row justify-between items-center w-full h-full rounded px-2 pr-1 bg-white border cursor-pointer select-none;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&.active {
|
|
||||||
@apply bg-gray-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .value-text {
|
|
||||||
@apply text-sm mr-0 truncate;
|
|
||||||
width: calc(100% - 20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
> .arrow-text {
|
|
||||||
@apply flex flex-row justify-center items-center w-4 shrink-0;
|
|
||||||
|
|
||||||
> .icon-img {
|
|
||||||
@apply w-4 h-auto opacity-40;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .items-wrapper {
|
|
||||||
@apply flex flex-col justify-start items-start absolute top-full left-0 w-auto p-1 mt-1 -ml-2 bg-white rounded-md overflow-y-auto z-1;
|
|
||||||
min-width: calc(100% + 16px);
|
|
||||||
max-height: 256px;
|
|
||||||
box-shadow: 0 0 8px 0 rgb(0 0 0 / 20%);
|
|
||||||
|
|
||||||
> .item-container {
|
|
||||||
@apply flex flex-col justify-start items-start w-full px-3 text-sm select-none leading-8 cursor-pointer rounded whitespace-nowrap hover:bg-gray-100;
|
|
||||||
|
|
||||||
&.selected {
|
|
||||||
color: @text-green;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .tip-text {
|
|
||||||
@apply px-3 py-1 text-sm text-gray-600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -22,14 +22,16 @@ const Home: React.FC = () => {
|
|||||||
<Header />
|
<Header />
|
||||||
{loadingState.isLoading ? null : (
|
{loadingState.isLoading ? null : (
|
||||||
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start">
|
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start">
|
||||||
<p className="font-mono mb-2 text-gray-400">Workspace List</p>
|
<div className="mb-4 w-full flex flex-row justify-between items-center">
|
||||||
<WorkspaceListView workspaceList={workspaceList} />
|
<span className="font-mono text-gray-400">Workspace List</span>
|
||||||
<div
|
<div
|
||||||
className="flex flex-row justify-start items-center border px-3 py-3 rounded-lg cursor-pointer"
|
className="text-sm flex flex-row justify-start items-center border px-3 py-2 rounded-lg cursor-pointer hover:shadow"
|
||||||
onClick={() => showCreateWorkspaceDialog()}
|
onClick={() => showCreateWorkspaceDialog()}
|
||||||
>
|
>
|
||||||
<Icon.Plus className="w-5 h-auto mr-1" /> Create Workspace
|
<Icon.Plus className="w-5 h-auto mr-1" /> Create Workspace
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<WorkspaceListView workspaceList={workspaceList} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
19
web/src/pages/UserDetail.tsx
Normal file
19
web/src/pages/UserDetail.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { useAppSelector } from "../store";
|
||||||
|
import Header from "../components/Header";
|
||||||
|
|
||||||
|
const UserDetail: React.FC = () => {
|
||||||
|
const { user } = useAppSelector((state) => state.user);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex flex-col justify-start items-start">
|
||||||
|
<Header />
|
||||||
|
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start">
|
||||||
|
<p className="text-3xl mt-2 mb-4">{user?.name}</p>
|
||||||
|
<p className="leading-10">Email: {user?.email}</p>
|
||||||
|
<p className="leading-10">OpenID: {user?.openId}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserDetail;
|
@ -49,22 +49,22 @@ const WorkspaceDetail: React.FC = () => {
|
|||||||
{loadingState.isLoading ? null : (
|
{loadingState.isLoading ? null : (
|
||||||
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start">
|
<div className="mx-auto max-w-4xl w-full px-3 py-6 flex flex-col justify-start items-start">
|
||||||
<div className="w-full flex flex-row justify-start items-center mb-4">
|
<div className="w-full flex flex-row justify-start items-center mb-4">
|
||||||
|
<span className="font-mono text-gray-600 cursor-pointer hover:underline" onClick={() => handleBackToHome()}>
|
||||||
|
Home
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-gray-200 mx-4">/</span>
|
||||||
|
<span className="font-mono text-gray-600">Workspace: {state?.workspace.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex flex-row justify-between items-center mb-4">
|
||||||
|
<span className="font-mono text-gray-400">Shortcut List</span>
|
||||||
<div
|
<div
|
||||||
className="flex flex-row justify-start items-center text-gray-600 border rounded-md px-2 py-1 cursor-pointer"
|
className="text-sm flex flex-row justify-start items-center border px-3 py-2 rounded-lg cursor-pointer hover:shadow"
|
||||||
onClick={() => handleBackToHome()}
|
onClick={() => showCreateShortcutDialog(state.workspace.id)}
|
||||||
>
|
>
|
||||||
<Icon.ChevronLeft className="w-5 h-auto" /> Back to Home
|
<Icon.Plus className="w-5 h-auto mr-1" /> Create Shortcut
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-4 font-mono text-gray-600">Workspace: {state?.workspace.name}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="font-mono mb-2 text-gray-400">Shortcut List</p>
|
|
||||||
<ShortcutListView workspaceId={state.workspace.id} shortcutList={shortcutList} />
|
<ShortcutListView workspaceId={state.workspace.id} shortcutList={shortcutList} />
|
||||||
<div
|
|
||||||
className="flex flex-row justify-start items-center border px-3 py-3 rounded-lg cursor-pointer"
|
|
||||||
onClick={() => showCreateShortcutDialog(state.workspace.id)}
|
|
||||||
>
|
|
||||||
<Icon.Plus className="w-5 h-auto mr-1" /> Create Shortcut
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
2
web/src/types/modules/user.d.ts
vendored
2
web/src/types/modules/user.d.ts
vendored
@ -9,6 +9,7 @@ interface User {
|
|||||||
|
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
openId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserCreate {
|
interface UserCreate {
|
||||||
@ -24,6 +25,7 @@ interface UserPatch {
|
|||||||
|
|
||||||
name?: string;
|
name?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
resetOpenID?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserDelete {
|
interface UserDelete {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user