chore: fix calendar timestamps

This commit is contained in:
Steven 2024-07-27 09:47:12 +08:00
parent bdc257d837
commit 139090fb8f
11 changed files with 544 additions and 894 deletions

View File

@ -357,42 +357,6 @@ paths:
$ref: '#/definitions/v1CreateMemoRequest'
tags:
- MemoService
/api/v1/memos/stats:
get:
summary: GetUserMemosStats gets stats of memos for a user.
operationId: MemoService_GetUserMemosStats
responses:
"200":
description: A successful response.
schema:
$ref: '#/definitions/v1GetUserMemosStatsResponse'
default:
description: An unexpected error response.
schema:
$ref: '#/definitions/googlerpcStatus'
parameters:
- name: name
description: |-
name is the name of the user to get stats for.
Format: users/{id}
in: query
required: false
type: string
- name: timezone
description: |-
timezone location
Format: uses tz identifier
https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
in: query
required: false
type: string
- name: filter
description: Same as ListMemosRequest.filter
in: query
required: false
type: string
tags:
- MemoService
/api/v1/memos:by-uid/{uid}:
get:
summary: GetMemoByUid gets a memo by uid
@ -2417,17 +2381,6 @@ definitions:
content:
type: string
format: byte
v1GetUserMemosStatsResponse:
type: object
properties:
stats:
type: object
additionalProperties:
type: integer
format: int32
description: |-
stats is the stats of memo creating/updating activities.
key is the year-month-day string. e.g. "2020-01-01".
v1HTMLElementNode:
type: object
properties:
@ -2555,11 +2508,11 @@ definitions:
v1ListMemoPropertiesResponse:
type: object
properties:
properties:
entities:
type: array
items:
type: object
$ref: '#/definitions/v1MemoProperty'
$ref: '#/definitions/v1MemoPropertyEntity'
v1ListMemoReactionsResponse:
type: object
properties:
@ -2744,6 +2697,21 @@ definitions:
type: boolean
hasIncompleteTasks:
type: boolean
v1MemoPropertyEntity:
type: object
properties:
name:
type: string
title: |-
The name of the memo property.
Format: memos/{id}/properties/{property_id}
property:
$ref: '#/definitions/v1MemoProperty'
readOnly: true
displayTime:
type: string
format: date-time
readOnly: true
v1MemoRelation:
type: object
properties:

View File

