Skip to content

Commit

Permalink
Merge pull request #20 from ahonn/feature/theme-settings
Browse files Browse the repository at this point in the history
feat: add plugin settings to customize theme colors, #18
  • Loading branch information
ahonn committed Aug 6, 2022
2 parents e3be9cb + 3f0d862 commit 1c7448c
Show file tree
Hide file tree
Showing 13 changed files with 256 additions and 42 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"root": true,
"extends": [
"eslint:recommended",
"plugin:react/recommended",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"conventional-changelog-conventionalcommits": "4.6.3",
"eslint": "8.13.0",
"eslint-plugin-react": "7.29.4",
"eslint-plugin-react-hooks": "^4.6.0",
"node-fetch": "3.2.3",
"semantic-release": "19.0.2",
"typescript": "4.6.3",
Expand Down
20 changes: 19 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 12 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'virtual:windi.css';
import React, { useCallback, useEffect, useRef } from 'react';
import React, { useEffect, useRef } from 'react';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
import dayjs from 'dayjs';
import advancedFormat from 'dayjs/plugin/advancedFormat';
Expand All @@ -9,13 +9,14 @@ import TaskSection from './components/TaskSection';
import { logseq as plugin } from '../package.json';
import useAppState, { withAppState } from './hooks/useAppState';
import './style.css';
import useThemeStyle from './hooks/useThemeStyle';

dayjs.extend(advancedFormat);

function ErrorFallback({ error }: FallbackProps) {
useEffect(() => {
window.logseq.App.showMsg(`[${plugin.id}]: ${error.message}`, 'error');
}, []);
}, [error.message]);

return (
<div role="alert" className="text-red-500 font-semibold">
Expand All @@ -30,6 +31,7 @@ function App() {
const inputRef = useRef<ITaskInputRef>(null);
const visible = useAppVisible();
const { userConfigs, refresh, tasks } = useAppState();
const themeStyle = useThemeStyle();

useEffect(() => {
if (visible) {
Expand All @@ -46,7 +48,7 @@ function App() {
document.removeEventListener('keydown', keydownHandler);
};
}
}, [visible]);
}, [refresh, visible]);

