Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,20 @@
<link rel="icon" type="image/svg+xml" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BooMap</title>
<meta name="description" content="AI를 통한 자동 마인드맵 생성 서비스" />
<meta name="author" content="부스트캠프 웹모바일 9기 Web32팀" />
<meta property="og:title" content="BooMap" />
<meta property="og:description" content="AI를 통한 자동 마인드맵 생성 서비스" />
<meta property="og:url" content="<https://boomap.site/>" />
<meta property="og:image" content="/thumbnail.png" />
<meta property="og:image-alt" content="마인드맵 저작도구 BooMap의 썸네일" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="600" />
<meta property="og:site_name" content="BooMap" />
<meta property="og:type" content="website" />
</head>
<body>
<div id="root"></div>
<div id="root" aria-label="BooMap 플랫폼"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Binary file added client/public/thumbnail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions client/src/components/Dashboard/MindMapInfoItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import profile from "@/assets/profile.png";
import useModal from "@/hooks/useModal";
import { Button } from "@headlessui/react";
import extractDate from "@/utils/extractDate";
Expand All @@ -13,9 +12,10 @@ import { useConnectionStore } from "@/store/useConnectionStore";
interface MindMapInfoItemProps {
data: DashBoard;
index: number;
onDelete: (id: number) => void;
}