@ -123,11 +123,6 @@ service MemoService {
option (google.api.http) = {get: "/api/v1/{name=memos/*}/comments"};
option (google.api.method_signature) = "name";
}
// GetUserMemosStats gets stats of memos for a user.
rpc GetUserMemosStats(GetUserMemosStatsRequest) returns (GetUserMemosStatsResponse) {
option (google.api.http) = {get: "/api/v1/memos/stats"};
option (google.api.method_signature) = "username";
}
// ListMemoReactions lists reactions for a memo.
rpc ListMemoReactions(ListMemoReactionsRequest) returns (ListMemoReactionsResponse) {
option (google.api.http) = {get: "/api/v1/{name=memos/*}/reactions"};
@ -281,7 +276,17 @@ message ListMemoPropertiesRequest {
}
message ListMemoPropertiesResponse {
repeated MemoProperty properties = 1;
repeated MemoPropertyEntity entities = 1;
}
message MemoPropertyEntity {
// The name of the memo property.
// Format: memos/{id}/properties/{property_id}
string name = 1;
MemoProperty property = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
google.protobuf.Timestamp display_time = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
}
message RebuildMemoPropertyRequest {
@ -377,26 +382,6 @@ message ListMemoCommentsResponse {
repeated Memo memos = 1;
}
message GetUserMemosStatsRequest {
// name is the name of the user to get stats for.
// Format: users/{id}
string name = 1;
// timezone location
// Format: uses tz identifier
// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
string timezone = 2;
// Same as ListMemosRequest.filter
string filter = 3;
}
message GetUserMemosStatsResponse {
// stats is the stats of memo creating/updating activities.
// key is the year-month-day string. e.g. "2020-01-01".
map<string, int32> stats = 1;
}
message ListMemoReactionsRequest {
// The name of the memo.
// Format: memos/{id}

File diff suppressed because it is too large Load Diff

View File

@ -1043,42 +1043,6 @@ func local_request_MemoService_ListMemoComments_0(ctx context.Context, marshaler
}
var (
filter_MemoService_GetUserMemosStats_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_MemoService_GetUserMemosStats_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetUserMemosStatsRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_GetUserMemosStats_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.GetUserMemosStats(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_MemoService_GetUserMemosStats_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq GetUserMemosStatsRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_GetUserMemosStats_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.GetUserMemosStats(ctx, &protoReq)
return msg, metadata, err
}
func request_MemoService_ListMemoReactions_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ListMemoReactionsRequest
var metadata runtime.ServerMetadata
@ -1699,31 +1663,6 @@ func RegisterMemoServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux
})
mux.Handle("GET", pattern_MemoService_GetUserMemosStats_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/GetUserMemosStats", runtime.WithHTTPPathPattern("/api/v1/memos/stats"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_MemoService_GetUserMemosStats_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_MemoService_GetUserMemosStats_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_MemoService_ListMemoReactions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@ -2236,28 +2175,6 @@ func RegisterMemoServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux
})
mux.Handle("GET", pattern_MemoService_GetUserMemosStats_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/GetUserMemosStats", runtime.WithHTTPPathPattern("/api/v1/memos/stats"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_MemoService_GetUserMemosStats_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_MemoService_GetUserMemosStats_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_MemoService_ListMemoReactions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@ -2364,8 +2281,6 @@ var (
pattern_MemoService_ListMemoComments_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "memos", "name", "comments"}, ""))
pattern_MemoService_GetUserMemosStats_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "memos", "stats"}, ""))
pattern_MemoService_ListMemoReactions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "memos", "name", "reactions"}, ""))
pattern_MemoService_UpsertMemoReaction_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "memos", "name", "reactions"}, ""))
@ -2410,8 +2325,6 @@ var (
forward_MemoService_ListMemoComments_0 = runtime.ForwardResponseMessage
forward_MemoService_GetUserMemosStats_0 = runtime.ForwardResponseMessage
forward_MemoService_ListMemoReactions_0 = runtime.ForwardResponseMessage
forward_MemoService_UpsertMemoReaction_0 = runtime.ForwardResponseMessage

View File

@ -38,7 +38,6 @@ const (
MemoService_ListMemoRelations_FullMethodName = "/memos.api.v1.MemoService/ListMemoRelations"
MemoService_CreateMemoComment_FullMethodName = "/memos.api.v1.MemoService/CreateMemoComment"
MemoService_ListMemoComments_FullMethodName = "/memos.api.v1.MemoService/ListMemoComments"
MemoService_GetUserMemosStats_FullMethodName = "/memos.api.v1.MemoService/GetUserMemosStats"
MemoService_ListMemoReactions_FullMethodName = "/memos.api.v1.MemoService/ListMemoReactions"
MemoService_UpsertMemoReaction_FullMethodName = "/memos.api.v1.MemoService/UpsertMemoReaction"
MemoService_DeleteMemoReaction_FullMethodName = "/memos.api.v1.MemoService/DeleteMemoReaction"
@ -84,8 +83,6 @@ type MemoServiceClient interface {
CreateMemoComment(ctx context.Context, in *CreateMemoCommentRequest, opts ...grpc.CallOption) (*Memo, error)
// ListMemoComments lists comments for a memo.
ListMemoComments(ctx context.Context, in *ListMemoCommentsRequest, opts ...grpc.CallOption) (*ListMemoCommentsResponse, error)
// GetUserMemosStats gets stats of memos for a user.
GetUserMemosStats(ctx context.Context, in *GetUserMemosStatsRequest, opts ...grpc.CallOption) (*GetUserMemosStatsResponse, error)
// ListMemoReactions lists reactions for a memo.
ListMemoReactions(ctx context.Context, in *ListMemoReactionsRequest, opts ...grpc.CallOption) (*ListMemoReactionsResponse, error)
// UpsertMemoReaction upserts a reaction for a memo.
@ -282,16 +279,6 @@ func (c *memoServiceClient) ListMemoComments(ctx context.Context, in *ListMemoCo
return out, nil
}
func (c *memoServiceClient) GetUserMemosStats(ctx context.Context, in *GetUserMemosStatsRequest, opts ...grpc.CallOption) (*GetUserMemosStatsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetUserMemosStatsResponse)
err := c.cc.Invoke(ctx, MemoService_GetUserMemosStats_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *memoServiceClient) ListMemoReactions(ctx context.Context, in *ListMemoReactionsRequest, opts ...grpc.CallOption) (*ListMemoReactionsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListMemoReactionsResponse)
@ -362,8 +349,6 @@ type MemoServiceServer interface {
CreateMemoComment(context.Context, *CreateMemoCommentRequest) (*Memo, error)
// ListMemoComments lists comments for a memo.
ListMemoComments(context.Context, *ListMemoCommentsRequest) (*ListMemoCommentsResponse, error)
// GetUserMemosStats gets stats of memos for a user.
GetUserMemosStats(context.Context, *GetUserMemosStatsRequest) (*GetUserMemosStatsResponse, error)
// ListMemoReactions lists reactions for a memo.
ListMemoReactions(context.Context, *ListMemoReactionsRequest) (*ListMemoReactionsResponse, error)
// UpsertMemoReaction upserts a reaction for a memo.
@ -431,9 +416,6 @@ func (UnimplementedMemoServiceServer) CreateMemoComment(context.Context, *Create
func (UnimplementedMemoServiceServer) ListMemoComments(context.Context, *ListMemoCommentsRequest) (*ListMemoCommentsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListMemoComments not implemented")
}
func (UnimplementedMemoServiceServer) GetUserMemosStats(context.Context, *GetUserMemosStatsRequest) (*GetUserMemosStatsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUserMemosStats not implemented")
}
func (UnimplementedMemoServiceServer) ListMemoReactions(context.Context, *ListMemoReactionsRequest) (*ListMemoReactionsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListMemoReactions not implemented")
}
@ -780,24 +762,6 @@ func _MemoService_ListMemoComments_Handler(srv interface{}, ctx context.Context,
return interceptor(ctx, in, info, handler)
}
func _MemoService_GetUserMemosStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetUserMemosStatsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MemoServiceServer).GetUserMemosStats(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: MemoService_GetUserMemosStats_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MemoServiceServer).GetUserMemosStats(ctx, req.(*GetUserMemosStatsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MemoService_ListMemoReactions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListMemoReactionsRequest)
if err := dec(in); err != nil {
@ -931,10 +895,6 @@ var MemoService_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListMemoComments",
Handler: _MemoService_ListMemoComments_Handler,
},
{
MethodName: "GetUserMemosStats",
Handler: _MemoService_GetUserMemosStats_Handler,
},
{
MethodName: "ListMemoReactions",
Handler: _MemoService_ListMemoReactions_Handler,

View File

@ -491,61 +491,6 @@ func (s *APIV1Service) ListMemoComments(ctx context.Context, request *v1pb.ListM
return response, nil
}
func (s *APIV1Service) GetUserMemosStats(ctx context.Context, request *v1pb.GetUserMemosStatsRequest) (*v1pb.GetUserMemosStatsResponse, error) {
userID, err := ExtractUserIDFromName(request.Name)
if err != nil {
return nil, errors.Wrap(err, "invalid user name")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user")
}
if user == nil {
return nil, status.Errorf(codes.NotFound, "user not found")
}
normalRowStatus := store.Normal
memoFind := &store.FindMemo{
CreatorID: &user.ID,
RowStatus: &normalRowStatus,
ExcludeComments: true,
ExcludeContent: true,
}
if err := s.buildMemoFindWithFilter(ctx, memoFind, request.Filter); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "failed to build find memos with filter")
}
memos, err := s.Store.ListMemos(ctx, memoFind)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err)
}
location, err := time.LoadLocation(request.Timezone)
if err != nil {
return nil, status.Errorf(codes.Internal, "invalid timezone location")
}
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get workspace memo related setting")
}
stats := make(map[string]int32)
for _, memo := range memos {
displayTs := memo.CreatedTs
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
displayTs = memo.UpdatedTs
}
stats[time.Unix(displayTs, 0).In(location).Format("2006-01-02")]++
}
response := &v1pb.GetUserMemosStatsResponse{
Stats: stats,
}
return response, nil
}
func (s *APIV1Service) ExportMemos(ctx context.Context, request *v1pb.ExportMemosRequest) (*v1pb.ExportMemosResponse, error) {
normalRowStatus := store.Normal
memoFind := &store.FindMemo{
@ -614,14 +559,28 @@ func (s *APIV1Service) ListMemoProperties(ctx context.Context, request *v1pb.Lis
return nil, status.Errorf(codes.Internal, "failed to list memos")
}
properties := []*v1pb.MemoProperty{}
workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to get workspace memo related setting")
}
entities := []*v1pb.MemoPropertyEntity{}
for _, memo := range memos {
if memo.Payload.Property != nil {
properties = append(properties, convertMemoPropertyFromStore(memo.Payload.Property))
displayTs := memo.CreatedTs
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
displayTs = memo.UpdatedTs
}
entity := &v1pb.MemoPropertyEntity{
Name: fmt.Sprintf("%s%d", MemoNamePrefix, memo.ID),
DisplayTime: timestamppb.New(time.Unix(displayTs, 0)),
}
if memo.Payload.Property != nil {
entity.Property = convertMemoPropertyFromStore(memo.Payload.Property)
}
entities = append(entities, entity)
}
return &v1pb.ListMemoPropertiesResponse{
Properties: properties,
Entities: entities,
}, nil
}

