Skip to content

Commit

Permalink
Feature/add new section (#88)
Browse files Browse the repository at this point in the history
* Store content of open project sections, display project sections in sidebar with contenteditable div

* Save section content in project file instead of separate md files to simplify application logic

* Add scrolling to sidebar sections, add war and peace test project

* Add content header instead of add section button

* Cleaning up sidebar content scroll and styling, add auto-focus and edit content on add new section
  • Loading branch information
zachhannum authored May 4, 2022
1 parent 7a93667 commit 4b78be0
Show file tree
Hide file tree
Showing 27 changed files with 546 additions and 165 deletions.
57 changes: 29 additions & 28 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
module.exports = {
extends: 'erb',
rules: {
// A temporary hack related to IDE not resolving correct package.json
'import/no-extraneous-dependencies': 'off',
'import/no-unresolved': 'error',
// Since React 17 and typescript 4.1 you can safely disable the rule
'react/react-in-jsx-scope': 'off',
'react/jsx-props-no-spreading': 'off',
},
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
project: './tsconfig.json',
tsconfigRootDir: __dirname,
createDefaultProgram: true,
},
settings: {
'import/resolver': {
// See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below
node: {},
webpack: {
config: require.resolve('./.erb/configs/webpack.config.eslint.ts'),
},
typescript: {},
extends: 'erb',
rules: {
// A temporary hack related to IDE not resolving correct package.json
'import/no-extraneous-dependencies': 'off',
'import/no-unresolved': 'error',
// Since React 17 and typescript 4.1 you can safely disable the rule
'react/react-in-jsx-scope': 'off',
'react/jsx-props-no-spreading': 'off',
"prettier/prettier": ["error", { endOfLine: "off" }],
},
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
project: './tsconfig.json',
tsconfigRootDir: __dirname,
createDefaultProgram: true,
},
},
};
settings: {
'import/resolver': {
// See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below
node: {},
webpack: {
config: require.resolve('./.erb/configs/webpack.config.eslint.ts'),
},
typescript: {},
},
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
},
};
2 changes: 1 addition & 1 deletion app/main/preload.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
import type { BookDetails, Project, ProjectData } from '../types/types';
import type { BookDetails, ProjectData } from '../types/types';

