Skip to content

Commit

Permalink
application-level banner
Browse files Browse the repository at this point in the history
  • Loading branch information
suchanlee committed Jan 22, 2020
1 parent 94427f5 commit 4554b0a
Show file tree
Hide file tree
Showing 18 changed files with 221 additions and 13 deletions.
9 changes: 9 additions & 0 deletions src/renderer/actions/bannerActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { TypedAction } from "redoodle";

export namespace BannerActions {
export const set = TypedAction.define("banner::set")<string | undefined>();
}

export namespace BannerInternalActions {
export const set = TypedAction.define("banner-internal::set")<string | undefined>();
}
2 changes: 2 additions & 0 deletions src/renderer/components/Application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { Classes } from "@blueprintjs/core";
import classNames from "classnames";
import * as React from "react";
import { hot } from "react-hot-loader/root";
import { Banner } from "./banner/Banner";
import { Content } from "./content/Content";
import { TopMenu } from "./top-menu/TopMenu";

require("./Application.scss");

export const Application = hot(() => (
<div className={classNames(Classes.DARK, "application")}>
<Banner />
<TopMenu />
<Content />
</div>
Expand Down
7 changes: 7 additions & 0 deletions src/renderer/components/banner/Banner.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.banner-info-banner {
// can't text-align:center :(
// https://github.com/palantir/blueprint/issues/2021#issuecomment-393744854
text-align: left;
display: flex;
justify-content: center;
}
75 changes: 75 additions & 0 deletions src/renderer/components/banner/Banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { EditableText } from "@blueprintjs/core";
import React, { useCallback, useState } from "react";
import { connect } from "react-redux";
import { BannerActions } from "../../actions/bannerActions";
import { selectBannerValue } from "../../selectors/bannerSelectors";
import { RootState } from "../../states/rootState";
import { InfoBanner } from "../../views/notes/components/InfoBanner";
import "./Banner.scss";

export namespace Banner {
export interface StoreProps {
value: string | undefined;
}

export interface DispatchProps {
setBanner: typeof BannerActions.set;
}

export type Props = StoreProps & DispatchProps;

export interface State {
inputValue: string;
}
}

const BannerInternal = React.memo((props: Banner.Props) => {
if (props.value == null) {
return null;
}

const [inputValue, setInputValue] = useState<string>(props.value);

const handleChange = useCallback((value: string) => {
setInputValue(value);
}, []);

const handleConfirm = useCallback((value: string) => {
const trimmedValue = value.trim();
if (trimmedValue.length === 0) {
props.setBanner(undefined);
} else {
props.setBanner(trimmedValue);
}
}, []);

const swallowKeyDown = useCallback((evt: React.KeyboardEvent) => {
if (evt.key === "Enter") {
evt.stopPropagation();
evt.nativeEvent.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
evt.preventDefault();
console.log(evt.key);
}
}, []);

const input = (
<div onKeyDown={swallowKeyDown}>
<EditableText value={inputValue} onChange={handleChange} onConfirm={handleConfirm} />
</div>
);
return <InfoBanner className="banner-info-banner" value={input} />;
});

function mapStateToProps(state: RootState): Banner.StoreProps {
return {
value: selectBannerValue(state)
};
}

const mapDispatchToProps: Banner.DispatchProps = {
setBanner: BannerActions.set
};

const enhance = connect(mapStateToProps, mapDispatchToProps);
export const Banner = enhance(BannerInternal);
47 changes: 47 additions & 0 deletions src/renderer/components/top-menu/BannerMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import classNames from "classnames";
import React, { useCallback } from "react";
import { connect } from "react-redux";
import { BannerActions } from "../../actions/bannerActions";
import { selectBannerHasValue } from "../../selectors/bannerSelectors";
import { RootState } from "../../states/rootState";

export namespace BannerMenuItem {
export interface StoreProps {
hasValue: boolean;
}

export interface DispatchProps {
setBanner: typeof BannerActions.set;
}

export type Props = StoreProps & DispatchProps;
}

const BannerMenuItemInternal = React.memo((props: BannerMenuItem.Props) => {
if (props.hasValue) {
return null;
}

const handleClick = useCallback(() => {
props.setBanner("<CLICK AND CHANGE ME // EMPTY BANNER REMOVES IT>");
}, []);

return (
<span className={classNames("top-menu-item", "banner-menu-item")} onClick={handleClick}>
SET BANNER
</span>
);
});

function mapStateToProps(state: RootState): BannerMenuItem.StoreProps {
return {
hasValue: selectBannerHasValue(state)
};
}

const mapDispatchToProps: BannerMenuItem.DispatchProps = {
setBanner: BannerActions.set
};

const enhance = connect(mapStateToProps, mapDispatchToProps);
export const BannerMenuItem = enhance(BannerMenuItemInternal);
2 changes: 2 additions & 0 deletions src/renderer/components/top-menu/TopMenu.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
@import "~@blueprintjs/core/lib/scss/variables";

.top-menu {
display: flex;
justify-content: space-between;
padding: 10px 15px;

.top-menu-item {
Expand Down
8 changes: 6 additions & 2 deletions src/renderer/components/top-menu/TopMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { NavigationActions } from "../../actions/navigationActions";
import { selectNavigationActiveView } from "../../selectors/navigationSelectors";
import { RootState } from "../../states/rootState";
import { Views } from "../../views/view";
import { BannerMenuItem } from "./BannerMenuItem";

require("./TopMenu.scss");
import "./TopMenu.scss";

export namespace TopMenu {
export interface StoreProps {
Expand All @@ -25,7 +26,10 @@ class TopMenuInternal extends React.PureComponent<TopMenu.Props> {
public render() {
return (
<div className={classNames("top-menu", Classes.ELEVATION_0)}>
{Views.map(view => this.renderMenuItem(view.name))}
<span className="top-menu-views">{Views.map(view => this.renderMenuItem(view.name))}</span>
<span className="top-menu-others">
<BannerMenuItem />
</span>
</div>
);
}
Expand Down
9 changes: 9 additions & 0 deletions src/renderer/reducers/bannerReducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { setWith, TypedReducer } from "redoodle";
import { BannerInternalActions } from "../actions/bannerActions";
import { BannerState } from "../states/bannerState";

export const bannerReducer = TypedReducer.builder<BannerState>()
.withHandler(BannerInternalActions.set.TYPE, (state, value) => {
return setWith(state, { value });
})
.build();
4 changes: 3 additions & 1 deletion src/renderer/reducers/rootReducer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { combineReducers } from "redoodle";
import { Reducer } from "redux";
import { View } from "../types/viewTypes";
import { bannerReducer } from "./bannerReducer";
import { floatingMenuReducer } from "./floatingMenuReducer";
import { keyNavListReducer } from "./keyNavListReducer";
import { navigationReducer } from "./navigationReducer";
Expand All @@ -9,7 +10,8 @@ export function createRootReducer(views: readonly View<any, any>[]) {
const reducersByKey: Record<string, Reducer> = {
floatingMenu: floatingMenuReducer,
navigation: navigationReducer,
keyNavList: keyNavListReducer
keyNavList: keyNavListReducer,
banner: bannerReducer
};

for (const view of views) {
Expand Down
34 changes: 34 additions & 0 deletions src/renderer/sagas/bannerSaga.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ipcRenderer } from "electron-better-ipc";
import { TypedAction } from "redoodle";
import { all, put, takeEvery } from "redux-saga/effects";
import { IpcEvent } from "../../shared/ipcEvent";
import { BannerActions, BannerInternalActions } from "../actions/bannerActions";

const BANNER_FILE_NAME = "banner";

export function* bannerSaga() {
yield initializeBanner();
yield all([yield takeEvery(BannerActions.set.TYPE, setBanner)]);
}

function* initializeBanner() {
const banner: string | undefined = yield ipcRenderer.callMain(
IpcEvent.READ_DATA,
BANNER_FILE_NAME
);

yield put(BannerInternalActions.set(banner));
}

function* setBanner(action: TypedAction<string | undefined>) {
const banner = action.payload;
yield put(BannerInternalActions.set(banner));
writeBanner(banner);
}

function writeBanner(banner: string | undefined) {
ipcRenderer.callMain(IpcEvent.WRITE_DATA, {
fileName: BANNER_FILE_NAME,
data: banner
});
}
5 changes: 3 additions & 2 deletions src/renderer/sagas/rootSaga.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { all, Effect } from "redux-saga/effects";
import { all, Effect, spawn } from "redux-saga/effects";
import { View } from "../types/viewTypes";
import { bannerSaga } from "./bannerSaga";

export function createRootSaga(views: readonly View<any, any>[]) {
return function*() {
const effects: Effect[] = [];
const effects: Effect[] = [spawn(bannerSaga)];
for (const view of views) {
if (view.redux?.saga != null) {
effects.push(view.redux.saga);
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/selectors/bannerSelectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { RootState } from "../states/rootState";

export const selectBannerValue = (state: RootState) => state.banner.value;
export const selectBannerHasValue = (state: RootState) => state.banner.value != null;
9 changes: 9 additions & 0 deletions src/renderer/states/bannerState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface BannerState {
value: string | undefined;
}

export function createInitialBannerState(): BannerState {
return {
value: undefined
};
}
5 changes: 4 additions & 1 deletion src/renderer/states/rootState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { View } from "../types/viewTypes";
import { WithNotesState } from "../views/notes/redux/notesState";
import { WithReadingsState } from "../views/readings/redux/readingsState";
import { WithTodosState } from "../views/todos/redux/todosState";
import { BannerState, createInitialBannerState } from "./bannerState";
import {
createInitiailFloatingMenuState as createInitialFloatingMenuState,
FloatingMenuState
Expand All @@ -13,6 +14,7 @@ export type RootState = {
navigation: NavigationState;
floatingMenu: FloatingMenuState;
keyNavList: KeyNavListState;
banner: BannerState;
} & WithNotesState &
WithReadingsState &
WithTodosState;
Expand All @@ -21,7 +23,8 @@ export function createInitialRootState(views: readonly View<any, any>[]): RootSt
const rootState: Record<string, any> = {
navigation: createInitialNavigationState(),
floatingMenu: createInitialFloatingMenuState(),
keyNavList: createInitialKeyNavListState()
keyNavList: createInitialKeyNavListState(),
banner: createInitialBannerState()
};

for (const view of views) {
Expand Down
8 changes: 4 additions & 4 deletions src/renderer/views/notes/components/InfoBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ export namespace InfoBanner {
export interface Props {
className?: string;
intent?: Intent;
text: string;
value: string | JSX.Element;
}
}

export function InfoBanner(props: InfoBanner.Props) {
export const InfoBanner = React.memo((props: InfoBanner.Props) => {
return (
<div
className={classNames("info-banner", props.className, {
Expand All @@ -21,7 +21,7 @@ export function InfoBanner(props: InfoBanner.Props) {
"-warning": props.intent === Intent.WARNING
})}
>
{props.text}
{props.value}
</div>
);
}
});
2 changes: 1 addition & 1 deletion src/renderer/views/notes/components/NotePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class NotePanelInternal extends React.PureComponent<NotePanel.Props> {
onClose={this.handleClose}
title={note != null ? getNoteTitle(note) : ""}
>
<InfoBanner text="⌘+N FOR NEW, ⌘+W TO CLOSE TAB, ⌘+⌥+←→ TO NAV" />
<InfoBanner value="⌘+N FOR NEW, ⌘+W TO CLOSE TAB, ⌘+⌥+←→ TO NAV" />
{note != null && (
<React.Fragment>
<NoteTabs
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/views/readings/components/ReadingBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function ReadingBannerInternal(props: ReadingBanner.Props) {
? "ENTER TO OPEN IN PANEL / ⌘+ENTER TO OPEN IN BROWSER"
: "PASTE URL AND PRESS ENTER TO ADD";

return <InfoBanner intent={props.isSelectingReading ? Intent.PRIMARY : undefined} text={text} />;
return <InfoBanner intent={props.isSelectingReading ? Intent.PRIMARY : undefined} value={text} />;
}

function mapStoreProps(
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/views/todos/components/TodosPanelBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export namespace TodosPanelBanner {

class TodosPanelBannerInternal extends React.PureComponent<TodosPanelBanner.Props> {
public render() {
return <InfoBanner intent={this.getIntent()} text={this.getText()} />;
return <InfoBanner intent={this.getIntent()} value={this.getText()} />;
}

private getIntent() {
Expand Down

0 comments on commit 4554b0a

Please sign in to comment.