const handleClickOutside = (e: React.MouseEvent) => {
if (!innerRef.current?.contains(e.target as any)) {
Expand Down Expand Up @@ -78,7 +80,13 @@ function App() {
onClick={handleClickOutside}
>
<div ref={innerRef} id={plugin.id}>
<div className="absolute p-4 w-90 h-120 -left-13rem bg-white shadow rounded-lg overflow-y-auto border border-gray-100">
<div
className="absolute p-4 w-90 h-120 -left-13rem bg-white shadow rounded-lg overflow-y-auto border-2"
style={{
backgroundColor: themeStyle.primaryBackgroundColor,
borderColor: themeStyle.secondaryBackgroundColor,
}}
>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<TaskInput ref={inputRef} onCreateTask={createNewTask} />
<div>
Expand Down
18 changes: 14 additions & 4 deletions src/components/TaskInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useImperativeHandle, useRef } from 'react';
import { CirclePlus } from 'tabler-icons-react';
import useThemeStyle from '../hooks/useThemeStyle';

export interface ITaskInputRef {
focus: () => void;
Expand All @@ -9,9 +10,13 @@ export interface ITaskInputProps {
onCreateTask(content: string): void;
}

const TaskInput: React.ForwardRefRenderFunction<ITaskInputRef, ITaskInputProps> = (props, ref) => {
const TaskInput: React.ForwardRefRenderFunction<
ITaskInputRef,
ITaskInputProps
> = (props, ref) => {
const [content, setContent] = React.useState('');
const inputRef = useRef<HTMLInputElement>(null);
const themeStyle = useThemeStyle();

const focus = () => {
if (inputRef.current) {
Expand All @@ -25,12 +30,17 @@ const TaskInput: React.ForwardRefRenderFunction<ITaskInputRef, ITaskInputProps>

return (
<div className="flex mb-2">
<div className="px-2 h-9 flex items-center flex-1 bg-gray-100 inline rounded-lg">
<CirclePlus size={20} className="stroke-gray-400" />
<div
className="px-2 h-9 flex items-center flex-1 inline rounded-lg"
style={{
backgroundColor: themeStyle.secondaryBackgroundColor,
}}
>
<CirclePlus size={20} className="stroke-gray-400 dark:stroke-gray-200" />
<input
type="text"
ref={inputRef}
className="flex-1 bg-transparent p-1 outline-none text-sm"
className="flex-1 bg-transparent p-1 outline-none text-sm dark:text-gray-100"
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Type a task and hit enter"
Expand Down
33 changes: 23 additions & 10 deletions src/components/TaskItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@ import useTaskManager from '../hooks/useTaskManager';
import { TaskEntityObject, TaskMarker } from '../models/TaskEntity';
import useAppState from '../hooks/useAppState';
import 'rc-checkbox/assets/index.css';
import useThemeStyle from '../hooks/useThemeStyle';

export interface ITaskItemProps {
item: TaskEntityObject;
}

const TaskItem: React.FC<ITaskItemProps> = (props) => {
const { userConfigs: { preferredDateFormat, preferredWorkflow } } = useAppState();
const {
userConfigs: { preferredDateFormat, preferredWorkflow },
} = useAppState();
const task = useTaskManager(props.item);
const [checked, setChecked] = React.useState(task.completed);
const themeStyle = useThemeStyle();

const isExpiredTask = useMemo(() => {
if (!task.scheduled) {
Expand All @@ -37,36 +41,45 @@ const TaskItem: React.FC<ITaskItemProps> = (props) => {

const toggleMarker = () => {
if (preferredWorkflow === 'now') {
task.setMarker(task.marker === TaskMarker.NOW ? TaskMarker.LATER : TaskMarker.NOW);
task.setMarker(
task.marker === TaskMarker.NOW ? TaskMarker.LATER : TaskMarker.NOW,
);
return;
}
task.setMarker(task.marker === TaskMarker.TODO ? TaskMarker.DOING : TaskMarker.TODO);
}
task.setMarker(
task.marker === TaskMarker.TODO ? TaskMarker.DOING : TaskMarker.TODO,
);
};

const contentClassName = classnames('mb-1 line-clamp-3 cursor-pointer', {
'line-through': checked,
'text-gray-400': checked,
});

return (
<div key={task.uuid} className={`flex flex-row pb-1 priority-${task.priority.toLowerCase()}`}>
<div
key={task.uuid}
className={`flex flex-row pb-1 dark:text-gray-100 priority-${task.priority.toLowerCase()}`}
>
<div>
<Checkbox
checked={checked}
onChange={handleTaskChange}
className="pt-1 mr-1"
/>
</div>
<div className="flex-1 border-b border-gray-100 pb-2 pt-1 text-sm leading-normal break-all">
<div className="flex-1 border-b border-gray-100 dark:border-gray-400 pb-2 pt-1 text-sm leading-normal break-all">
<div className="flex justify-between items-center">
<div className="flex-col">
<div className={contentClassName}>
<span className="py-0.5 px-1 mr-1 text-xs font-gray-300 bg-gray-200 rounded" onClick={toggleMarker}>
<span
className="py-0.5 px-1 mr-1 text-xs font-gray-300 rounded"
style={{ backgroundColor: themeStyle.secondaryBackgroundColor }}
onClick={toggleMarker}
>
{task.marker}
</span>
<span onClick={openTaskBlock}>
{task.content}
</span>
<span onClick={openTaskBlock}>{task.content}</span>
</div>
<p>
{task.scheduled && (
Expand Down
16 changes: 11 additions & 5 deletions src/components/TaskSection.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import useThemeStyle from '../hooks/useThemeStyle';
import { TaskEntityObject } from '../models/TaskEntity';
import TaskItem from './TaskItem';

Expand All @@ -9,20 +10,25 @@ export interface ITaskSectionProps {

const TaskSection: React.FC<ITaskSectionProps> = (props) => {
const { title, tasks } = props;
const themeStyle = useThemeStyle();

if (tasks.length === 0) {
return null;
}

return (
<div className="py-1">
<h2 className="py-1 text-red-500">{title}</h2>
<h2
className="py-1 text-blue-400"
style={{
color: themeStyle.sectionTitleColor,
}}
>
{title}
</h2>
<div>
{tasks.map((task) => (
<TaskItem
key={task.uuid}
item={task}
/>
<TaskItem key={task.uuid} item={task} />
))}
</div>
</div>
Expand Down
71 changes: 55 additions & 16 deletions src/hooks/useAppState.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AppUserConfigs } from '@logseq/libs/dist/LSPlugin.user';
import React, { useCallback, useContext, useMemo } from 'react';
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { TaskEntityObject } from '../models/TaskEntity';
import getAnytimeTaskQuery from '../querys/anytime';
import getScheduledTaskQuery from '../querys/scheduled';
Expand All @@ -8,22 +8,39 @@ import useTaskQuery from './useTaskQuery';
import useUserConfigs, { DEFAULT_USER_CONFIGS } from './useUserConfigs';

export interface IAppState {
userConfigs: Partial<AppUserConfigs>,
userConfigs: Partial<AppUserConfigs>;
settings: {
lightPrimaryBackgroundColor: string;
lightSecondaryBackgroundColor: string;
darkPrimaryBackgroundColor: string;
darkSecondaryBackgroundColor: string;
sectionTitleColor: string;
};
tasks: {
today: TaskEntityObject[],
scheduled: TaskEntityObject[],
anytime: TaskEntityObject[],
},
refresh(): void,
today: TaskEntityObject[];
scheduled: TaskEntityObject[];
anytime: TaskEntityObject[];
};
refresh(): void;
}

const DEFAULT_SETTINGS = {
sectionTitleColor: '#0a0a0a',
lightPrimaryBackgroundColor: '#ffffff',
lightSecondaryBackgroundColor: '#f7f7f7',
darkPrimaryBackgroundColor: '#002B37',
darkSecondaryBackgroundColor: '#106ba3',
};

const AppStateContext = React.createContext<IAppState>({
userConfigs: DEFAULT_USER_CONFIGS,
settings: DEFAULT_SETTINGS,
tasks: {
today: [],
scheduled: [],
anytime: [],
},
// eslint-disable-next-line @typescript-eslint/no-empty-function
refresh: () => {},
});

Expand All @@ -32,10 +49,14 @@ const useAppState = () => {
return appState;
};

// eslint-disable-next-line @typescript-eslint/ban-types
export const withAppState = <P extends {}>(
WrapComponent: React.ComponentType<P>,
) => {
const WithAppState: typeof WrapComponent = (props) => {
const [settings, setSettings] = useState(
(logseq.settings as unknown as IAppState['settings']) ?? DEFAULT_SETTINGS,
);
const userConfigs = useUserConfigs();
const todayTask = useTaskQuery(getTodayTaskQuery());
const scheduledTask = useTaskQuery(getScheduledTaskQuery());
Expand All @@ -47,15 +68,33 @@ export const withAppState = <P extends {}>(
anytimeTask.mutate();
}, [todayTask, scheduledTask, anytimeTask]);

const state = useMemo(() => ({
userConfigs,
tasks: {
today: todayTask.data || [],
scheduled: scheduledTask.data || [],
anytime: anytimeTask.data || [],
},
refresh,
}), [userConfigs, todayTask, scheduledTask, anytimeTask, refresh]);
const state = useMemo(
() => ({
userConfigs,
settings,
tasks: {
today: todayTask.data || [],
scheduled: scheduledTask.data || [],
anytime: anytimeTask.data || [],
},
refresh,
}),
[
userConfigs,
settings,
todayTask.data,
scheduledTask.data,
anytimeTask.data,
refresh,
],
);

useEffect(() => {
const unlisten = logseq.onSettingsChanged((newSettings) => {
setSettings(newSettings);
});
return () => unlisten();
}, []);

return (
<AppStateContext.Provider value={state}>
Expand Down
Loading

0 comments on commit 1c7448c

Please sign in to comment.