mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 06:59:31 +01:00
feat: add header in notebook view
This commit is contained in:
@@ -123,6 +123,8 @@ import {
|
|||||||
mdiFirefox,
|
mdiFirefox,
|
||||||
mdiAppleSafari,
|
mdiAppleSafari,
|
||||||
mdiBugOutline,
|
mdiBugOutline,
|
||||||
|
mdiLinkVariant,
|
||||||
|
mdiLinkVariantOff,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import { useTheme } from "emotion-theming";
|
import { useTheme } from "emotion-theming";
|
||||||
import { AnimatedFlex } from "../animated";
|
import { AnimatedFlex } from "../animated";
|
||||||
@@ -149,6 +151,7 @@ function createIcon(name, rotate = false) {
|
|||||||
const [isHovering, setIsHovering] = useState();
|
const [isHovering, setIsHovering] = useState();
|
||||||
return (
|
return (
|
||||||
<AnimatedFlex
|
<AnimatedFlex
|
||||||
|
flexShrink={0}
|
||||||
id={props.id}
|
id={props.id}
|
||||||
title={props.title}
|
title={props.title}
|
||||||
variant={props.variant}
|
variant={props.variant}
|
||||||
@@ -312,3 +315,5 @@ export const Anonymous = createIcon(mdiIncognito);
|
|||||||
export const CloudLock = createIcon(mdiCloudLockOutline);
|
export const CloudLock = createIcon(mdiCloudLockOutline);
|
||||||
export const Timebomb = createIcon(mdiBomb);
|
export const Timebomb = createIcon(mdiBomb);
|
||||||
export const Issue = createIcon(mdiBugOutline);
|
export const Issue = createIcon(mdiBugOutline);
|
||||||
|
export const ShortcutLink = createIcon(mdiLinkVariant);
|
||||||
|
export const RemoveShortcutLink = createIcon(mdiLinkVariantOff);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import Announcements from "../announcements";
|
|||||||
import useAnnouncements from "../../utils/use-announcements";
|
import useAnnouncements from "../../utils/use-announcements";
|
||||||
|
|
||||||
function ListContainer(props) {
|
function ListContainer(props) {
|
||||||
const { type, groupType, items, context, refresh } = props;
|
const { type, groupType, items, context, refresh, header } = props;
|
||||||
const [announcements, removeAnnouncement] = useAnnouncements();
|
const [announcements, removeAnnouncement] = useAnnouncements();
|
||||||
const profile = useMemo(() => ListProfiles[type], [type]);
|
const profile = useMemo(() => ListProfiles[type], [type]);
|
||||||
const shouldSelectAll = useSelectionStore((store) => store.shouldSelectAll);
|
const shouldSelectAll = useSelectionStore((store) => store.shouldSelectAll);
|
||||||
@@ -27,9 +27,12 @@ function ListContainer(props) {
|
|||||||
return (
|
return (
|
||||||
<Flex variant="columnFill">
|
<Flex variant="columnFill">
|
||||||
{!props.items.length && props.placeholder ? (
|
{!props.items.length && props.placeholder ? (
|
||||||
<Flex variant="columnCenterFill">
|
<>
|
||||||
{props.isLoading ? <Icon.Loading rotate /> : <props.placeholder />}
|
{header}
|
||||||
</Flex>
|
<Flex variant="columnCenterFill">
|
||||||
|
{props.isLoading ? <Icon.Loading rotate /> : <props.placeholder />}
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Flex variant="columnFill" data-test-id="note-list">
|
<Flex variant="columnFill" data-test-id="note-list">
|
||||||
@@ -43,7 +46,9 @@ function ListContainer(props) {
|
|||||||
components={{
|
components={{
|
||||||
Scroller: CustomScrollbarsVirtualList,
|
Scroller: CustomScrollbarsVirtualList,
|
||||||
Header: () =>
|
Header: () =>
|
||||||
announcements.length ? (
|
header ? (
|
||||||
|
header
|
||||||
|
) : announcements.length ? (
|
||||||
<Announcements
|
<Announcements
|
||||||
announcements={announcements}
|
announcements={announcements}
|
||||||
removeAnnouncement={removeAnnouncement}
|
removeAnnouncement={removeAnnouncement}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ function Header(props) {
|
|||||||
(store) => store.toggleSelectionMode
|
(store) => store.toggleSelectionMode
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!title && !subtitle) return null;
|
// if (!subtitle) return null;
|
||||||
return (
|
return (
|
||||||
<Flex mx={2} flexDirection="column" justifyContent="center">
|
<Flex mx={2} flexDirection="column" justifyContent="center">
|
||||||
<Flex alignItems="center" justifyContent="space-between">
|
<Flex alignItems="center" justifyContent="space-between">
|
||||||
@@ -75,11 +75,14 @@ function Header(props) {
|
|||||||
size={30}
|
size={30}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<RouteTitle
|
{title && (
|
||||||
title={title}
|
<RouteTitle
|
||||||
isEditable={isEditable}
|
subtitle={subtitle}
|
||||||
onChange={onChange}
|
title={title}
|
||||||
/>
|
isEditable={isEditable}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
<SelectionOptions options={SELECTION_OPTIONS_MAP[type]} />
|
<SelectionOptions options={SELECTION_OPTIONS_MAP[type]} />
|
||||||
{!isSelectionMode && (
|
{!isSelectionMode && (
|
||||||
@@ -111,18 +114,6 @@ function Header(props) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
{subtitle && (
|
|
||||||
<Text
|
|
||||||
variant="title"
|
|
||||||
color="primary"
|
|
||||||
sx={{
|
|
||||||
marginBottom: 2,
|
|
||||||
cursor: "normal",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{subtitle}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{isSelectionMode && (
|
{isSelectionMode && (
|
||||||
<Flex
|
<Flex
|
||||||
mb={2}
|
mb={2}
|
||||||
@@ -165,7 +156,7 @@ function SelectionOptions(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RouteTitle({ title, isEditable, onChange }) {
|
function RouteTitle({ title, subtitle, isEditable, onChange }) {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -173,45 +164,48 @@ function RouteTitle({ title, isEditable, onChange }) {
|
|||||||
}, [title]);
|
}, [title]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Flex flexDirection="column">
|
||||||
ref={ref}
|
{subtitle && <Text variant="subBody">{subtitle}</Text>}
|
||||||
variant="heading"
|
<Input
|
||||||
data-test-id="routeHeader"
|
ref={ref}
|
||||||
color={"text"}
|
variant="clean"
|
||||||
title={title}
|
data-test-id="routeHeader"
|
||||||
sx={{
|
color={"text"}
|
||||||
overflow: "hidden",
|
title={title}
|
||||||
textOverflow: isEditing ? "initial" : "ellipsis",
|
sx={{
|
||||||
whiteSpace: "nowrap",
|
overflow: "hidden",
|
||||||
|
textOverflow: isEditing ? "initial" : "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
|
||||||
p: 0,
|
p: 0,
|
||||||
m: 0,
|
m: 0,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
fontFamily: "heading",
|
fontFamily: "heading",
|
||||||
fontSize: "heading",
|
fontSize: subtitle ? "subheading" : "heading",
|
||||||
border: "none",
|
border: "none",
|
||||||
bg: isEditing ? "bgSecondary" : "transparent",
|
bg: isEditing ? "bgSecondary" : "transparent",
|
||||||
|
|
||||||
":focus-visible": { outline: "none" },
|
":focus-visible": { outline: "none" },
|
||||||
}}
|
}}
|
||||||
onDoubleClick={(e) => {
|
onDoubleClick={(e) => {
|
||||||
setIsEditing(isEditable && true);
|
setIsEditing(isEditable && true);
|
||||||
e.target.focus();
|
e.target.focus();
|
||||||
}}
|
}}
|
||||||
onKeyUp={(e) => {
|
onKeyUp={(e) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
e.target.value = title;
|
e.target.value = title;
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
} else if (e.key === "Enter") {
|
} else if (e.key === "Enter") {
|
||||||
|
if (onChange) onChange(e.target.value);
|
||||||
|
setIsEditing(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
if (onChange) onChange(e.target.value);
|
if (onChange) onChange(e.target.value);
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
}
|
}}
|
||||||
}}
|
readOnly={!isEditing}
|
||||||
onBlur={(e) => {
|
/>
|
||||||
if (onChange) onChange(e.target.value);
|
</Flex>
|
||||||
setIsEditing(false);
|
|
||||||
}}
|
|
||||||
readOnly={!isEditing}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,12 +44,6 @@ const routes = {
|
|||||||
return {
|
return {
|
||||||
key: "topics",
|
key: "topics",
|
||||||
type: "topics",
|
type: "topics",
|
||||||
title: notebook.title,
|
|
||||||
isEditable: true,
|
|
||||||
onChange: (title) => {
|
|
||||||
db.notebooks.add({ id: notebookId, title });
|
|
||||||
showToast("success", "Notebook title updated!");
|
|
||||||
},
|
|
||||||
component: <Topics />,
|
component: <Topics />,
|
||||||
buttons: {
|
buttons: {
|
||||||
back: {
|
back: {
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import ListContainer from "../components/list-container";
|
import ListContainer from "../components/list-container";
|
||||||
import { useStore as useNbStore } from "../stores/notebook-store";
|
import { useStore as useNbStore } from "../stores/notebook-store";
|
||||||
|
import { useStore as useAppStore } from "../stores/app-store";
|
||||||
import { hashNavigate } from "../navigation";
|
import { hashNavigate } from "../navigation";
|
||||||
import TopicsPlaceholder from "../components/placeholders/topics-placeholder";
|
import TopicsPlaceholder from "../components/placeholders/topics-placeholder";
|
||||||
|
import { Button, Flex, Text } from "rebass";
|
||||||
|
import { Edit, RemoveShortcutLink, ShortcutLink } from "../components/icons";
|
||||||
|
import { getTotalNotes } from "../common";
|
||||||
|
import { formatDate } from "notes-core/utils/date";
|
||||||
|
import { db } from "../common/db";
|
||||||
|
|
||||||
function Topics() {
|
function Topics() {
|
||||||
const selectedNotebookTopics = useNbStore(
|
const selectedNotebookTopics = useNbStore(
|
||||||
@@ -20,6 +26,11 @@ function Topics() {
|
|||||||
items={selectedNotebookTopics}
|
items={selectedNotebookTopics}
|
||||||
context={{ notebookId: selectedNotebookId }}
|
context={{ notebookId: selectedNotebookId }}
|
||||||
placeholder={TopicsPlaceholder}
|
placeholder={TopicsPlaceholder}
|
||||||
|
header={
|
||||||
|
<NotebookHeader
|
||||||
|
notebook={db.notebooks.notebook(selectedNotebookId).data}
|
||||||
|
/>
|
||||||
|
}
|
||||||
button={{
|
button={{
|
||||||
content: "Add a new topic",
|
content: "Add a new topic",
|
||||||
onClick: () => hashNavigate(`/topics/create`),
|
onClick: () => hashNavigate(`/topics/create`),
|
||||||
@@ -29,3 +40,60 @@ function Topics() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default Topics;
|
export default Topics;
|
||||||
|
|
||||||
|
function NotebookHeader({ notebook }) {
|
||||||
|
const { title, description, topics, dateEdited } = notebook;
|
||||||
|
const [isShortcut, setIsShortcut] = useState(false);
|
||||||
|
const menuPins = useAppStore((store) => store.menuPins);
|
||||||
|
const pinItemToMenu = useAppStore((store) => store.pinItemToMenu);
|
||||||
|
useEffect(() => {
|
||||||
|
setIsShortcut(menuPins.findIndex((p) => p.id === notebook.id) > -1);
|
||||||
|
}, [menuPins, notebook]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDirection="column" mx={2} my={2}>
|
||||||
|
<Text variant="subBody">{formatDate(dateEdited)}</Text>
|
||||||
|
<Flex justifyContent="space-between" alignItems="center">
|
||||||
|
<Text variant="heading">{title}</Text>
|
||||||
|
<Flex>
|
||||||
|
<Button
|
||||||
|
variant="tool"
|
||||||
|
sx={{ borderRadius: 100 }}
|
||||||
|
mr={1}
|
||||||
|
p={0}
|
||||||
|
width={30}
|
||||||
|
height={30}
|
||||||
|
title={isShortcut ? "Remove shortcut" : "Create shortcut"}
|
||||||
|
onClick={() => pinItemToMenu(notebook)}
|
||||||
|
>
|
||||||
|
{isShortcut ? (
|
||||||
|
<RemoveShortcutLink size={16} />
|
||||||
|
) : (
|
||||||
|
<ShortcutLink size={16} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="tool"
|
||||||
|
sx={{ borderRadius: 100 }}
|
||||||
|
p={0}
|
||||||
|
width={30}
|
||||||
|
height={30}
|
||||||
|
title="Edit notebook"
|
||||||
|
onClick={() => hashNavigate(`/notebooks/${notebook.id}/edit`)}
|
||||||
|
>
|
||||||
|
<Edit size={16} />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{description && (
|
||||||
|
<Text variant="body" fontSize="subtitle">
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text as="em" variant="subBody" mt={2}>
|
||||||
|
{topics.length} topic, {getTotalNotes(notebook)} notes
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user