mirror of
https://github.com/usememos/memos.git
synced 2025-03-24 14:40:16 +01:00
feat: add explore page (#205)
This commit is contained in:
parent
5eea1339c9
commit
e9ac6affef
@ -162,10 +162,6 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
|
||||
}
|
||||
|
||||
rowStatus := api.RowStatus(c.QueryParam("rowStatus"))
|
||||
if rowStatus != "" {
|
||||
memoFind.RowStatus = &rowStatus
|
||||
}
|
||||
pinnedStr := c.QueryParam("pinned")
|
||||
if pinnedStr != "" {
|
||||
pinned := pinnedStr == "true"
|
||||
@ -191,6 +187,10 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
memoFind.Offset = offset
|
||||
}
|
||||
|
||||
// Only fetch normal status memos.
|
||||
normalStatus := api.Normal
|
||||
memoFind.RowStatus = &normalStatus
|
||||
|
||||
list, err := s.Store.FindMemoList(ctx, memoFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch all memo list").SetInternal(err)
|
||||
|
@ -28,7 +28,7 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
|
||||
if (showConfirmDeleteBtn) {
|
||||
try {
|
||||
await memoService.deleteMemoById(memo.id);
|
||||
await memoService.fetchAllMemos();
|
||||
await memoService.fetchMemos();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toastHelper.error(error.response.data.message);
|
||||
@ -44,7 +44,7 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
|
||||
id: memo.id,
|
||||
rowStatus: "NORMAL",
|
||||
});
|
||||
await memoService.fetchAllMemos();
|
||||
await memoService.fetchMemos();
|
||||
toastHelper.info("Restored successfully");
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
|
29
web/src/components/MemoContent.tsx
Normal file
29
web/src/components/MemoContent.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { useRef } from "react";
|
||||
import { formatMemoContent } from "../helpers/marked";
|
||||
import "../less/memo-content.less";
|
||||
|
||||
interface Props {
|
||||
className: string;
|
||||
content: string;
|
||||
onMemoContentClick: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
const MemoContent: React.FC<Props> = (props: Props) => {
|
||||
const { className, content, onMemoContentClick } = props;
|
||||
const memoContentContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMemoContentClick = async (e: React.MouseEvent) => {
|
||||
onMemoContentClick(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={memoContentContainerRef}
|
||||
className={`memo-content-text ${className}`}
|
||||
onClick={handleMemoContentClick}
|
||||
dangerouslySetInnerHTML={{ __html: formatMemoContent(content) }}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemoContent;
|
@ -80,7 +80,7 @@ const MemoList = () => {
|
||||
|
||||
useEffect(() => {
|
||||
memoService
|
||||
.fetchAllMemos()
|
||||
.fetchMemos()
|
||||
.then(() => {
|
||||
// do nth
|
||||
})
|
||||
|
@ -29,7 +29,7 @@ const MemosHeader = () => {
|
||||
const now = Date.now();
|
||||
if (now - prevRequestTimestamp > 1 * 1000) {
|
||||
prevRequestTimestamp = now;
|
||||
memoService.fetchAllMemos().catch(() => {
|
||||
memoService.fetchMemos().catch(() => {
|
||||
// do nth
|
||||
});
|
||||
}
|
||||
|
@ -51,6 +51,10 @@ const MenuBtnsPopup: React.FC<Props> = (props: Props) => {
|
||||
showAboutSiteDialog();
|
||||
};
|
||||
|
||||
const handleExploreBtnClick = () => {
|
||||
locationService.pushHistory("/explore");
|
||||
};
|
||||
|
||||
const handleSignOutBtnClick = async () => {
|
||||
userService
|
||||
.doSignOut()
|
||||
@ -65,12 +69,15 @@ const MenuBtnsPopup: React.FC<Props> = (props: Props) => {
|
||||
|
||||
return (
|
||||
<div className={`menu-btns-popup ${shownStatus ? "" : "hidden"}`} ref={popupElRef}>
|
||||
<button className="btn action-btn" onClick={handleAboutBtnClick}>
|
||||
<span className="icon">🤠</span> {t("common.about")}
|
||||
<button className="btn action-btn" onClick={handleExploreBtnClick}>
|
||||
<span className="icon">👾</span> Explore
|
||||
</button>
|
||||
<button className="btn action-btn" onClick={handlePingBtnClick}>
|
||||
<span className="icon">🎯</span> Ping
|
||||
</button>
|
||||
<button className="btn action-btn" onClick={handleAboutBtnClick}>
|
||||
<span className="icon">🤠</span> {t("common.about")}
|
||||
</button>
|
||||
<Only when={!userService.isVisitorMode()}>
|
||||
<button className="btn action-btn" onClick={handleSignOutBtnClick}>
|
||||
<span className="icon">👋</span> {t("common.sign-out")}
|
||||
|
@ -58,6 +58,10 @@ export function deleteUser(userDelete: UserDelete) {
|
||||
return axios.delete(`/api/user/${userDelete.id}`);
|
||||
}
|
||||
|
||||
export function getAllMemos() {
|
||||
return axios.get<ResponseObject<Memo[]>>("/api/memo/all");
|
||||
}
|
||||
|
||||
export function getMemoList(memoFind?: MemoFind) {
|
||||
const queryList = [];
|
||||
if (memoFind?.creatorId) {
|
||||
|
58
web/src/less/explore.less
Normal file
58
web/src/less/explore.less
Normal file
@ -0,0 +1,58 @@
|
||||
@import "./mixin.less";
|
||||
|
||||
.page-wrapper.explore {
|
||||
@apply relative top-0 w-full h-screen overflow-y-auto overflow-x-hidden;
|
||||
background-color: #f6f5f4;
|
||||
|
||||
> .page-container {
|
||||
@apply relative w-full min-h-screen mx-auto flex flex-col justify-start items-center;
|
||||
|
||||
> .page-header {
|
||||
@apply relative max-w-2xl w-full min-h-full flex flex-row justify-start items-center px-4 sm:pr-6;
|
||||
|
||||
> .logo-img {
|
||||
@apply h-14 w-auto mt-6 mb-2;
|
||||
}
|
||||
}
|
||||
|
||||
> .memos-wrapper {
|
||||
@apply relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4 sm:pr-6;
|
||||
|
||||
> .memo-container {
|
||||
@apply flex flex-col justify-start items-start w-full p-4 mt-2 bg-white rounded-lg border border-white hover:border-gray-200;
|
||||
|
||||
> .memo-header {
|
||||
@apply mb-2 w-full flex flex-row justify-start items-center text-sm font-mono text-gray-400;
|
||||
|
||||
> .split-text {
|
||||
@apply mx-2;
|
||||
}
|
||||
|
||||
> .name-text {
|
||||
@apply hover:text-green-600 hover:underline;
|
||||
}
|
||||
}
|
||||
|
||||
> .memo-content {
|
||||
@apply cursor-default;
|
||||
|
||||
> * {
|
||||
@apply cursor-default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .addtion-btn-container {
|
||||
@apply fixed bottom-12 left-1/2 -translate-x-1/2;
|
||||
|
||||
> .btn {
|
||||
@apply bg-blue-600 text-white px-4 py-2 rounded-3xl shadow-2xl hover:opacity-80;
|
||||
|
||||
> .icon {
|
||||
@apply text-lg mr-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
86
web/src/pages/Explore.tsx
Normal file
86
web/src/pages/Explore.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { locationService, memoService, userService } from "../services";
|
||||
import { useAppSelector } from "../store";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import MemoContent from "../components/MemoContent";
|
||||
import "../less/explore.less";
|
||||
|
||||
interface State {
|
||||
memos: Memo[];
|
||||
}
|
||||
|
||||
const Explore = () => {
|
||||
const { t, locale } = useI18n();
|
||||
const user = useAppSelector((state) => state.user.user);
|
||||
const location = useAppSelector((state) => state.location);
|
||||
const [state, setState] = useState<State>({
|
||||
memos: [],
|
||||
});
|
||||
const loadingState = useLoading();
|
||||
|
||||
useEffect(() => {
|
||||
userService
|
||||
.initialState()
|
||||
.catch()
|
||||
.finally(async () => {
|
||||
const { host } = userService.getState();
|
||||
if (!host) {
|
||||
locationService.replaceHistory("/auth");
|
||||
return;
|
||||
}
|
||||
|
||||
memoService.fetchAllMemos().then((memos) => {
|
||||
setState({
|
||||
...state,
|
||||
memos,
|
||||
});
|
||||
});
|
||||
loadingState.setFinish();
|
||||
});
|
||||
}, [location]);
|
||||
|
||||
return (
|
||||
<section className="page-wrapper explore">
|
||||
{loadingState.isLoading ? null : (
|
||||
<div className="page-container">
|
||||
<div className="page-header">
|
||||
<img className="logo-img" src="/logo-full.webp" alt="" />
|
||||
</div>
|
||||
<main className="memos-wrapper">
|
||||
{state.memos.map((memo) => {
|
||||
const createdAtStr = dayjs(memo.createdTs).locale(locale).format("YYYY/MM/DD HH:mm:ss");
|
||||
return (
|
||||
<div className="memo-container" key={memo.id}>
|
||||
<div className="memo-header">
|
||||
<span className="time-text">{createdAtStr}</span>
|
||||
<span className="split-text">by</span>
|
||||
<a className="name-text" href={`/u/${memo.creator.id}`}>
|
||||
{memo.creator.name}
|
||||
</a>
|
||||
</div>
|
||||
<MemoContent className="memo-content" content={memo.content} onMemoContentClick={() => undefined} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</main>
|
||||
|
||||
<div className="addtion-btn-container">
|
||||
{user ? (
|
||||
<button className="btn" onClick={() => (window.location.href = "/")}>
|
||||
<span className="icon">🏠</span> {t("common.back-to-home")}
|
||||
</button>
|
||||
) : (
|
||||
<button className="btn" onClick={() => (window.location.href = "/auth")}>
|
||||
<span className="icon">👉</span> {t("common.sign-in")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Explore;
|
@ -35,7 +35,7 @@ function Home() {
|
||||
}
|
||||
} else {
|
||||
if (!user) {
|
||||
locationService.replaceHistory(`/u/${host.id}`);
|
||||
locationService.replaceHistory(`/explore`);
|
||||
}
|
||||
}
|
||||
loadingState.setFinish();
|
||||
|
@ -1,8 +1,10 @@
|
||||
import Home from "../pages/Home";
|
||||
import Auth from "../pages/Auth";
|
||||
import Explore from "../pages/Explore";
|
||||
|
||||
const appRouter = {
|
||||
"/auth": <Auth />,
|
||||
"/explore": <Explore />,
|
||||
"*": <Home />,
|
||||
};
|
||||
|
||||
|
@ -17,6 +17,16 @@ const memoService = {
|
||||
},
|
||||
|
||||
fetchAllMemos: async () => {
|
||||
const memoFind: MemoFind = {};
|
||||
if (userService.isVisitorMode()) {
|
||||
memoFind.creatorId = userService.getUserIdFromPath();
|
||||
}
|
||||
const { data } = (await api.getAllMemos()).data;
|
||||
const memos = data.map((m) => convertResponseModelMemo(m));
|
||||
return memos;
|
||||
},
|
||||
|
||||
fetchMemos: async () => {
|
||||
const timeoutIndex = setTimeout(() => {
|
||||
store.dispatch(setIsFetching(true));
|
||||
}, 1000);
|
||||
|
@ -21,7 +21,7 @@ interface State {
|
||||
|
||||
const getValidPathname = (pathname: string): string => {
|
||||
const userPageUrlRegex = /^\/u\/\d+.*/;
|
||||
if (["/", "/auth"].includes(pathname) || userPageUrlRegex.test(pathname)) {
|
||||
if (["/", "/auth", "/explore"].includes(pathname) || userPageUrlRegex.test(pathname)) {
|
||||
return pathname;
|
||||
} else {
|
||||
return "/";
|
||||
|
1
web/src/types/modules/memo.d.ts
vendored
1
web/src/types/modules/memo.d.ts
vendored
@ -6,6 +6,7 @@ interface Memo {
|
||||
id: MemoId;
|
||||
|
||||
creatorId: UserId;
|
||||
creator: User;
|
||||
createdTs: TimeStamp;
|
||||
updatedTs: TimeStamp;
|
||||
rowStatus: RowStatus;
|
||||
|
Loading…
x
Reference in New Issue
Block a user