mirror of
https://github.com/usememos/memos.git
synced 2025-02-16 11:21:02 +01:00
chore: add statistics view
This commit is contained in:
parent
138b69e36e
commit
914c0620c4
@ -1,14 +1,15 @@
|
|||||||
import MemoCreationHeatMap from "./MemoCreationHeatMap";
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||||
|
import PersonalStatistics from "./PersonalStatistics";
|
||||||
import SearchBar from "./SearchBar";
|
import SearchBar from "./SearchBar";
|
||||||
import TagList from "./TagList";
|
import TagList from "./TagList";
|
||||||
|
|
||||||
const HomeSidebar = () => {
|
const HomeSidebar = () => {
|
||||||
|
const currentUser = useCurrentUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="relative w-full pr-2 h-full max-h-screen overflow-auto hide-scrollbar flex flex-col justify-start items-start py-4 sm:pt-6">
|
<aside className="relative w-full px-4 h-full max-h-screen overflow-auto hide-scrollbar flex flex-col justify-start items-start py-4 sm:pt-6">
|
||||||
<div className="px-4 pr-8 mb-4 w-full">
|
<SearchBar />
|
||||||
<SearchBar />
|
<PersonalStatistics user={currentUser} />
|
||||||
</div>
|
|
||||||
<MemoCreationHeatMap />
|
|
||||||
<TagList />
|
<TagList />
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
@ -26,7 +26,7 @@ const HomeSidebarDrawer = () => {
|
|||||||
<Icon.Search className="w-5 h-auto dark:text-gray-200" />
|
<Icon.Search className="w-5 h-auto dark:text-gray-200" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Drawer anchor="right" size="sm" open={open} onClose={toggleDrawer(false)}>
|
<Drawer anchor="right" size="sm" open={open} onClose={toggleDrawer(false)}>
|
||||||
<div className="w-full px-4">
|
<div className="w-full px-1">
|
||||||
<HomeSidebar />
|
<HomeSidebar />
|
||||||
</div>
|
</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
@ -1,156 +0,0 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
|
||||||
import { memoServiceClient } from "@/grpcweb";
|
|
||||||
import { DAILY_TIMESTAMP } from "@/helpers/consts";
|
|
||||||
import { getDateStampByDate, getDateString, getTimeStampByDate } from "@/helpers/datetime";
|
|
||||||
import * as utils from "@/helpers/utils";
|
|
||||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
|
||||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
|
||||||
import { useGlobalStore } from "@/store/module";
|
|
||||||
import { useMemoStore } from "@/store/v1";
|
|
||||||
import { useTranslate, Translations } from "@/utils/i18n";
|
|
||||||
import "@/less/usage-heat-map.less";
|
|
||||||
|
|
||||||
interface DailyUsageStat {
|
|
||||||
timestamp: number;
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tableConfig = {
|
|
||||||
width: 10,
|
|
||||||
height: 7,
|
|
||||||
};
|
|
||||||
|
|
||||||
const getInitialCreationStats = (usedDaysAmount: number, beginDayTimestamp: number): DailyUsageStat[] => {
|
|
||||||
const initialUsageStat: DailyUsageStat[] = [];
|
|
||||||
for (let i = 1; i <= usedDaysAmount; i++) {
|
|
||||||
initialUsageStat.push({
|
|
||||||
timestamp: beginDayTimestamp + DAILY_TIMESTAMP * i,
|
|
||||||
count: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return initialUsageStat;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MemoCreationHeatMap = () => {
|
|
||||||
const t = useTranslate();
|
|
||||||
const navigateTo = useNavigateTo();
|
|
||||||
const user = useCurrentUser();
|
|
||||||
const memoStore = useMemoStore();
|
|
||||||
const todayTimeStamp = getDateStampByDate(Date.now());
|
|
||||||
const weekDay = new Date(todayTimeStamp).getDay();
|
|
||||||
const weekFromMonday = ["zh-Hans", "ko"].includes(useGlobalStore().state.locale);
|
|
||||||
const dayTips = weekFromMonday ? ["mon", "", "wed", "", "fri", "", "sun"] : ["sun", "", "tue", "", "thu", "", "sat"];
|
|
||||||
const todayDay = weekFromMonday ? (weekDay == 0 ? 7 : weekDay) : weekDay + 1;
|
|
||||||
const nullCell = new Array(7 - todayDay).fill(0);
|
|
||||||
const usedDaysAmount = (tableConfig.width - 1) * tableConfig.height + todayDay;
|
|
||||||
const beginDayTimestamp = todayTimeStamp - usedDaysAmount * DAILY_TIMESTAMP;
|
|
||||||
const [memoAmount, setMemoAmount] = useState(0);
|
|
||||||
const [creationStatus, setCreationStatus] = useState<DailyUsageStat[]>(getInitialCreationStats(usedDaysAmount, beginDayTimestamp));
|
|
||||||
const containerElRef = useRef<HTMLDivElement>(null);
|
|
||||||
const memos = Object.values(memoStore.getState().memoMapById);
|
|
||||||
const createdDays = Math.ceil((Date.now() - getTimeStampByDate(user.createTime)) / 1000 / 3600 / 24);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (memos.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const { memoCreationStats } = await memoServiceClient.getUserMemosStats({
|
|
||||||
name: user.name,
|
|
||||||
});
|
|
||||||
const tempStats = getInitialCreationStats(usedDaysAmount, beginDayTimestamp);
|
|
||||||
Object.entries(memoCreationStats).forEach(([k, v]) => {
|
|
||||||
const dayIndex = Math.floor((getDateStampByDate(k) - beginDayTimestamp) / DAILY_TIMESTAMP) - 1;
|
|
||||||
if (tempStats[dayIndex]) {
|
|
||||||
tempStats[dayIndex].count = v;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setCreationStatus(tempStats);
|
|
||||||
setMemoAmount(Object.values(memoCreationStats).reduce((acc, cur) => acc + cur, 0));
|
|
||||||
})();
|
|
||||||
}, [memos.length, user.name]);
|
|
||||||
|
|
||||||
const handleUsageStatItemMouseEnter = useCallback((event: React.MouseEvent, item: DailyUsageStat) => {
|
|
||||||
const tempDiv = document.createElement("div");
|
|
||||||
tempDiv.className = "usage-detail-container pop-up";
|
|
||||||
const bounding = utils.getElementBounding(event.target as HTMLElement);
|
|
||||||
tempDiv.style.left = bounding.left + "px";
|
|
||||||
tempDiv.style.top = bounding.top - 2 + "px";
|
|
||||||
const tMemoOnOpts = { amount: item.count, date: getDateString(item.timestamp as number) };
|
|
||||||
tempDiv.innerHTML = item.count === 1 ? t("heatmap.memo-on", tMemoOnOpts) : t("heatmap.memos-on", tMemoOnOpts);
|
|
||||||
document.body.appendChild(tempDiv);
|
|
||||||
|
|
||||||
if (tempDiv.offsetLeft - tempDiv.clientWidth / 2 < 0) {
|
|
||||||
tempDiv.style.left = bounding.left + tempDiv.clientWidth * 0.4 + "px";
|
|
||||||
tempDiv.className += " offset-left";
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleUsageStatItemMouseLeave = useCallback(() => {
|
|
||||||
document.body.querySelectorAll("div.usage-detail-container.pop-up").forEach((node) => node.remove());
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleUsageStatItemClick = useCallback((item: DailyUsageStat) => {
|
|
||||||
navigateTo(`/timeline?timestamp=${item.timestamp}`);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// This interpolation is not being used because of the current styling,
|
|
||||||
// but it can improve translation quality by giving it a more meaningful context
|
|
||||||
const tMemoInOpts = { amount: memoAmount, period: "", date: "" };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="usage-heat-map-wrapper" ref={containerElRef}>
|
|
||||||
<div className="usage-heat-map">
|
|
||||||
{}
|
|
||||||
{creationStatus.map((v, i) => {
|
|
||||||
const count = v.count;
|
|
||||||
const colorLevel =
|
|
||||||
count <= 0
|
|
||||||
? ""
|
|
||||||
: count <= 1
|
|
||||||
? "stat-day-l1-bg"
|
|
||||||
: count <= 2
|
|
||||||
? "stat-day-l2-bg"
|
|
||||||
: count <= 4
|
|
||||||
? "stat-day-l3-bg"
|
|
||||||
: "stat-day-l4-bg";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="stat-wrapper"
|
|
||||||
key={i}
|
|
||||||
onMouseEnter={(e) => handleUsageStatItemMouseEnter(e, v)}
|
|
||||||
onMouseLeave={handleUsageStatItemMouseLeave}
|
|
||||||
onClick={() => handleUsageStatItemClick(v)}
|
|
||||||
>
|
|
||||||
<span className={`stat-container ${colorLevel} ${todayTimeStamp === v.timestamp ? "today" : ""}`}></span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{nullCell.map((_, i) => (
|
|
||||||
<div className="stat-wrapper" key={i}>
|
|
||||||
<span className="stat-container null"></span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="day-tip-text-container">
|
|
||||||
{dayTips.map((v, i) => (
|
|
||||||
<span className="tip-text" key={i}>
|
|
||||||
{v && t(("days." + v) as Translations)}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="w-full pl-4 text-xs -mt-2 mb-3 text-gray-400 dark:text-zinc-400">
|
|
||||||
<span className="font-medium text-gray-500 dark:text-zinc-300 number">{memoAmount} </span>
|
|
||||||
{memoAmount === 1 ? t("heatmap.memo-in", tMemoInOpts) : t("heatmap.memos-in", tMemoInOpts)}{" "}
|
|
||||||
<span className="font-medium text-gray-500 dark:text-zinc-300">{createdDays} </span>
|
|
||||||
{createdDays === 1 ? t("heatmap.day", tMemoInOpts) : t("heatmap.days", tMemoInOpts)}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MemoCreationHeatMap;
|
|
@ -31,6 +31,7 @@ const MemoResourceListView = ({ resourceList = [] }: { resourceList: Resource[]
|
|||||||
const MediaCard = ({ resource, thumbnail }: { resource: Resource; thumbnail?: boolean }) => {
|
const MediaCard = ({ resource, thumbnail }: { resource: Resource; thumbnail?: boolean }) => {
|
||||||
const type = getResourceType(resource);
|
const type = getResourceType(resource);
|
||||||
const url = getResourceUrl(resource);
|
const url = getResourceUrl(resource);
|
||||||
|
|
||||||
if (type === "image/*") {
|
if (type === "image/*") {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
@ -40,9 +41,7 @@ const MemoResourceListView = ({ resourceList = [] }: { resourceList: Resource[]
|
|||||||
decoding="async"
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
} else if (type === "video/*") {
|
||||||
|
|
||||||
if (type === "video/*") {
|
|
||||||
return (
|
return (
|
||||||
<video
|
<video
|
||||||
className="cursor-pointer w-full h-full object-contain bg-zinc-100 dark:bg-zinc-800"
|
className="cursor-pointer w-full h-full object-contain bg-zinc-100 dark:bg-zinc-800"
|
||||||
@ -52,9 +51,9 @@ const MemoResourceListView = ({ resourceList = [] }: { resourceList: Resource[]
|
|||||||
controls
|
controls
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <></>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const MediaList = ({ resources = [] }: { resources: Resource[] }) => {
|
const MediaList = ({ resources = [] }: { resources: Resource[] }) => {
|
||||||
|
69
web/src/components/PersonalStatistics.tsx
Normal file
69
web/src/components/PersonalStatistics.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { memoServiceClient } from "@/grpcweb";
|
||||||
|
import { useTagStore } from "@/store/module";
|
||||||
|
import { useMemoStore } from "@/store/v1";
|
||||||
|
import { User } from "@/types/proto/api/v2/user_service";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PersonalStatistics = (props: Props) => {
|
||||||
|
const { user } = props;
|
||||||
|
const tagStore = useTagStore();
|
||||||
|
const memoStore = useMemoStore();
|
||||||
|
const [memoAmount, setMemoAmount] = useState(0);
|
||||||
|
const [isRequesting, setIsRequesting] = useState(false);
|
||||||
|
const days = Math.ceil((Date.now() - user.createTime!.getTime()) / 86400000);
|
||||||
|
const memos = Object.values(memoStore.getState().memoMapById);
|
||||||
|
const tags = tagStore.state.tags.length;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (memos.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
setIsRequesting(true);
|
||||||
|
const { memoCreationStats } = await memoServiceClient.getUserMemosStats({
|
||||||
|
name: user.name,
|
||||||
|
});
|
||||||
|
setIsRequesting(false);
|
||||||
|
setMemoAmount(Object.values(memoCreationStats).reduce((acc, cur) => acc + cur, 0));
|
||||||
|
})();
|
||||||
|
}, [memos.length, user.name]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full border mt-2 py-2 px-3 rounded-md space-y-0.5 bg-zinc-50 dark:bg-zinc-900 dark:border-zinc-800">
|
||||||
|
<p className="text-sm font-medium text-gray-500">Statistics</p>
|
||||||
|
<div className="w-full flex justify-between items-center">
|
||||||
|
<div className="w-full flex justify-start items-center text-gray-500">
|
||||||
|
<Icon.CalendarDays className="w-4 h-auto mr-1" />
|
||||||
|
<span className="block text-base sm:text-sm">Days</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-gray-500 font-mono">{days}</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex justify-between items-center">
|
||||||
|
<div className="w-full flex justify-start items-center text-gray-500">
|
||||||
|
<Icon.PencilLine className="w-4 h-auto mr-1" />
|
||||||
|
<span className="block text-base sm:text-sm">Memos</span>
|
||||||
|
</div>
|
||||||
|
{isRequesting ? (
|
||||||
|
<Icon.Loader className="animate-spin w-4 h-auto text-gray-400" />
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-500 font-mono">{memoAmount}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex justify-between items-center">
|
||||||
|
<div className="w-full flex justify-start items-center text-gray-500">
|
||||||
|
<Icon.Hash className="w-4 h-auto mr-1" />
|
||||||
|
<span className="block text-base sm:text-sm">Tags</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-gray-500 font-mono">{tags}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PersonalStatistics;
|
@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { Input } from "@mui/joy";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import useDebounce from "react-use/lib/useDebounce";
|
import useDebounce from "react-use/lib/useDebounce";
|
||||||
import { useFilterStore } from "@/store/module";
|
import { useFilterStore } from "@/store/module";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
@ -8,7 +9,6 @@ const SearchBar = () => {
|
|||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const filterStore = useFilterStore();
|
const filterStore = useFilterStore();
|
||||||
const [queryText, setQueryText] = useState("");
|
const [queryText, setQueryText] = useState("");
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const text = filterStore.getState().text;
|
const text = filterStore.getState().text;
|
||||||
@ -28,13 +28,12 @@ const SearchBar = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-9 flex flex-row justify-start items-center py-2 px-3 rounded-md bg-gray-200 dark:bg-zinc-700">
|
<div className="w-full h-9 flex flex-row justify-start items-center">
|
||||||
<Icon.Search className="w-4 h-auto opacity-30 dark:text-gray-200" />
|
<Input
|
||||||
<input
|
className="w-full !shadow-none !border-gray-200 dark:!border-zinc-800"
|
||||||
className="flex ml-2 w-24 grow text-sm outline-none bg-transparent dark:text-gray-200"
|
size="md"
|
||||||
type="text"
|
startDecorator={<Icon.Search className="w-4 h-auto opacity-30" />}
|
||||||
placeholder={t("memo.search-placeholder")}
|
placeholder={t("memo.search-placeholder")}
|
||||||
ref={inputRef}
|
|
||||||
value={queryText}
|
value={queryText}
|
||||||
onChange={handleTextQueryInput}
|
onChange={handleTextQueryInput}
|
||||||
/>
|
/>
|
||||||
|
@ -70,8 +70,8 @@ const TagList = () => {
|
|||||||
}, [tagsText]);
|
}, [tagsText]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-start items-start w-full mt-2 h-auto shrink-0 flex-nowrap hide-scrollbar">
|
<div className="flex flex-col justify-start items-start w-full mt-3 px-1 h-auto shrink-0 flex-nowrap hide-scrollbar">
|
||||||
<div className="flex flex-row justify-start items-center w-full px-4">
|
<div className="flex flex-row justify-start items-center w-full">
|
||||||
<span className="text-sm leading-6 font-mono text-gray-400">{t("common.tags")}</span>
|
<span className="text-sm leading-6 font-mono text-gray-400">{t("common.tags")}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => showCreateTagDialog()}
|
onClick={() => showCreateTagDialog()}
|
||||||
@ -80,7 +80,7 @@ const TagList = () => {
|
|||||||
<Icon.Plus className="w-4 h-4 text-gray-400" />
|
<Icon.Plus className="w-4 h-4 text-gray-400" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col justify-start items-start relative w-full h-auto flex-nowrap mt-2 mb-2">
|
<div className="flex flex-col justify-start items-start relative w-full h-auto flex-nowrap">
|
||||||
{tags.map((t, idx) => (
|
{tags.map((t, idx) => (
|
||||||
<TagItemContainer key={t.text + "-" + idx} tag={t} tagQuery={filter.tag} />
|
<TagItemContainer key={t.text + "-" + idx} tag={t} tagQuery={filter.tag} />
|
||||||
))}
|
))}
|
||||||
@ -117,15 +117,15 @@ const TagItemContainer: React.FC<TagItemContainerProps> = (props: TagItemContain
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="relative group flex flex-row justify-between items-center w-full h-10 py-0 px-4 mt-px first:mt-1 rounded-lg text-base cursor-pointer select-none shrink-0 hover:opacity-60"
|
className="relative group flex flex-row justify-between items-center w-full h-8 py-0 mt-px first:mt-1 rounded-lg text-base sm:text-sm cursor-pointer select-none shrink-0 hover:opacity-80"
|
||||||
onClick={handleTagClick}
|
onClick={handleTagClick}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex flex-row justify-start items-center truncate shrink leading-5 mr-1 text-black dark:text-gray-200 ${
|
className={`flex flex-row justify-start items-center truncate shrink leading-5 mr-1 text-gray-600 dark:text-gray-400 ${
|
||||||
isActive && "text-green-600"
|
isActive && "text-green-600"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="block w-4 shrink-0">#</span>
|
<Icon.Hash className="w-4 h-auto shrink-0 opacity-60 mr-1" />
|
||||||
<span className="truncate">{tag.key}</span>
|
<span className="truncate">{tag.key}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row justify-end items-center">
|
<div className="flex flex-row justify-end items-center">
|
||||||
@ -141,7 +141,7 @@ const TagItemContainer: React.FC<TagItemContainerProps> = (props: TagItemContain
|
|||||||
</div>
|
</div>
|
||||||
{hasSubTags ? (
|
{hasSubTags ? (
|
||||||
<div
|
<div
|
||||||
className={`w-[calc(100%-1rem)] flex flex-col justify-start items-start h-auto ml-4 pl-1 border-l-2 border-l-gray-200 dark:border-l-gray-400 ${
|
className={`w-[calc(100%-0.5rem)] flex flex-col justify-start items-start h-auto ml-2 pl-2 border-l-2 border-l-gray-200 dark:border-l-gray-400 ${
|
||||||
!showSubTags && "!hidden"
|
!showSubTags && "!hidden"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import Icon from "@/components/Icon";
|
|
||||||
import MemoContent from "@/components/MemoContent";
|
import MemoContent from "@/components/MemoContent";
|
||||||
import MemoResourceListView from "@/components/MemoResourceListView";
|
import MemoResourceListView from "@/components/MemoResourceListView";
|
||||||
import { getTimeString } from "@/helpers/datetime";
|
import { getTimeString } from "@/helpers/datetime";
|
||||||
@ -18,8 +17,6 @@ const TimelineMemo = (props: Props) => {
|
|||||||
<div className="relative w-full flex flex-col justify-start items-start">
|
<div className="relative w-full flex flex-col justify-start items-start">
|
||||||
<div className="w-full flex flex-row justify-start items-center mt-0.5 mb-1 text-sm font-mono text-gray-500 dark:text-gray-400">
|
<div className="w-full flex flex-row justify-start items-center mt-0.5 mb-1 text-sm font-mono text-gray-500 dark:text-gray-400">
|
||||||
<span className="opacity-80">{getTimeString(memo.displayTime)}</span>
|
<span className="opacity-80">{getTimeString(memo.displayTime)}</span>
|
||||||
<Icon.Dot className="w-5 h-auto opacity-60" />
|
|
||||||
<span className="opacity-60">#{memo.id}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<MemoContent nodes={memo.nodes} />
|
<MemoContent nodes={memo.nodes} />
|
||||||
<MemoResourceListView resourceList={memo.resources} />
|
<MemoResourceListView resourceList={memo.resources} />
|
||||||
|
@ -1,73 +0,0 @@
|
|||||||
.usage-heat-map-wrapper {
|
|
||||||
@apply flex flex-row justify-start items-center flex-nowrap w-full h-32 pl-4 pb-3 shrink-0;
|
|
||||||
|
|
||||||
> .usage-heat-map {
|
|
||||||
@apply w-full h-full grid grid-rows-7 grid-cols-10 grid-flow-col;
|
|
||||||
|
|
||||||
> .stat-wrapper {
|
|
||||||
> .stat-container {
|
|
||||||
@apply block rounded bg-gray-200 dark:bg-zinc-700;
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
|
|
||||||
&.stat-day-l1-bg {
|
|
||||||
@apply bg-green-400 dark:bg-green-800;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.stat-day-l2-bg {
|
|
||||||
@apply bg-green-500 dark:bg-green-700;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.stat-day-l3-bg {
|
|
||||||
@apply bg-green-600 dark:bg-green-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.stat-day-l4-bg {
|
|
||||||
@apply bg-green-700 dark:bg-green-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.today {
|
|
||||||
@apply border border-black dark:border-gray-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.null {
|
|
||||||
@apply opacity-40;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .day-tip-text-container {
|
|
||||||
@apply w-8 h-full grid grid-rows-7;
|
|
||||||
|
|
||||||
> .tip-text {
|
|
||||||
@apply pl-1 w-full h-full text-left font-mono text-gray-400;
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.usage-detail-container {
|
|
||||||
@apply fixed left-0 top-0 ml-2 -mt-9 p-2 z-100 -translate-x-1/2 select-none text-white text-xs rounded whitespace-nowrap;
|
|
||||||
background-color: rgba(0, 0, 0, 0.8);
|
|
||||||
|
|
||||||
> .date-text {
|
|
||||||
@apply text-gray-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.offset-left {
|
|
||||||
&::before {
|
|
||||||
left: calc(10% - 5px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
bottom: -4px;
|
|
||||||
left: calc(50% - 5px);
|
|
||||||
border-left: 4px solid transparent;
|
|
||||||
border-right: 4px solid transparent;
|
|
||||||
border-top: 4px solid rgba(0, 0, 0, 0.8);
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,6 +13,7 @@ import useCurrentUser from "@/hooks/useCurrentUser";
|
|||||||
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
|
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
|
||||||
import { useFilterStore } from "@/store/module";
|
import { useFilterStore } from "@/store/module";
|
||||||
import { useMemoList, useMemoStore } from "@/store/v1";
|
import { useMemoList, useMemoStore } from "@/store/v1";
|
||||||
|
import { RowStatus } from "@/types/proto/api/v2/common";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
@ -26,6 +27,7 @@ const Home = () => {
|
|||||||
const [isComplete, setIsComplete] = useState(false);
|
const [isComplete, setIsComplete] = useState(false);
|
||||||
const { tag: tagQuery, text: textQuery } = filterStore.state;
|
const { tag: tagQuery, text: textQuery } = filterStore.state;
|
||||||
const sortedMemos = memoList.value
|
const sortedMemos = memoList.value
|
||||||
|
.filter((memo) => memo.rowStatus === RowStatus.ACTIVE)
|
||||||
.sort((a, b) => getTimeStampByDate(b.displayTime) - getTimeStampByDate(a.displayTime))
|
.sort((a, b) => getTimeStampByDate(b.displayTime) - getTimeStampByDate(a.displayTime))
|
||||||
.sort((a, b) => Number(b.pinned) - Number(a.pinned));
|
.sort((a, b) => Number(b.pinned) - Number(a.pinned));
|
||||||
|
|
||||||
@ -62,7 +64,7 @@ const Home = () => {
|
|||||||
<MobileHeader>{!md && <HomeSidebarDrawer />}</MobileHeader>
|
<MobileHeader>{!md && <HomeSidebarDrawer />}</MobileHeader>
|
||||||
<div className="w-full px-4 sm:px-6 md:pr-2">
|
<div className="w-full px-4 sm:px-6 md:pr-2">
|
||||||
<MemoEditor className="mb-2" cacheKey="home-memo-editor" />
|
<MemoEditor className="mb-2" cacheKey="home-memo-editor" />
|
||||||
<div className="flex flex-col justify-start items-start w-full max-w-full overflow-y-scroll pb-28 hide-scrollbar">
|
<div className="flex flex-col justify-start items-start w-full max-w-full pb-28">
|
||||||
<MemoFilter />
|
<MemoFilter />
|
||||||
{sortedMemos.map((memo) => (
|
{sortedMemos.map((memo) => (
|
||||||
<MemoView key={`${memo.id}-${memo.updateTime}`} memo={memo} showVisibility showPinnedStyle showParent />
|
<MemoView key={`${memo.id}-${memo.updateTime}`} memo={memo} showVisibility showPinnedStyle showParent />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user