feat: add explore page (#205)

This commit is contained in:
boojack 2022-09-09 00:06:05 +08:00 committed by GitHub
parent 5eea1339c9
commit e9ac6affef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 209 additions and 12 deletions

View file

@ -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)

View file

@ -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);

View 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;

View file

@ -80,7 +80,7 @@ const MemoList = () => {
useEffect(() => {
memoService
.fetchAllMemos()
.fetchMemos()
.then(() => {
// do nth
})

View file

@ -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
});
}

View file

@ -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")}

View file

@ -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
View 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
View 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;

View file

@ -35,7 +35,7 @@ function Home() {
}
} else {
if (!user) {
locationService.replaceHistory(`/u/${host.id}`);
locationService.replaceHistory(`/explore`);
}
}
loadingState.setFinish();

View file

@ -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 />,
};

View file

@ -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);

View file

@ -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 "/";

View file

@ -6,6 +6,7 @@ interface Memo {
id: MemoId;
creatorId: UserId;
creator: User;
createdTs: TimeStamp;
updatedTs: TimeStamp;
rowStatus: RowStatus;