export default function MindMapInfoItem({ data, index }: MindMapInfoItemProps) {
export default function MindMapInfoItem({ data, index, onDelete }: MindMapInfoItemProps) {
const { open, openModal, closeModal } = useModal();
const keywordData = data.keyword.slice(0, 4);
const navigate = useNavigate();
Expand All @@ -31,6 +31,7 @@ export default function MindMapInfoItem({ data, index }: MindMapInfoItemProps) {
function handleDelete() {
mutation.mutate();
closeModal();
onDelete(data.id);
}

function navigateToMindMap() {
Expand Down
101 changes: 92 additions & 9 deletions client/src/components/Dashboard/UserDashBoard.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import MindMapInfoItem from "./MindMapInfoItem";
import { Button, Input } from "@headlessui/react";
import { useEffect, useState } from "react";
import { FaPlus, FaSearch } from "react-icons/fa";
import { FaArrowDown, FaPlus, FaArrowUp } from "react-icons/fa";
import { useNavigate } from "react-router-dom";
import useDashBoard from "@/api/fetchHooks/useDashBoard";
import NoMindMap from "@/components/Dashboard/NoMindMap";
import { useConnectionStore } from "@/store/useConnectionStore";
import { IoSearch } from "react-icons/io5";

type SortKey = "title" | "ownerName" | "createDate";
type SortOrder = "asc" | "desc";

export default function UserDashBoard() {
const { data } = useDashBoard();
const [searchContent, setSearchContent] = useState("");
const [filteredData, setFilteredData] = useState(data);
const [sortKey, setSortKey] = useState<SortKey>("createDate");
const [sortOrder, setSortOrder] = useState<SortOrder>("desc");
const [searchQuery, setSearchQuery] = useState("");
const navigate = useNavigate();
const createConnection = useConnectionStore((state) => state.createConnection);
const updateOwnedMindMap = useConnectionStore((state) => state.updateOwnedMindMap);
Expand All @@ -21,17 +27,96 @@ export default function UserDashBoard() {
}
}, [data]);

useEffect(() => {
applyFilters(data, searchQuery);
}, [sortKey, sortOrder]);

function searchData(content: string) {
if (!data) return [];
return data.filter((item) => item.title.includes(content) || item.keyword.some((k) => k.includes(content)));
}

const filteredData = searchData(searchContent);
function sortData(items, key: SortKey, order: SortOrder) {
return [...items].sort((a, b) => {
const valueA = key === "createDate" ? new Date(a[key]).getTime() : a[key];
const valueB = key === "createDate" ? new Date(b[key]).getTime() : b[key];

if (order === "asc") return valueA < valueB ? -1 : valueA > valueB ? 1 : 0;
return valueA > valueB ? -1 : valueA < valueB ? 1 : 0;
});
}

function applyFilters(originalData, query: string) {
let result = query ? searchData(query) : originalData;
result = sortData(result, sortKey, sortOrder);
setFilteredData(result);
}

function handleSearchChange(query: string) {
setSearchQuery(query);
applyFilters(data, query);
}

function handleSortChange(key: SortKey, order: SortOrder) {
setSortKey(key);
setSortOrder(order);
applyFilters(data, searchQuery);
}

function handleDeleteMindMap(id: number) {
setFilteredData((prevData) => prevData.filter((item) => item.id !== id));
}

if (!data.length) return <NoMindMap />;

const buttonBaseClass = "flex items-center gap-0.5 text-sm";
const buttonInactiveClass = "text-grayscale-300 hover:text-white";
const buttonActiveClass = "text-white";

return (
<div className="relative flex h-full w-full flex-col rounded-[20px] bg-grayscale-700 px-8 pb-24 pt-8">
<div className="relative flex h-full w-full flex-col rounded-[20px] bg-grayscale-700 px-8 pb-24 pt-3">
<div className="mb-2 flex items-center justify-end gap-2">
<Button
className={`${buttonBaseClass} ${sortKey === "createDate" && sortOrder === "desc" ? buttonActiveClass : buttonInactiveClass}`}
onClick={() => handleSortChange("createDate", "desc")}
>
최신 순
</Button>
<span>|</span>
<Button
className={`${buttonBaseClass} ${sortKey === "createDate" && sortOrder === "asc" ? buttonActiveClass : buttonInactiveClass}`}
onClick={() => handleSortChange("createDate", "asc")}
>
오래된 순
</Button>
<span>|</span>
<Button
className={`${buttonBaseClass} ${sortKey === "title" && sortOrder === "asc" ? buttonActiveClass : buttonInactiveClass}`}
onClick={() => handleSortChange("title", "asc")}
>
제목 순<FaArrowUp />
</Button>
<span>|</span>
<Button
className={`${buttonBaseClass} ${sortKey === "title" && sortOrder === "desc" ? buttonActiveClass : buttonInactiveClass}`}
onClick={() => handleSortChange("title", "desc")}
>
제목 순<FaArrowDown />
</Button>
<span>|</span>
<Button
className={`${buttonBaseClass} ${sortKey === "ownerName" && sortOrder === "asc" ? buttonActiveClass : buttonInactiveClass}`}
onClick={() => handleSortChange("ownerName", "asc")}
>
소유자 이름 순<FaArrowUp />
</Button>
<span>|</span>
<Button
className={`${buttonBaseClass} ${sortKey === "ownerName" && sortOrder === "desc" ? buttonActiveClass : buttonInactiveClass}`}
onClick={() => handleSortChange("ownerName", "desc")}
>
소유자 이름 순<FaArrowDown />
</Button>
</div>
<header className="flex items-center justify-between px-3 py-2 font-bold">
<div className="min-w-72 pl-2">제목</div>
<div className="min-w-60 pl-8">키워드</div>
Expand All @@ -40,7 +125,7 @@ export default function UserDashBoard() {
</header>
<div className="no-scrollbar h-[calc(100%-40px)] overflow-y-scroll border-b-[1px] border-t-[1px] border-grayscale-500">
{filteredData.map((info, i) => (
<MindMapInfoItem key={`dashboard-${i}`} data={info} index={i} />
<MindMapInfoItem key={`dashboard-${info.id}`} data={info} index={i} onDelete={handleDeleteMindMap} />
))}
</div>
<div className="absolute bottom-8 right-8">
Expand All @@ -55,9 +140,7 @@ export default function UserDashBoard() {
<div className="absolute right-0 top-[-47px] flex items-center gap-2 rounded-2xl bg-grayscale-700 px-4 py-2 text-grayscale-200">
<Input
className="w-48 bg-inherit text-xs focus:outline-none"
onChange={(e) => {
setSearchContent(e.target.value);
}}
onChange={(e) => handleSearchChange(e.target.value)}
placeholder="키워드나 제목을 입력하세요"
/>
<IoSearch size={24} />
Expand Down
1 change: 1 addition & 0 deletions client/src/components/MindMapCanvas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export default function MindMapCanvas({ showMinutes, handleShowMinutes }) {
case "Escape":
groupRelease();
selectNode({});
break;
case "Tab":
if (e.shiftKey) {
moveToPreviousNode(data, selectedNode, selectNode);
Expand Down
11 changes: 8 additions & 3 deletions client/src/components/Modal/ShareModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ interface ShareModalProps {

export default function ShareModal({ open, closeModal }: ShareModalProps) {
const [copySuccess, setCopySuccess] = useState(false);

const currentUrl = window.location.href;

async function copyLink() {
Expand All @@ -23,15 +22,21 @@ export default function ShareModal({ open, closeModal }: ShareModalProps) {
setTimeout(() => setCopySuccess(false), 2000);
}

function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
e.stopPropagation();
}

return (
<Modal open={open} closeModal={closeModal}>
<div className="py-4">
<p className="mb-3 text-lg font-bold text-black">협업 링크</p>
<Input
type="text"
value={currentUrl}
readOnly
className="pointer-events-none h-10 w-full truncate rounded-lg bg-grayscale-200 px-3 py-2 text-grayscale-500"
value={currentUrl}
onClick={(e) => e.currentTarget.select()}
onKeyDown={handleKeyDown}
className="h-10 w-full truncate rounded-lg bg-grayscale-200 px-3 py-2 text-grayscale-500 focus:border-transparent focus:outline-none"
/>
<Button
onClick={copyLink}
Expand Down
1 change: 1 addition & 0 deletions client/src/store/createSocketSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export const createSocketSlice: StateCreator<ConnectionStore, [], [], SocketSlic
const socket = get().socket;
if (socket) socket.disconnect();
set({ socket: null });
get().nodeError.length > 0 && set({ nodeError: [] });
get().updateRole("editor");
},

Expand Down
Loading