feat(mobile): new journal date-picker (#8757)

close AF-1649

<div class='graphite__hidden'>
          <div>🎥 Video uploaded on Graphite:</div>
            <a href="https://app.graphite.dev/media/video/LakojjjzZNf6ogjOVwKE/a83ca41a-75ac-4d12-959a-23f06740a76d.mp4">
              <img src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/LakojjjzZNf6ogjOVwKE/a83ca41a-75ac-4d12-959a-23f06740a76d.mp4">
            </a>
          </div>
<video src="https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/LakojjjzZNf6ogjOVwKE/a83ca41a-75ac-4d12-959a-23f06740a76d.mp4">CleanShot 2024-11-09 at 12.54.41.mp4</video>
This commit is contained in:
CatsJuice 2024-11-11 09:31:30 +00:00
parent 9239eed6a7
commit 50a04f6443
No known key found for this signature in database
GPG Key ID: 1C1E76924FAFDDE4
15 changed files with 828 additions and 47 deletions

View File

@ -0,0 +1,11 @@
export const CELL_HEIGHT = 34;
export const TOTAL_WEEKS = 6;
export const ROWS_GAP = 4;
export const MONTH_VIEW_HEIGHT =
TOTAL_WEEKS * CELL_HEIGHT + (TOTAL_WEEKS - 1) * ROWS_GAP;
export const WEEK_VIEW_HEIGHT = CELL_HEIGHT;
export const HORIZONTAL_SWIPE_THRESHOLD = 4 * CELL_HEIGHT;
export const DATE_FORMAT = 'YYYY-MM-DD';

View File

@ -0,0 +1,21 @@
import { createContext } from 'react';
export const JournalDatePickerContext = createContext<{
width: number;
/**
* Is used to determine the current date, not same as selected,
* `is-current-month` is based on cursor
*/
cursor: string;
setCursor: (date: string) => void;
selected: string;
onSelect: (date: string) => void;
withDotDates: Set<string | null | undefined>;
}>({
width: window.innerWidth,
cursor: '',
setCursor: () => {},
selected: '',
onSelect: () => {},
withDotDates: new Set(),
});

View File

@ -0,0 +1,40 @@
import { bodyRegular } from '@toeverything/theme/typography';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const dayCell = style([
bodyRegular,
{
position: 'relative',
height: 34,
minWidth: 34,
padding: 4,
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
selectors: {
'&[data-is-today=true]': {},
'&[data-is-current-month=false]': {
color: cssVarV2('text/disable'),
},
'&[data-is-selected=true]': {
background: cssVarV2('button/primary'),
color: cssVarV2('button/pureWhiteText'),
fontWeight: 600,
},
},
},
]);
export const dot = style({
position: 'absolute',
width: 3,
height: 3,
borderRadius: 3,
background: cssVarV2('button/primary'),
bottom: 3,
left: '50%',
transform: 'translateX(-50%)',
});

View File

@ -0,0 +1,39 @@
import dayjs from 'dayjs';
import { memo, useContext, useMemo } from 'react';
import { DATE_FORMAT } from './constants';
import { JournalDatePickerContext } from './context';
import { dayCell, dot } from './day-cell.css';
export interface DayCellProps {
date: string;
}
export const DayCell = memo(function DayCell({ date }: DayCellProps) {
const { selected, onSelect, cursor, withDotDates } = useContext(
JournalDatePickerContext
);
const dayjsObj = useMemo(() => dayjs(date), [date]);
const isToday = dayjsObj.isSame(dayjs(), 'day');
const isSelected = dayjsObj.isSame(dayjs(selected), 'day');
const isCurrentMonth = dayjsObj.isSame(dayjs(cursor), 'month');
const day = dayjsObj.get('date');
const label = dayjsObj.format(DATE_FORMAT);
const hasDot = withDotDates.has(date);
return (
<div
className={dayCell}
data-is-today={isToday}
data-is-selected={isSelected}
data-is-current-month={isCurrentMonth}
aria-label={label}
onClick={() => onSelect(date)}
>
{day}
{hasDot && <div className={dot} />}
</div>
);
});

View File

@ -0,0 +1,45 @@
import { useCallback, useEffect, useState } from 'react';
import { JournalDatePickerContext } from './context';
import { ResizeViewport } from './viewport';
export interface JournalDatePickerProps {
date: string;
onChange: (date: string) => void;
withDotDates: Set<string | null | undefined>;
}
export const JournalDatePicker = ({
date: selected,
onChange,
withDotDates,
}: JournalDatePickerProps) => {
const [cursor, setCursor] = useState(selected);
// should update cursor when selected modified outside
useEffect(() => {
setCursor(selected);
}, [selected]);
const onSelect = useCallback(
(date: string) => {
setCursor(date);
onChange(date);
},
[onChange]
);
return (
<JournalDatePickerContext.Provider
value={{
selected,
onSelect,
cursor,
setCursor,
width: window.innerWidth,
withDotDates,
}}
>
<ResizeViewport></ResizeViewport>
</JournalDatePickerContext.Provider>
);
};

View File

@ -0,0 +1,24 @@
import { style } from '@vanilla-extract/css';
export const monthViewClip = style({
width: '100%',
height: '100%',
overflow: 'hidden',
});
export const monthsSwipe = style({
width: '300%',
height: '100%',
marginLeft: '-100%',
display: 'flex',
justifyContent: 'center',
});
export const monthView = style({
width: 0,
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: 4,
padding: '0 16px',
});

View File

@ -0,0 +1,160 @@
import anime from 'animejs';
import clsx from 'clsx';
import dayjs from 'dayjs';
import {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
CELL_HEIGHT,
DATE_FORMAT,
HORIZONTAL_SWIPE_THRESHOLD,
MONTH_VIEW_HEIGHT,
ROWS_GAP,
WEEK_VIEW_HEIGHT,
} from './constants';
import { JournalDatePickerContext } from './context';
import * as styles from './month.css';
import { SwipeHelper } from './swipe-helper';
import { getFirstDayOfMonth } from './utils';
import { WeekRow } from './week';
export interface MonthViewProps {
viewportHeight: number;
}
function getWeeks(date: string) {
const today = dayjs(date);
const firstDayOfMonth = today.startOf('month');
const firstWeekday = firstDayOfMonth.startOf('week');
const weeks = [];
for (let i = 0; i < 6; i++) {
const week = firstWeekday.add(i * 7, 'day');
weeks.push(week.format(DATE_FORMAT));
}
return weeks;
}
export const MonthView = ({ viewportHeight }: MonthViewProps) => {
const rootRef = useRef<HTMLDivElement>(null);
const swipeRef = useRef<HTMLDivElement>(null);
const { width, selected, onSelect, setCursor } = useContext(
JournalDatePickerContext
);
const [swiping, setSwiping] = useState(false);
const [animating, setAnimating] = useState(false);
const [swipingDeltaX, setSwipingDeltaX] = useState(0);
const weeks = useMemo(
() => getWeeks(selected ?? dayjs().format(DATE_FORMAT)),
[selected]
);
const firstWeekDayOfSelected = dayjs(selected)
.startOf('week')
.format(DATE_FORMAT);
const activeRowIndex = weeks.indexOf(firstWeekDayOfSelected);
// pointA: (WEEK_VIEW_HEIGHT, maxY)
// pointB: (MONTH_VIEW_HEIGHT, 0)
const maxY = -(activeRowIndex * (CELL_HEIGHT + ROWS_GAP));
const k = maxY / (WEEK_VIEW_HEIGHT - MONTH_VIEW_HEIGHT);
const b = -k * MONTH_VIEW_HEIGHT;
const translateY = k * viewportHeight + b;
const translateX = Math.max(-width, Math.min(width, swipingDeltaX));
const animateTo = useCallback(
(dir: 0 | 1 | -1) => {
setAnimating(true);
anime({
targets: swipeRef.current,
translateX: -dir * width,
duration: 300,
easing: 'easeInOutSine',
complete: () => {
setSwipingDeltaX(0);
setAnimating(false);
// should recover swipe before change month
if (dir !== 0) {
setTimeout(() => onSelect(getFirstDayOfMonth(selected, dir)));
}
},
});
},
[onSelect, selected, width]
);
useEffect(() => {
if (!rootRef.current) return;
const swipeHelper = new SwipeHelper();
return swipeHelper.init(rootRef.current, {
preventScroll: true,
onSwipe: ({ deltaX }) => {
setSwiping(true);
setSwipingDeltaX(deltaX);
if (Math.abs(deltaX) > HORIZONTAL_SWIPE_THRESHOLD) {
setCursor(getFirstDayOfMonth(selected, deltaX > 0 ? -1 : 1));
} else {
setCursor(selected);
}
},
onSwipeEnd: ({ deltaX }) => {
setSwiping(false);
if (Math.abs(deltaX) > HORIZONTAL_SWIPE_THRESHOLD) {
animateTo(deltaX > 0 ? -1 : 1);
} else {
animateTo(0);
}
},
});
}, [animateTo, selected, setCursor]);
return (
<div className={styles.monthViewClip} ref={rootRef}>
<div
ref={swipeRef}
className={styles.monthsSwipe}
style={{ transform: `translateX(${translateX}px)` }}
>
<MonthGrid
hidden={!swiping && !animating}
date={getFirstDayOfMonth(selected, -1)}
/>
{/* Active month */}
<MonthGrid
style={{ transform: `translateY(${translateY}px)` }}
date={selected ?? ''}
/>
<MonthGrid
hidden={!swiping && !animating}
date={getFirstDayOfMonth(selected, 1)}
/>
</div>
</div>
);
};
interface MonthGridProps extends React.HTMLAttributes<HTMLDivElement> {
date: string;
hidden?: boolean;
}
const MonthGrid = ({ date, className, hidden, ...props }: MonthGridProps) => {
const weeks = useMemo(
() => getWeeks(date ?? dayjs().format(DATE_FORMAT)),
[date]
);
return (
<div className={clsx(styles.monthView, className)} {...props}>
{hidden ? null : weeks.map(week => <WeekRow key={week} start={week} />)}
</div>
);
};

View File

@ -0,0 +1,133 @@
export interface SwipeInfo {
e: TouchEvent;
startX: number;
startY: number;
endX: number;
endY: number;
deltaX: number;
deltaY: number;
isFirst: boolean;
}
export interface SwipeHelperOptions {
scope?: 'global' | 'inside';
preventScroll?: boolean;
onTap?: () => void;
onSwipeStart?: () => void;
onSwipe?: (info: SwipeInfo) => void;
onSwipeEnd?: (info: SwipeInfo) => void;
}
export class SwipeHelper {
private _trigger: HTMLElement | null = null;
private _options: SwipeHelperOptions = {
scope: 'global',
};
private _startPos: { x: number; y: number } = { x: 0, y: 0 };
private _isFirst: boolean = true;
private _lastInfo: SwipeInfo | null = null;
get scopeElement() {
return this._options.scope === 'inside'
? (this._trigger ?? document.body)
: document.body;
}
private _dragStartCleanup: () => void = () => {};
private _dragMoveCleanup: () => void = () => {};
private _dragEndCleanup: () => void = () => {};
/**
* Register touch event to observe drag gesture
*/
public init(trigger: HTMLElement, options?: SwipeHelperOptions) {
this.destroy();
this._options = { ...this._options, ...options };
this._trigger = trigger;
const handler = this._handleTouchStart.bind(this);
trigger.addEventListener('touchstart', handler, {
passive: !this._options.preventScroll,
});
this._dragStartCleanup = () => {
trigger.removeEventListener('touchstart', handler);
};
return () => this.destroy();
}
/**
* Remove all listeners
*/
public destroy() {
this._dragStartCleanup();
this._clearDrag();
}
private _handleTouchStart(e: TouchEvent) {
const touch = e.touches[0];
this._startPos = {
x: touch.clientX,
y: touch.clientY,
};
this._options.onSwipeStart?.();
const moveHandler = this._handleTouchMove.bind(this);
this.scopeElement.addEventListener('touchmove', moveHandler, {
passive: !this._options.preventScroll,
});
const endHandler = this._handleTouchEnd.bind(this);
this.scopeElement.addEventListener('touchend', endHandler, {
passive: !this._options.preventScroll,
});
this._dragMoveCleanup = () => {
this.scopeElement.removeEventListener('touchmove', moveHandler);
};
this._dragEndCleanup = () => {
this.scopeElement.removeEventListener('touchend', endHandler);
};
}
private _handleTouchMove(e: TouchEvent) {
if (this._options.preventScroll) {
e.preventDefault();
}
const info = this._getInfo(e);
this._lastInfo = info;
this._isFirst = false;
this._options.onSwipe?.(info);
}
private _handleTouchEnd() {
if (
!this._lastInfo ||
(Math.abs(this._lastInfo.deltaY) < 1 &&
Math.abs(this._lastInfo.deltaX) < 1)
) {
this._options.onTap?.();
} else {
this._options.onSwipeEnd?.(this._lastInfo);
}
this._clearDrag();
}
private _getInfo(e: TouchEvent): SwipeInfo {
const touch = e.touches[0];
const deltaX = touch.clientX - this._startPos.x;
const deltaY = touch.clientY - this._startPos.y;
return {
e,
startX: this._startPos.x,
startY: this._startPos.y,
endX: touch.clientX,
endY: touch.clientY,
deltaX,
deltaY,
isFirst: this._isFirst,
};
}
private _clearDrag() {
this._lastInfo = null;
this._dragMoveCleanup();
this._dragEndCleanup();
}
}

View File

@ -0,0 +1,10 @@
import dayjs from 'dayjs';
import { DATE_FORMAT } from './constants';
export const getFirstDayOfMonth = (date: string, offset: number) => {
return dayjs(date).add(offset, 'month').startOf('month').format(DATE_FORMAT);
};
export const getFirstDayOfWeek = (date: string, offset: number) => {
return dayjs(date).add(offset, 'week').startOf('week').format(DATE_FORMAT);
};

View File

@ -0,0 +1,24 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const root = style({
width: '100%',
borderBottom: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
});
export const weekRow = style({
padding: '0 16px',
});
export const draggable = style({
width: '100%',
padding: '10px 0',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});
export const draggableHandle = style({
width: 36,
height: 5,
borderRadius: 5,
background: cssVarV2('block/notSupportedBlock/inlineBg/hover'),
});

View File

@ -0,0 +1,115 @@
import anime from 'animejs';
import dayjs from 'dayjs';
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
import {
CELL_HEIGHT,
DATE_FORMAT,
MONTH_VIEW_HEIGHT,
WEEK_VIEW_HEIGHT,
} from './constants';
import { JournalDatePickerContext } from './context';
import { MonthView } from './month';
import { SwipeHelper } from './swipe-helper';
import * as styles from './viewport.css';
import { WeekHeader, WeekRowSwipe } from './week';
export const ResizeViewport = () => {
const { selected } = useContext(JournalDatePickerContext);
const draggableRef = useRef<HTMLDivElement>(null);
const viewportRef = useRef<HTMLDivElement>(null);
const [mode, setMode] = useState<'week' | 'month'>('week');
const [dragOffset, setDragOffset] = useState(0);
const [isDragging, setIsDragging] = useState(false);
const [isAnimating, setIsAnimating] = useState(false);
const firstDayOfWeek = dayjs(selected).startOf('week').format(DATE_FORMAT);
const staticHeight = mode === 'week' ? WEEK_VIEW_HEIGHT : MONTH_VIEW_HEIGHT;
const draggedHeight = Math.max(
WEEK_VIEW_HEIGHT,
Math.min(MONTH_VIEW_HEIGHT, staticHeight + dragOffset)
);
const handleToggleModeWithAnimation = useCallback(
(
targetMode: 'week' | 'month',
draggedDistance: number,
isCancel = false
) => {
const targetDragOffset = isCancel
? 0
: targetMode === 'week'
? -(MONTH_VIEW_HEIGHT - WEEK_VIEW_HEIGHT)
: MONTH_VIEW_HEIGHT - WEEK_VIEW_HEIGHT;
const dragOffsetProxy = new Proxy<{ value: number }>(
{ value: draggedDistance },
{
set(target, key, value) {
if (key !== 'value') return false;
setDragOffset(value);
target.value = value;
return true;
},
}
);
setIsAnimating(true);
anime({
targets: dragOffsetProxy,
value: targetDragOffset,
duration: 300,
easing: 'easeOutCubic',
complete: () => {
setMode(targetMode);
setDragOffset(0);
setIsDragging(false);
setIsAnimating(false);
},
});
},
[]
);
useEffect(() => {
if (!draggableRef.current) return;
const swipeHelper = new SwipeHelper();
return swipeHelper.init(draggableRef.current, {
preventScroll: true,
onTap() {
handleToggleModeWithAnimation(mode === 'week' ? 'month' : 'week', 0);
},
onSwipe: ({ deltaY }) => {
setDragOffset(deltaY);
setIsDragging(true);
},
onSwipeEnd: ({ deltaY }) => {
if (Math.abs(deltaY) > 2 * CELL_HEIGHT) {
handleToggleModeWithAnimation(
mode === 'week' ? 'month' : 'week',
deltaY
);
} else {
handleToggleModeWithAnimation(mode, deltaY, true);
}
},
});
}, [handleToggleModeWithAnimation, mode]);
return (
<div className={styles.root}>
<WeekHeader className={styles.weekRow} />
<div ref={viewportRef} style={{ height: draggedHeight }}>
{mode === 'month' || isDragging || isAnimating ? (
<MonthView viewportHeight={draggedHeight} />
) : (
<WeekRowSwipe start={firstDayOfWeek} />
)}
</div>
<div className={styles.draggable} ref={draggableRef}>
<div className={styles.draggableHandle} />
</div>
</div>
);
};

View File

@ -0,0 +1,31 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
import { dayCell } from './day-cell.css';
export const weekRow = style({
display: 'flex',
gap: 4,
justifyContent: 'space-between',
});
export const weekHeaderCell = style([
dayCell,
{
color: cssVarV2('text/secondary'),
},
]);
export const weekSwipeRoot = style({
width: '100%',
overflow: 'hidden',
});
export const weekSwipeSlide = style({
width: '300%',
marginLeft: '-100%',
display: 'flex',
});
export const weekSwipeItem = style({
width: 0,
flex: 1,
padding: '0 16px',
});

View File

@ -0,0 +1,157 @@
import { useI18n } from '@affine/i18n';
import anime from 'animejs';
import clsx from 'clsx';
import dayjs from 'dayjs';
import {
type HTMLAttributes,
memo,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { DATE_FORMAT, HORIZONTAL_SWIPE_THRESHOLD } from './constants';
import { JournalDatePickerContext } from './context';
import { DayCell } from './day-cell';
import { SwipeHelper } from './swipe-helper';
import { getFirstDayOfWeek } from './utils';
import * as styles from './week.css';
export interface WeekRowProps extends HTMLAttributes<HTMLDivElement> {
start: string;
}
export const WeekHeader = memo(function WeekHeader({
className,
...attrs
}: HTMLAttributes<HTMLDivElement>) {
const t = useI18n();
const days = useMemo(
() => t['com.affine.calendar-date-picker.week-days']().split(','),
[t]
);
return (
<div className={clsx(styles.weekRow, className)} {...attrs}>
{days.map(day => {
return (
<div className={styles.weekHeaderCell} key={day}>
{day}
</div>
);
})}
</div>
);
});
export const WeekRow = memo(function WeekRow({
start,
className,
...attrs
}: WeekRowProps) {
const days = useMemo(() => {
const days = [];
for (let i = 0; i < 7; i++) {
days.push(dayjs(start).add(i, 'day').format(DATE_FORMAT));
}
return days;
}, [start]);
return (
<div className={clsx(styles.weekRow, className)} {...attrs}>
{days.map(day => (
<DayCell date={day} key={day} />
))}
</div>
);
});
export const WeekRowSwipe = ({ start }: WeekRowProps) => {
const { width, onSelect, setCursor } = useContext(JournalDatePickerContext);
const rootRef = useRef<HTMLDivElement>(null);
const swipeRef = useRef<HTMLDivElement>(null);
const [swiping, setSwiping] = useState(false);
const [swipingDeltaX, setSwipingDeltaX] = useState(0);
const [animating, setAnimating] = useState(false);
const translateX = Math.max(-width, Math.min(width, swipingDeltaX));
const animateTo = useCallback(
(dir: 0 | 1 | -1) => {
setAnimating(true);
anime({
targets: swipeRef.current,
translateX: -dir * width,
easing: 'easeInOutSine',
duration: 300,
complete: () => {
setSwipingDeltaX(0);
setAnimating(false);
if (dir !== 0) {
setTimeout(() => onSelect(getFirstDayOfWeek(start, dir)));
}
},
});
},
[onSelect, start, width]
);
useEffect(() => {
if (!rootRef.current) return;
const swipeHelper = new SwipeHelper();
return swipeHelper.init(rootRef.current, {
preventScroll: true,
onSwipe({ deltaX }) {
setSwiping(true);
setSwipingDeltaX(deltaX);
if (Math.abs(deltaX) > HORIZONTAL_SWIPE_THRESHOLD) {
setCursor(getFirstDayOfWeek(start, deltaX > 0 ? -1 : 1));
} else {
setCursor(start);
}
},
onSwipeEnd({ deltaX }) {
setSwiping(false);
if (Math.abs(deltaX) > HORIZONTAL_SWIPE_THRESHOLD) {
animateTo(deltaX > 0 ? -1 : 1);
} else {
animateTo(0);
}
},
});
}, [animateTo, setCursor, start]);
return (
<div ref={rootRef} className={styles.weekSwipeRoot}>
<div
ref={swipeRef}
className={styles.weekSwipeSlide}
style={{ transform: `translateX(${translateX}px)` }}
>
{swiping || animating ? (
<WeekRow
className={styles.weekSwipeItem}
start={getFirstDayOfWeek(start, -1)}
/>
) : (
<div className={styles.weekSwipeItem} />
)}
<WeekRow className={styles.weekSwipeItem} start={start} />
{swiping || animating ? (
<WeekRow
className={styles.weekSwipeItem}
start={getFirstDayOfWeek(start, 1)}
/>
) : (
<div className={styles.weekSwipeItem} />
)}
</div>
</div>
);
};

View File

@ -1,39 +0,0 @@
import { IconButton, MobileMenu } from '@affine/component';
import { useJournalInfoHelper } from '@affine/core/components/hooks/use-journal';
import { EditorJournalPanel } from '@affine/core/desktop/pages/workspace/detail-page/tabs/journal';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
import { useLiveData, useService } from '@toeverything/infra';
export const JournalIconButton = ({
docId,
className,
}: {
docId: string;
className?: string;
}) => {
const { isJournal } = useJournalInfoHelper(docId);
const docDisplayMetaService = useService(DocDisplayMetaService);
const Icon = useLiveData(
docDisplayMetaService.icon$(docId, {
compareDate: new Date(),
})
);
if (!isJournal) {
return null;
}
return (
<MobileMenu
items={<EditorJournalPanel />}
contentOptions={{
align: 'center',
}}
>
<IconButton className={className} size={24}>
<Icon />
</IconButton>
</MobileMenu>
);
};

View File

@ -5,6 +5,7 @@ import { useRegisterBlocksuiteEditorCommands } from '@affine/core/components/hoo
import { useActiveBlocksuiteEditor } from '@affine/core/components/hooks/use-block-suite-editor'; import { useActiveBlocksuiteEditor } from '@affine/core/components/hooks/use-block-suite-editor';
import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-page-meta'; import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-page-meta';
import { usePageDocumentTitle } from '@affine/core/components/hooks/use-global-state'; import { usePageDocumentTitle } from '@affine/core/components/hooks/use-global-state';
import { useJournalRouteHelper } from '@affine/core/components/hooks/use-journal';
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper'; import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
import { PageDetailEditor } from '@affine/core/components/page-detail-editor'; import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
import { DetailPageWrapper } from '@affine/core/desktop/pages/workspace/detail-page/detail-page-wrapper'; import { DetailPageWrapper } from '@affine/core/desktop/pages/workspace/detail-page/detail-page-wrapper';
@ -42,7 +43,7 @@ import { useCallback, useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { AppTabs, PageHeader } from '../../../components'; import { AppTabs, PageHeader } from '../../../components';
import { JournalIconButton } from './journal-icon-button'; import { JournalDatePicker } from './journal-date-picker';
import * as styles from './mobile-detail-page.css'; import * as styles from './mobile-detail-page.css';
import { PageHeaderMenuButton } from './page-header-more-button'; import { PageHeaderMenuButton } from './page-header-more-button';
import { PageHeaderShareButton } from './page-header-share-button'; import { PageHeaderShareButton } from './page-header-share-button';
@ -192,12 +193,6 @@ const DetailPageImpl = () => {
> >
{/* Add a key to force rerender when page changed, to avoid error boundary persisting. */} {/* Add a key to force rerender when page changed, to avoid error boundary persisting. */}
<AffineErrorBoundary key={doc.id}> <AffineErrorBoundary key={doc.id}>
{mode === 'page' && (
<JournalIconButton
docId={doc.id}
className={styles.journalIconButton}
/>
)}
<PageDetailEditor onLoad={onLoad} /> <PageDetailEditor onLoad={onLoad} />
</AffineErrorBoundary> </AffineErrorBoundary>
</div> </div>
@ -227,6 +222,17 @@ const JournalDetailPage = ({
pageId: string; pageId: string;
date: string; date: string;
}) => { }) => {
const journalService = useService(JournalService);
const { openJournal } = useJournalRouteHelper();
const allJournalDates = useLiveData(journalService.allJournalDates$);
const handleDateChange = useCallback(
(date: string) => {
openJournal(date);
},
[openJournal]
);
return ( return (
<div className={styles.root}> <div className={styles.root}>
<DetailPageWrapper <DetailPageWrapper
@ -248,7 +254,11 @@ const JournalDetailPage = ({
{i18nTime(dayjs(date), { absolute: { accuracy: 'month' } })} {i18nTime(dayjs(date), { absolute: { accuracy: 'month' } })}
</span> </span>
</PageHeader> </PageHeader>
{/* TODO(@CatsJuice): <JournalDatePicker /> */} <JournalDatePicker
date={date}
onChange={handleDateChange}
withDotDates={allJournalDates}
/>
<DetailPageImpl /> <DetailPageImpl />
<AppTabs background={cssVarV2('layer/background/primary')} /> <AppTabs background={cssVarV2('layer/background/primary')} />
</DetailPageWrapper> </DetailPageWrapper>