contextBridge.exposeInMainWorld('electron', {
ipcRenderer: {
Expand Down
4 changes: 3 additions & 1 deletion app/main/project/openProject.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { BrowserWindow } from 'electron';
import fs from 'fs';
import path from 'path';

const openProject = (mainWindow: BrowserWindow, projectPath: string) => {
fs.readFile(projectPath, (err, data) => {
Expand All @@ -8,7 +9,8 @@ const openProject = (mainWindow: BrowserWindow, projectPath: string) => {
} else {
mainWindow.webContents.send('openProject', {
projectContent: JSON.parse(data.toString()),
filePath: projectPath,
folderPath: path.dirname(projectPath),
fileName: path.basename(projectPath),
});
}
});
Expand Down
16 changes: 1 addition & 15 deletions app/main/project/projectTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,7 @@ const getProjectTemplate = (bookDetails: BookDetails): Project => {
ISBN: '',
language: '',
publisher: '',
frontMatter: [
{
path: '',
},
],
mainContent: [
{
path: '',
},
],
backMatter: [
{
path: '',
},
],
content: []
};
};

Expand Down
17 changes: 11 additions & 6 deletions app/main/project/saveProject.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import fs from 'fs';
import type { ProjectData } from '../../types/types';
import path from 'path';
import type { ProjectData } from 'types/types';

const saveProject = (projectData: ProjectData) => {
const { projectContent, filePath } = projectData;
fs.writeFile(filePath, JSON.stringify(projectContent), (err) => {
if (err) {
console.log(err);
const { projectContent, folderPath, fileName } = projectData;
fs.writeFile(
path.join(folderPath, fileName),
JSON.stringify(projectContent),
(err) => {
if (err) {
console.log(err);
}
}
});
);
};

export default saveProject;
21 changes: 3 additions & 18 deletions app/renderer/components/MoreOptionsSidebarMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import {
OpenBookIcon,
PreviewIcon,
UpdateIcon,
SaveIcon
} from '../icons';
import icon from '../../../assets/icon.png';
import useStore from '../store/useStore';
import SaveIcon from '../icons/SaveIcon';
import { saveProject } from '../utils/projectUtils';

const StyledPopupDiv = styled.div`
width: 180px;
Expand Down Expand Up @@ -95,23 +96,7 @@ const MoreOptionsSidebarMenu = () => {
label="Save Book"
onClick={() => {
menuRef.current?.close();
const projectContents = {
bookTitle: useStore.getState().bookTitle,
bookSubTitle: useStore.getState().bookSubTitle,
authorName: useStore.getState().authorName,
seriesName: useStore.getState().seriesName,
ISBN: useStore.getState().ISBN,
language: useStore.getState().language,
publisher: useStore.getState().publisher,
frontMatter: useStore.getState().frontMatter,
mainContent: useStore.getState().mainContent,
backMatter: useStore.getState().backMatter,
};
const savePath = useStore.getState().projectPath;
window.projectApi.saveProject({
projectContent: projectContents,
filePath: savePath,
});
saveProject();
}}
/>
<MoreOptionsSidebarItem
Expand Down
2 changes: 1 addition & 1 deletion app/renderer/components/NewBookModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useRef } from 'react';
import Modal from 'react-modal';
import styled, { useTheme, keyframes } from 'styled-components';
import styled, { useTheme } from 'styled-components';
import { TextField, Button, IconButton } from '../controls';
import { ModalExitIcon } from '../icons';

Expand Down
58 changes: 31 additions & 27 deletions app/renderer/components/ScrollContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import React from 'react';
import styled, { css } from 'styled-components';

type ScrollerProps = {
win32: boolean;
};

const Scroller = styled.div<ScrollerProps>`
const Scroller = styled.div`
overflow-y: overlay;
height: 100%;
width: calc(100% - 125px);
Expand All @@ -17,27 +14,35 @@ const Scroller = styled.div<ScrollerProps>`
padding-right: 50px;
padding-left: 75px;
${(p) =>
p.win32 &&
css`
background-color: rgba(0, 0, 0, 0);
-webkit-background-clip: text;
transition: background-color 0.8s;
&:hover {
background-color: rgba(0, 0, 0, 0.15);
}
::-webkit-scrollbar {
width: 12px;
height: 8px;
}
::-webkit-scrollbar-track {
display: none;
}
::-webkit-scrollbar-thumb {
background-color: inherit;
}
`}
mask-image: linear-gradient(to top, transparent, black),
linear-gradient(to left, transparent 17px, black 17px);
mask-size: 100% 20000px;
mask-position: left bottom;
-webkit-mask-image: linear-gradient(to top, transparent, black),
linear-gradient(to left, transparent 17px, black 17px);
-webkit-mask-size: 100% 20000px;
-webkit-mask-position: left bottom;
transition: mask-position 0.3s, -webkit-mask-position 0.3s;
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
/* display: none; */
}
::-webkit-scrollbar-thumb {
background-color: inherit;
background-color: rgba(0, 0, 0, 0.1);
}
&:hover {
-webkit-mask-position: left top;
}
${window.windowApi.os() === 'darwin' &&
css`
::-webkit-scrollbar-thumb {
border-radius: 4px;
}
`}
`;

const Padding = styled.div`
Expand All @@ -50,10 +55,9 @@ type Props = {
children: React.ReactNode;
};

const platform = window.windowApi.os();

const ScrollContainer = ({ children }: Props) => (
<Scroller win32={platform === 'win32'}>
<Scroller>
<Padding>{children}</Padding>
</Scroller>
);
Expand Down
15 changes: 5 additions & 10 deletions app/renderer/components/SidebarProjectContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import styled from 'styled-components';
import Color from 'color';
import { Button } from '../controls';
import useStore from '../store/useStore';
import SidebarProjectSections from './SidebarProjectSections';

const StyledSidebarProjectContent = styled.div`
display: flex;
user-select: none;
flex-direction: column;
padding: 30px;
padding: 20px;
box-sizing: border-box;
gap: 20px;
flex-grow: 1;
`;

const StyledTitleBlock = styled.div`
Expand All @@ -22,6 +24,7 @@ const StyledContentBlock = styled.div`
display: flex;
flex-direction: column;
gap: 5px;
flex-grow: 1;
`;

const StyledTitle = styled.div`
Expand Down Expand Up @@ -49,11 +52,6 @@ const StyledAnchor = styled.a`
}
`;

const StyledSidebarPSecondary = styled.p`
color: ${(p) => p.theme.sidebarFgTextSecondary};
font-size: 0.9em;
`;

const SidebarProjectContent = () => {
const isProjectOpen = useStore((state) => state.isProjectOpen);
const bookTitle = useStore((state) => state.bookTitle);
Expand All @@ -68,10 +66,7 @@ const SidebarProjectContent = () => {
<StyledAuthorName>{authorName}</StyledAuthorName>
</StyledTitleBlock>
<StyledContentBlock>
<StyledSidebarPSecondary>
You don&rsquo;t have any content yet.
</StyledSidebarPSecondary>
<Button label="Add a Section" onClick={() => {}} />
<SidebarProjectSections />
</StyledContentBlock>
</>
) : (
Expand Down
87 changes: 87 additions & 0 deletions app/renderer/components/SidebarProjectSectionItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { useState, useRef, useEffect } from 'react';
import styled, { css } from 'styled-components';
import Color from 'color';
import { updateSectionName } from '../utils/projectUtils';

const StyledItem = styled.div`
cursor: ${(p) => (p.contentEditable ? 'text' : 'pointer')};
color: ${(p) => p.theme.sidebarFgText};
font-size: 0.9em;
width: 95%;
box-sizing: border-box;
padding: 3px 6px;
border-radius: 5px;
&:hover {
background-color: ${(p) =>
p.contentEditable
? Color(p.theme.sidebarBg).darken(0.2)
: Color(p.theme.sidebarBg).lighten(0.3)};
}
${(p) =>
p.contentEditable &&
css`
background-color: ${(p) => Color(p.theme.sidebarBg).darken(0.2)};
:focus {
outline: none;
}
`}
transition: background-color 100ms ease-in-out;
`;

type SidebarProjectSectionItemProps = {
value: string;
addingNew: boolean;
};

const SidebarProjectSectionItem = ({
value,
addingNew,
}: SidebarProjectSectionItemProps) => {
const [isEditable, setIsEditable] = useState(false);
const itemRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (addingNew) {
setIsEditable(true);
itemRef.current?.scrollTo({ behavior: 'smooth' });
setTimeout(() => {
itemRef.current?.focus();
});
}
}, []);
const handleDoubleClick = () => {
setIsEditable(true);
setTimeout(() => {
itemRef.current?.focus();
}, 10);
};
const handleBlur = () => {
setIsEditable(false);

const newValue = itemRef.current?.innerText;
if (newValue) updateSectionName(value, newValue);
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.code === 'Enter') {
event.preventDefault();
handleBlur();
} else if (event.code === 'Escape') {
event.preventDefault();
if (itemRef.current) itemRef.current.innerText = value;
setIsEditable(false);
}
};
return (
<StyledItem
ref={itemRef}
contentEditable={isEditable}
suppressContentEditableWarning
onDoubleClick={handleDoubleClick}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
>
{value}
</StyledItem>
);
};

export default SidebarProjectSectionItem;
Loading

0 comments on commit 4b78be0

Please sign in to comment.