View File

@ -1,6 +1,6 @@
import { Tooltip } from "@mui/joy";
import clsx from "clsx";
import { getNormalizedDateString, getDateWithOffset } from "@/helpers/datetime";
import dayjs from "dayjs";
import { useTranslate } from "@/utils/i18n";
interface Props {
@ -29,8 +29,8 @@ const getCellAdditionalStyles = (count: number, maxCount: number) => {
const ActivityCalendar = (props: Props) => {
const t = useTranslate();
const { month: monthStr, data, onClick } = props;
const year = new Date(monthStr).getFullYear();
const month = new Date(monthStr).getMonth() + 1;
const year = dayjs(monthStr).toDate().getFullYear();
const month = dayjs(monthStr).toDate().getMonth() + 1;
const dayInMonth = new Date(year, month, 0).getDate();
const firstDay = new Date(year, month - 1, 1).getDay();
const lastDay = new Date(year, month - 1, dayInMonth).getDay();
@ -49,14 +49,19 @@ const ActivityCalendar = (props: Props) => {
return (
<div className={clsx("w-full h-auto shrink-0 grid grid-cols-7 grid-flow-row gap-1")}>
<div className={clsx("w-6 h-5 text-xs flex justify-center items-center cursor-default opacity-60")}>Su</div>
<div className={clsx("w-6 h-5 text-xs flex justify-center items-center cursor-default opacity-60")}>Mo</div>
<div className={clsx("w-6 h-5 text-xs flex justify-center items-center cursor-default opacity-60")}>Tu</div>
<div className={clsx("w-6 h-5 text-xs flex justify-center items-center cursor-default opacity-60")}>We</div>
<div className={clsx("w-6 h-5 text-xs flex justify-center items-center cursor-default opacity-60")}>Th</div>
<div className={clsx("w-6 h-5 text-xs flex justify-center items-center cursor-default opacity-60")}>Fr</div>
<div className={clsx("w-6 h-5 text-xs flex justify-center items-center cursor-default opacity-60")}>Sa</div>
{days.map((day, index) => {
const date = getNormalizedDateString(
getDateWithOffset(`${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`),
);
const date = dayjs(`${year}-${month}-${day}`).format("YYYY-MM-DD");
const count = data[date] || 0;
const isToday = new Date().toDateString() === new Date(date).toDateString();
const isToday = dayjs().format("YYYY-MM-DD") === date;
const tooltipText = count ? t("memo.count-memos-in-date", { count: count, date: date }) : date;
const isSelected = new Date(props.selectedDate).toDateString() === new Date(date).toDateString();
const isSelected = dayjs(props.selectedDate).format("YYYY-MM-DD") === date;
return day ? (
count > 0 ? (
<Tooltip className="shrink-0" key={`${date}-${index}`} title={tooltipText} placement="top" arrow>
@ -68,7 +73,7 @@ const ActivityCalendar = (props: Props) => {
isSelected && "font-bold border-zinc-400 dark:border-zinc-300",
!isToday && !isSelected && "border-transparent",
)}
onClick={() => count && onClick && onClick(new Date(date).toDateString())}
onClick={() => count && onClick && onClick(date)}
>
{day}
</div>

View File

@ -1,6 +1,7 @@
import { Divider, Tooltip } from "@mui/joy";
import clsx from "clsx";
import dayjs from "dayjs";
import { chain } from "lodash-es";
import { useState } from "react";
import toast from "react-hot-toast";
import { memoServiceClient } from "@/grpcweb";
@ -33,43 +34,36 @@ const UserStatisticsView = () => {
const days = Math.ceil((Date.now() - currentUser.createTime!.getTime()) / 86400000);
useAsyncEffect(async () => {
const { properties } = await memoServiceClient.listMemoProperties({
const { entities } = await memoServiceClient.listMemoProperties({
name: `memos/-`,
});
const memoStats: UserMemoStats = { link: 0, taskList: 0, code: 0, incompleteTasks: 0 };
properties.forEach((property) => {
if (property.hasLink) {
entities.forEach((entity) => {
const { property } = entity;
if (property?.hasLink) {
memoStats.link += 1;
}
if (property.hasTaskList) {
if (property?.hasTaskList) {
memoStats.taskList += 1;
}
if (property.hasCode) {
if (property?.hasCode) {
memoStats.code += 1;
}
if (property.hasIncompleteTasks) {
if (property?.hasIncompleteTasks) {
memoStats.incompleteTasks += 1;
}
});
const displayTimes = entities.map((entity) => entity.displayTime).filter(Boolean) as Date[];
const monthStrGroup = chain(displayTimes)
.map((date) => dayjs(date).format("YYYY-MM-DD"))
.countBy()
.value();
setMemoStats(memoStats);
setMemoAmount(properties.length);
const filters = [`row_status == "NORMAL"`];
const { stats } = await memoServiceClient.getUserMemosStats({
name: currentUser.name,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
filter: filters.join(" && "),
});
setActivityStats(
Object.fromEntries(
Object.entries(stats).filter(([date]) => {
return dayjs(date).format("YYYY-MM") === monthString;
}),
),
);
setMemoAmount(entities.length);
setActivityStats(monthStrGroup);
}, [memoStore.stateId]);
const handleRebuildMemoTags = async () => {
const rebuildMemoTags = async () => {
await memoServiceClient.rebuildMemoProperty({
name: "memos/-",
});
@ -77,12 +71,17 @@ const UserStatisticsView = () => {
window.location.reload();
};
const onCalendarClick = (date: string) => {
memoFilterStore.removeFilter((f) => f.factor === "displayTime");
memoFilterStore.addFilter({ factor: "displayTime", value: date });
};
return (
<div className="group w-full border mt-2 py-2 px-3 rounded-lg space-y-0.5 text-gray-500 dark:text-gray-400 bg-zinc-50 dark:bg-zinc-900 dark:border-zinc-800">
<div className="w-full mb-2 flex flex-row justify-between items-center">
<div className="w-full mb-1 flex flex-row justify-between items-center">
<div className="relative text-base font-medium leading-6 flex flex-row items-center dark:text-gray-400">
<Icon.CalendarDays className="w-5 h-auto mr-1 opacity-60" strokeWidth={1.5} />
<span>{new Date(monthString).toLocaleString(i18n.language, { year: "numeric", month: "long" })}</span>
<span>{dayjs(monthString).toDate().toLocaleString(i18n.language, { year: "numeric", month: "long" })}</span>
<input
className="inset-0 absolute z-1 opacity-0"
type="month"
@ -97,7 +96,7 @@ const UserStatisticsView = () => {
<Icon.MoreVertical className="w-4 h-auto shrink-0 opacity-60" />
</PopoverTrigger>
<PopoverContent>
<button className="w-auto flex flex-row justify-between items-center gap-2 hover:opacity-80" onClick={handleRebuildMemoTags}>
<button className="w-auto flex flex-row justify-between items-center gap-2 hover:opacity-80" onClick={rebuildMemoTags}>
<Icon.RefreshCcw className="text-gray-400 w-4 h-auto cursor-pointer opacity-60" />
<span className="text-sm shrink-0 text-gray-500 dark:text-gray-400">Refresh</span>
</button>
@ -106,10 +105,10 @@ const UserStatisticsView = () => {
</div>
</div>
<div className="w-full">
<ActivityCalendar month={monthString} selectedDate={selectedDate.toDateString()} data={activityStats} />
<ActivityCalendar month={monthString} selectedDate={selectedDate.toDateString()} data={activityStats} onClick={onCalendarClick} />
{memoAmount > 0 && (
<p className="mt-1 w-full text-xs italic opacity-80">
<span>{memoAmount}</span> memos in <span>{days}</span> days
<span>{memoAmount}</span> memos in <span>{days}</span> {days > 1 ? "days" : "day"}
</p>
)}
</div>

View File

@ -4,26 +4,6 @@ export function getTimeStampByDate(t: Date | number | string | any): number {
return new Date(t).getTime();
}
/**
* Get a time string to provided time.
*
* If no date is provided, the current date is used.
*
* Output is always ``HH:MM`` (24-hour format)
*/
export function getTimeString(t?: Date | number | string): string {
const tsFromDate = getTimeStampByDate(t ? t : Date.now());
const d = new Date(tsFromDate);
const hours = d.getHours();
const mins = d.getMinutes();
const hoursStr = hours < 10 ? "0" + hours : hours;
const minsStr = mins < 10 ? "0" + mins : mins;
return `${hoursStr}:${minsStr}`;
}
/**
* Get a localized date and time string to provided time.
*
@ -49,51 +29,3 @@ export function getDateTimeString(t?: Date | number | string | any, locale = i18
return tsFromDate.toLocaleString();
}
}
/**
* This returns the normalized date string of the provided date.
* Format is always `YYYY-MM-DDT00:00`.
*
* If no date is provided, the current date is used.
*/
export function getNormalizedTimeString(t?: Date | number | string): string {
const date = new Date(t ? t : Date.now());
const yyyy = date.getFullYear();
const M = date.getMonth() + 1;
const d = date.getDate();
const h = date.getHours();
const m = date.getMinutes();
const MM = M < 10 ? "0" + M : M;
const dd = d < 10 ? "0" + d : d;
const hh = h < 10 ? "0" + h : h;
const mm = m < 10 ? "0" + m : m;
return `${yyyy}-${MM}-${dd}T${hh}:${mm}`;
}
export function getNormalizedDateString(t?: Date | number | string): string {
const date = new Date(t ? t : Date.now());
const yyyy = date.getFullYear();
const M = date.getMonth() + 1;
const d = date.getDate();
const MM = M < 10 ? "0" + M : M;
const dd = d < 10 ? "0" + d : d;
return `${yyyy}-${MM}-${dd}`;
}
/**
* Calculates a new Date object by adjusting the provided date, timestamp, or date string
* based on the current timezone offset.
*
* @param t - The input date, timestamp, or date string (optional). If not provided,
* the current date and time will be used.
* @returns A new Date object adjusted by the current timezone offset.
*/
export function getDateWithOffset(t?: Date | number | string): Date {
return new Date(getTimeStampByDate(t) + new Date().getTimezoneOffset() * 60 * 1000);
}

View File

@ -9,11 +9,13 @@ import useCurrentUser from "@/hooks/useCurrentUser";
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
import Loading from "@/pages/Loading";
import { Routes } from "@/router";
import { useMemoFilterStore } from "@/store/v1";
const RootLayout = () => {
const location = useLocation();
const { sm } = useResponsiveWidth();
const currentUser = useCurrentUser();
const memoFilterStore = useMemoFilterStore();
const [collapsed, setCollapsed] = useLocalStorage<boolean>("navigation-collapsed", false);
const [initialized, setInitialized] = useState(false);
@ -27,6 +29,11 @@ const RootLayout = () => {
setInitialized(true);
}, []);
useEffect(() => {
// When the route changes, remove all filters.
memoFilterStore.removeFilter(() => true);
}, [location.pathname]);
return !initialized ? (
<Loading />
) : (

View File

@ -51,6 +51,10 @@ const Home = () => {
filters.push(`has_task_list == true`);
} else if (filter.factor === "property.hasCode") {
filters.push(`has_code == true`);
} else if (filter.factor === "displayTime") {
const timestampAfter = getTimeStampByDate(new Date(filter.value)) / 1000;
filters.push(`display_time_after == ${timestampAfter}`);
filters.push(`display_time_before == ${timestampAfter + 60 * 60 * 24}`);
}
}
if (contentSearch.length > 0) {