refactor(style): The Big Reformat, part 2

reformat: All files have been reformatted.
This commit is contained in:
tecc 2022-09-04 01:25:31 +02:00
parent 5622cc7c16
commit f1898cc133
No known key found for this signature in database
GPG Key ID: 400AAD881FCC028B
134 changed files with 2393 additions and 2122 deletions

View File

@ -1,4 +1,4 @@
{
"presets": ["next/babel"],
"plugins": ["lodash"]
"presets": ["next/babel"],
"plugins": ["lodash"]
}

6
.github/pull.yml vendored
View File

@ -1,5 +1,5 @@
version: '1'
rules:
- base: main
upstream: qingwei-li:main
mergeMethod: hardreset
- base: main
upstream: qingwei-li:main
mergeMethod: hardreset

View File

@ -3,49 +3,49 @@ name: Publish Docker
on: [push]
jobs:
# test:
# name: Test
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v2
# - name: Install modules
# run: yarn
# - name: Run MinIO
# run: docker-compose up -d
# - name: Run tests
# run: yarn test
publish:
name: Publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Docker meta
id: meta
uses: crazy-max/ghaction-docker-meta@v2
with:
images: cinwell/notea
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- uses: mstachniuk/ci-skip@v1
with:
fail-fast: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# test:
# name: Test
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v2
# - name: Install modules
# run: yarn
# - name: Run MinIO
# run: docker-compose up -d
# - name: Run tests
# run: yarn test
publish:
name: Publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Docker meta
id: meta
uses: crazy-max/ghaction-docker-meta@v2
with:
images: cinwell/notea
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- uses: mstachniuk/ci-skip@v1
with:
fail-fast: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

10
additional.d.ts vendored
View File

@ -1,9 +1,9 @@
declare module 'react-split'
declare module 'react-split';
declare module 'remove-markdown'
declare module 'remove-markdown';
declare module 'outline-icons'
declare module 'outline-icons';
declare module '@headwayapp/react-widget'
declare module '@headwayapp/react-widget';
declare module 'markdown-link-extractor'
declare module 'markdown-link-extractor';

View File

@ -2,11 +2,13 @@ import { Button, ButtonProps, CircularProgress } from '@material-ui/core';
import classNames from 'classnames';
import { forwardRef } from 'react';
export const ButtonProgress = forwardRef<HTMLSpanElement,
export const ButtonProgress = forwardRef<
HTMLSpanElement,
ButtonProps & {
loading?: boolean
progress?: number
}>(({children, loading, progress, ...props}, ref) => {
loading?: boolean;
progress?: number;
}
>(({ children, loading, progress, ...props }, ref) => {
return (
<Button
{...props}
@ -16,7 +18,9 @@ export const ButtonProgress = forwardRef<HTMLSpanElement,
variant="contained"
component="span"
>
<span className={classNames({invisible: loading})}>{children}</span>
<span className={classNames({ invisible: loading })}>
{children}
</span>
{loading ? (
<CircularProgress
className="absolute"

View File

@ -15,10 +15,10 @@ const MainEditor = dynamic(() => import('components/editor/main-editor'));
export const EditContainer = () => {
const {
title: {updateTitle},
settings: {settings},
title: { updateTitle },
settings: { settings },
} = UIState.useContainer();
const {genNewId} = NoteTreeState.useContainer();
const { genNewId } = NoteTreeState.useContainer();
const {
fetchNote,
abortFindNote,
@ -26,11 +26,11 @@ export const EditContainer = () => {
initNote,
note,
} = NoteState.useContainer();
const {query} = useRouter();
const { query } = useRouter();
const pid = query.pid as string;
const id = query.id as string;
const isNew = has(query, 'new');
const {mutate: mutateSettings} = useSettingsAPI();
const { mutate: mutateSettings } = useSettingsAPI();
const toast = useToast();
const loadNoteById = useCallback(
@ -46,23 +46,23 @@ export const EditContainer = () => {
} else if (id === 'new') {
const url = `/${genNewId()}?new` + (pid ? `&pid=${pid}` : '');
router.replace(url, undefined, {shallow: true});
router.replace(url, undefined, { shallow: true });
} else if (id && !isNew) {
try {
const result = await fetchNote(id);
if (!result) {
router.replace({query: {...router.query, new: 1}});
router.replace({ query: { ...router.query, new: 1 } });
return;
}
} catch (msg) {
if (msg.name !== 'AbortError') {
toast(msg.message, 'error');
router.push('/', undefined, {shallow: true});
router.push('/', undefined, { shallow: true });
}
}
} else {
if (await noteCache.getItem(id)) {
router.push(`/${id}`, undefined, {shallow: true});
router.push(`/${id}`, undefined, { shallow: true });
return;
}
@ -102,10 +102,10 @@ export const EditContainer = () => {
return (
<>
<NoteNav/>
<DeleteAlert/>
<NoteNav />
<DeleteAlert />
<section className="h-full">
<MainEditor note={note}/>
<MainEditor note={note} />
</section>
</>
);

View File

@ -10,12 +10,12 @@ import MainEditor from 'components/editor/main-editor';
const MAX_WIDTH = 900;
export const PostContainer: FC<{
isPreview?: boolean
note?: NoteModel
}> = ({isPreview = false, note}) => {
isPreview?: boolean;
note?: NoteModel;
}> = ({ isPreview = false, note }) => {
const {
settings: {
settings: {injection},
settings: { injection },
},
} = UIState.useContainer();
@ -42,7 +42,7 @@ export const PostContainer: FC<{
<InnerHTML
id="snippet-injection"
className={className}
style={{width: MAX_WIDTH}}
style={{ width: MAX_WIDTH }}
html={injectionHTML}
/>
) : null}

View File

@ -5,8 +5,8 @@ import Link from 'next/link';
import React, { FC, useEffect } from 'react';
const Backlinks: FC = () => {
const {getBackLinks, onHoverLink, backlinks} = EditorState.useContainer();
const {t} = useI18n();
const { getBackLinks, onHoverLink, backlinks } = EditorState.useContainer();
const { t } = useI18n();
useEffect(() => {
getBackLinks();
@ -18,7 +18,9 @@ const Backlinks: FC = () => {
return (
<div className="mb-40">
<h4 className="text-xs px-2 text-gray-400">{t('Linked to this page')}</h4>
<h4 className="text-xs px-2 text-gray-400">
{t('Linked to this page')}
</h4>
<ul className="bg-gray-100 mt-2 rounded overflow-hidden">
{backlinks?.map((link) => (
<li key={link.id}>
@ -27,8 +29,13 @@ const Backlinks: FC = () => {
className="p-2 flex items-center hover:bg-gray-300 truncate"
onMouseEnter={onHoverLink}
>
<IconButton className="mr-1" icon="DocumentText"></IconButton>
<span className="flex-1 truncate">{link.title}</span>
<IconButton
className="mr-1"
icon="DocumentText"
></IconButton>
<span className="flex-1 truncate">
{link.title}
</span>
</a>
</Link>
</li>

View File

@ -4,8 +4,8 @@ import NoteState from 'libs/web/state/note';
import useI18n from 'libs/web/hooks/use-i18n';
const Inner = () => {
const {t} = useI18n();
const {note} = NoteState.useContainer();
const { t } = useI18n();
const { note } = NoteState.useContainer();
if (note?.deleted !== NOTE_DELETED.DELETED) {
return null;
@ -26,5 +26,5 @@ const Inner = () => {
};
export default function DeleteAlert() {
return <Inner/>;
return <Inner />;
}

View File

@ -2,7 +2,7 @@ import useI18n from 'libs/web/hooks/use-i18n';
import { useMemo } from 'react';
export const useDictionary = () => {
const {t} = useI18n();
const { t } = useI18n();
const dictionary = useMemo(
() => ({
@ -26,7 +26,9 @@ export const useDictionary = () => {
deleteTable: t('Delete table'),
deleteImage: t('Delete image'),
em: t('Italic'),
embedInvalidLink: t('Sorry, that link wont work for this embed type'),
embedInvalidLink: t(
'Sorry, that link wont work for this embed type'
),
findOrCreateDoc: t('Find or create a note…'),
h1: t('Big heading'),
h2: t('Medium heading'),
@ -48,7 +50,7 @@ export const useDictionary = () => {
pageBreak: t('Page break'),
pasteLink: t('Paste a link…'),
pasteLinkWithTitle: (title: string): string =>
t(`Paste a {{title}} link…`, {title}),
t(`Paste a {{title}} link…`, { title }),
placeholder: t('Placeholder'),
quote: t('Quote'),
removeLink: t('Remove link'),

View File

@ -5,8 +5,8 @@ import { useRouter } from 'next/router';
import { FC, useCallback, KeyboardEvent, useRef, useMemo } from 'react';
import EditorState from 'libs/web/state/editor';
const EditTitle: FC<{ readOnly?: boolean }> = ({readOnly}) => {
const {editorEl, onNoteChange, note} = EditorState.useContainer();
const EditTitle: FC<{ readOnly?: boolean }> = ({ readOnly }) => {
const { editorEl, onNoteChange, note } = EditorState.useContainer();
const router = useRouter();
const inputRef = useRef<HTMLTextAreaElement>(null);
const onInputTitle = useCallback(
@ -23,13 +23,13 @@ const EditTitle: FC<{ readOnly?: boolean }> = ({readOnly}) => {
const onTitleChange = useCallback(
(event) => {
const title = event.target.value;
onNoteChange.callback({title});
onNoteChange.callback({ title });
},
[onNoteChange]
);
const autoFocus = useMemo(() => has(router.query, 'new'), [router.query]);
const {t} = useI18n();
const { t } = useI18n();
return (
<h1 className="text-3xl mb-8">

View File

@ -14,7 +14,7 @@ export interface EditorProps extends Pick<Props, 'readOnly'> {
isPreview?: boolean;
}
const Editor: FC<EditorProps> = ({readOnly, isPreview}) => {
const Editor: FC<EditorProps> = ({ readOnly, isPreview }) => {
const {
onSearchLink,
onCreateLink,
@ -62,38 +62,40 @@ const Editor: FC<EditorProps> = ({readOnly, isPreview}) => {
embeds={embeds}
/>
<style jsx global>{`
.ProseMirror ul {
list-style-type: disc;
}
.ProseMirror ul {
list-style-type: disc;
}
.ProseMirror ol {
list-style-type: decimal;
}
.ProseMirror ol {
list-style-type: decimal;
}
.ProseMirror {
${hasMinHeight
? `min-height: calc(${height ? height + 'px' : '100vh'} - 14rem);`
: ''}
padding-bottom: 10rem;
}
.ProseMirror {
${hasMinHeight
? `min-height: calc(${
height ? height + 'px' : '100vh'
} - 14rem);`
: ''}
padding-bottom: 10rem;
}
.ProseMirror h1 {
font-size: 2.8em;
}
.ProseMirror h2 {
font-size: 1.8em;
}
.ProseMirror h3 {
font-size: 1.5em;
}
.ProseMirror a:not(.bookmark) {
text-decoration: underline;
}
.ProseMirror h1 {
font-size: 2.8em;
}
.ProseMirror h2 {
font-size: 1.8em;
}
.ProseMirror h3 {
font-size: 1.5em;
}
.ProseMirror a:not(.bookmark) {
text-decoration: underline;
}
.ProseMirror .image .ProseMirror-selectednode img {
pointer-events: unset;
}
`}</style>
.ProseMirror .image .ProseMirror-selectednode img {
pointer-events: unset;
}
`}</style>
</>
);
};

View File

@ -5,8 +5,8 @@ import { FC, useEffect, useState } from 'react';
import { Metadata } from 'unfurl.js/dist/types';
import { EmbedProps } from '.';
export const Bookmark: FC<EmbedProps> = ({attrs: {href}}) => {
const {request} = useFetcher();
export const Bookmark: FC<EmbedProps> = ({ attrs: { href } }) => {
const { request } = useFetcher();
const [data, setData] = useState<Metadata>();
useEffect(() => {
@ -49,7 +49,11 @@ export const Bookmark: FC<EmbedProps> = ({attrs: {href}}) => {
</div>
{!!image && (
<div className="md:w-48 flex w-0">
<img className="m-auto object-cover h-full" src={image} alt={title}/>
<img
className="m-auto object-cover h-full"
src={image}
alt={title}
/>
</div>
)}
</a>

View File

@ -6,8 +6,8 @@ import { EmbedProps } from '.';
import InnerHTML from 'dangerously-set-html-content';
import { decode } from 'qss';
export const Embed: FC<EmbedProps> = ({attrs: {href}}) => {
const {request} = useFetcher();
export const Embed: FC<EmbedProps> = ({ attrs: { href } }) => {
const { request } = useFetcher();
const [data, setData] = useState<Metadata>();
useEffect(() => {
@ -26,12 +26,12 @@ export const Embed: FC<EmbedProps> = ({attrs: {href}}) => {
const html = (data?.oEmbed as any)?.html;
if (html) {
return <InnerHTML html={html}/>;
return <InnerHTML html={html} />;
}
const url =
data?.open_graph?.url ??
decode<{ url: string }>(href.replace(/.*\?/, '')).url;
return <iframe className="w-full h-96" src={url} allowFullScreen/>;
return <iframe className="w-full h-96" src={url} allowFullScreen />;
};

View File

@ -6,10 +6,10 @@ import { Embed } from './embed';
export type EmbedProps = {
attrs: {
href: string
matches: string[]
}
}
href: string;
matches: string[];
};
};
export const useEmbeds = () => {
const csrfToken = CsrfTokenState.useContainer();

View File

@ -15,7 +15,7 @@ export default class Bracket extends Mark {
inputRules() {
return [
new InputRule(/(?:(\[|【){2})$/, (state, _match, start, end) => {
const {tr} = state;
const { tr } = state;
tr.delete(start, end);
this.editor.handleOpenLinkMenu();

View File

@ -6,25 +6,29 @@ import UIState from 'libs/web/state/ui';
import { FC } from 'react';
import { NoteModel } from 'libs/shared/note';
const MainEditor: FC<EditorProps & {
note?: NoteModel
isPreview?: boolean
className?: string
}> = ({className, note, isPreview, ...props}) => {
const MainEditor: FC<
EditorProps & {
note?: NoteModel;
isPreview?: boolean;
className?: string;
}
> = ({ className, note, isPreview, ...props }) => {
const {
settings: {settings},
settings: { settings },
} = UIState.useContainer();
const editorWidthClass =
(note?.editorsize ?? settings.editorsize) > 0 ? 'max-w-4xl' : 'max-w-prose';
(note?.editorsize ?? settings.editorsize) > 0
? 'max-w-4xl'
: 'max-w-prose';
const articleClassName =
className || `pt-40 px-6 m-auto h-full ${editorWidthClass}`;
return (
<EditorState.Provider initialState={note}>
<article className={articleClassName}>
<EditTitle readOnly={props.readOnly}/>
<EditTitle readOnly={props.readOnly} />
<Editor isPreview={isPreview} {...props} />
{!isPreview && <Backlinks/>}
{!isPreview && <Backlinks />}
</article>
</EditorState.Provider>
);

View File

@ -17,7 +17,7 @@ export const lightTheme: typeof theme = {
};
export const useEditorTheme = () => {
const {resolvedTheme} = useTheme();
const { resolvedTheme } = useTheme();
return resolvedTheme === 'dark' ? darkTheme : lightTheme;
};

View File

@ -2,9 +2,9 @@ import { Tooltip as MuiTooltip } from '@material-ui/core';
import { FC } from 'react';
const Tooltip: FC<{
tooltip: string
placement: 'top' | 'bottom' | 'left' | 'right'
}> = ({children, tooltip, placement}) => {
tooltip: string;
placement: 'top' | 'bottom' | 'left' | 'right';
}> = ({ children, tooltip, placement }) => {
return (
<MuiTooltip title={tooltip} placement={placement}>
<div>{children}</div>

View File

@ -5,39 +5,39 @@ import { FC } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
const Title: FC<{
text: string
keys: string[]
}> = ({text, keys}) => {
text: string;
keys: string[];
}> = ({ text, keys }) => {
return (
<span>
{text} {keys.join('+')}
</span>
{text} {keys.join('+')}
</span>
);
};
const HotkeyTooltip: FC<{
text: string
keys?: string[]
text: string;
keys?: string[];
/**
* first key
*/
commandKey?: boolean
optionKey?: boolean
onClose?: TooltipProps['onClose']
onHotkey?: () => void
disableOnContentEditable?: boolean
commandKey?: boolean;
optionKey?: boolean;
onClose?: TooltipProps['onClose'];
onHotkey?: () => void;
disableOnContentEditable?: boolean;
}> = ({
text,
keys = [],
children,
onClose,
commandKey,
optionKey,
onHotkey = noop,
disableOnContentEditable = false,
}) => {
text,
keys = [],
children,
onClose,
commandKey,
optionKey,
onHotkey = noop,
disableOnContentEditable = false,
}) => {
const {
ua: {isMac},
ua: { isMac },
} = UIState.useContainer();
const keyMap = [...keys];
@ -66,8 +66,8 @@ const HotkeyTooltip: FC<{
return (
<Tooltip
enterDelay={200}
TransitionProps={{timeout: 0}}
title={<Title text={text} keys={keyMap}/>}
TransitionProps={{ timeout: 0 }}
title={<Title text={text} keys={keyMap} />}
onClose={onClose}
placement="bottom-start"
>

View File

@ -19,7 +19,7 @@ import {
BookmarkAltIcon,
PuzzleIcon,
ChevronDoubleUpIcon,
RefreshIcon
RefreshIcon,
} from '@heroicons/react/outline';
export const ICONS = {
@ -41,15 +41,17 @@ export const ICONS = {
BookmarkAlt: BookmarkAltIcon,
Puzzle: PuzzleIcon,
ChevronDoubleUp: ChevronDoubleUpIcon,
Refresh: RefreshIcon
Refresh: RefreshIcon,
};
const IconButton = forwardRef<HTMLSpanElement,
const IconButton = forwardRef<
HTMLSpanElement,
HTMLProps<HTMLSpanElement> & {
icon: keyof typeof ICONS
iconClassName?: string
rounded?: boolean
}>(
icon: keyof typeof ICONS;
iconClassName?: string;
rounded?: boolean;
}
>(
(
{
children,
@ -89,9 +91,9 @@ const IconButton = forwardRef<HTMLSpanElement,
className
)}
>
<Icon className={classNames(iconClassName)}></Icon>
<Icon className={classNames(iconClassName)}></Icon>
{children}
</span>
</span>
);
}
);

View File

@ -17,34 +17,34 @@ import { NoteModel } from 'libs/shared/note';
import PreviewModal from 'components/portal/preview-modal';
import LinkToolbar from 'components/portal/link-toolbar/link-toolbar';
const MainWrapper: FC = ({children}) => {
const MainWrapper: FC = ({ children }) => {
const {
sidebar: {isFold},
sidebar: { isFold },
} = UIState.useContainer();
const {ref, width = 0} = useResizeDetector<HTMLDivElement>({
const { ref, width = 0 } = useResizeDetector<HTMLDivElement>({
handleHeight: false,
});
return (
<div className="h-full" ref={ref}>
<Resizable width={width}>
<Sidebar/>
<Sidebar />
<main className="relative">{children}</main>
</Resizable>
<style jsx global>
{`
.gutter {
pointer-events: ${isFold ? 'none' : 'auto'};
}
`}
.gutter {
pointer-events: ${isFold ? 'none' : 'auto'};
}
`}
</style>
</div>
);
};
const MobileMainWrapper: FC = ({children}) => {
const MobileMainWrapper: FC = ({ children }) => {
const {
sidebar: {isFold, open, close},
sidebar: { isFold, open, close },
} = UIState.useContainer();
return (
@ -58,7 +58,7 @@ const MobileMainWrapper: FC = ({children}) => {
// todo 优化移动端左边按钮和滑动冲突的问题
disableDiscovery
>
<Sidebar/>
<Sidebar />
</SwipeableDrawer>
<main className="flex-grow" onClick={close}>
@ -66,20 +66,20 @@ const MobileMainWrapper: FC = ({children}) => {
</main>
<style jsx global>
{`
.gutter {
pointer-events: none;
}
`}
.gutter {
pointer-events: none;
}
`}
</style>
</div>
);
};
const LayoutMain: FC<{
tree?: TreeModel
note?: NoteModel
}> = ({children, tree, note}) => {
const {ua} = UIState.useContainer();
tree?: TreeModel;
note?: NoteModel;
}> = ({ children, tree, note }) => {
const { ua } = UIState.useContainer();
useEffect(() => {
document.body.classList.add('overscroll-none');
@ -97,15 +97,15 @@ const LayoutMain: FC<{
{/* modals */}
<TrashState.Provider>
<TrashModal/>
<TrashModal />
</TrashState.Provider>
<SearchState.Provider>
<SearchModal/>
<SearchModal />
</SearchState.Provider>
<ShareModal/>
<PreviewModal/>
<LinkToolbar/>
<SidebarMenu/>
<ShareModal />
<PreviewModal />
<LinkToolbar />
<SidebarMenu />
</NoteState.Provider>
</NoteTreeState.Provider>
);

View File

@ -10,12 +10,12 @@ import { NextSeo } from 'next-seo';
import { removeMarkdown } from 'libs/web/utils/markdown';
const LayoutPublic: FC<{
tree?: TreeModel
note?: NoteModel
pageMode: PageMode
baseURL: string
}> = ({children, note, tree, pageMode, baseURL}) => {
const {t} = useI18n();
tree?: TreeModel;
note?: NoteModel;
pageMode: PageMode;
baseURL: string;
}> = ({ children, note, tree, pageMode, baseURL }) => {
const { t } = useI18n();
const description = useMemo(
() => removeMarkdown(note?.content).slice(0, 100),
@ -36,7 +36,9 @@ const LayoutPublic: FC<{
title: note?.title,
description,
url: `${baseURL}/${note?.id}`,
images: [{url: note?.pic ?? `${baseURL}/logo_1280x640.png`}],
images: [
{ url: note?.pic ?? `${baseURL}/logo_1280x640.png` },
],
type: 'article',
article: {
publishedTime: note?.date,
@ -44,7 +46,9 @@ const LayoutPublic: FC<{
}}
/>
<NoteTreeState.Provider initialState={tree}>
<NoteState.Provider initialState={note}>{children}</NoteState.Provider>
<NoteState.Provider initialState={note}>
{children}
</NoteState.Provider>
</NoteTreeState.Provider>
</>
);

View File

@ -5,8 +5,8 @@ import HotkeyTooltip from './hotkey-tooltip';
import IconButton from './icon-button';
const NavButtonGroup: FC = () => {
const {t} = useI18n();
const {back: routerBack, beforePopState} = useRouter();
const { t } = useI18n();
const { back: routerBack, beforePopState } = useRouter();
const [canBack, setCanBack] = useState(false);
const [canForward, setCanForward] = useState(false);
const isBackRef = useRef(false);
@ -73,7 +73,11 @@ const NavButtonGroup: FC = () => {
commandKey
disableOnContentEditable
>
<IconButton disabled={!canBack} icon="ArrowSmLeft" onClick={back}/>
<IconButton
disabled={!canBack}
icon="ArrowSmLeft"
onClick={back}
/>
</HotkeyTooltip>
<HotkeyTooltip
text={t('Forward')}

View File

@ -15,7 +15,7 @@ import NavButtonGroup from './nav-button-group';
import { EyeIcon } from '@heroicons/react/outline';
const MenuButton = () => {
const {sidebar} = UIState.useContainer();
const { sidebar } = UIState.useContainer();
const onToggle = useCallback(
(e: MouseEvent) => {
@ -35,11 +35,15 @@ const MenuButton = () => {
};
const NoteNav = () => {
const {t} = useI18n();
const {note, loading} = NoteState.useContainer();
const {ua} = UIState.useContainer();
const {getPaths, showItem, checkItemIsShown} = NoteTreeState.useContainer();
const {share, menu} = PortalState.useContainer();
const { t } = useI18n();
const { note, loading } = NoteState.useContainer();
const { ua } = UIState.useContainer();
const {
getPaths,
showItem,
checkItemIsShown,
} = NoteTreeState.useContainer();
const { share, menu } = PortalState.useContainer();
const handleClickShare = useCallback(
(event: MouseEvent) => {
@ -76,8 +80,8 @@ const NoteNav = () => {
width: ua.isMobileOnly ? '100%' : 'inherit',
}}
>
{ua.isMobileOnly ? <MenuButton/> : null}
<NavButtonGroup/>
{ua.isMobileOnly ? <MenuButton /> : null}
<NavButtonGroup />
<div className="flex-auto ml-4">
{note && (
<Breadcrumbs
@ -99,42 +103,45 @@ const NoteNav = () => {
</Tooltip>
))}
<span>
<Tooltip title={note.title}>
<span
className="title inline-block text-gray-600 text-sm truncate select-none align-middle"
aria-current="page"
>
{note.title}
</span>
</Tooltip>
<Tooltip title={note.title}>
<span
className="title inline-block text-gray-600 text-sm truncate select-none align-middle"
aria-current="page"
>
{note.title}
</span>
</Tooltip>
{!checkItemIsShown(note) && (
<Tooltip title={t('Show note in tree')}>
<span>
<EyeIcon
width="20"
className="inline-block cursor-pointer ml-1"
onClick={handleClickOpenInTree}
/>
</span>
<span>
<EyeIcon
width="20"
className="inline-block cursor-pointer ml-1"
onClick={handleClickOpenInTree}
/>
</span>
</Tooltip>
)}
</span>
</span>
</Breadcrumbs>
)}
<style jsx>
{`
.title {
max-width: 120px;
}
`}
.title {
max-width: 120px;
}
`}
</style>
</div>
<div
className={classNames('flex mr-2 transition-opacity delay-100', {
'opacity-0': !loading,
})}
className={classNames(
'flex mr-2 transition-opacity delay-100',
{
'opacity-0': !loading,
}
)}
>
<CircularProgress size="14px" color="inherit"/>
<CircularProgress size="14px" color="inherit" />
</div>
<HotkeyTooltip text={t('Share page')}>
<IconButton

View File

@ -12,13 +12,13 @@ interface Props extends PopperProps {
}
const Popover: FC<Props> = ({
handleClose,
handleOpen,
setAnchor,
children,
delay = DELAY,
...props
}) => {
handleClose,
handleOpen,
setAnchor,
children,
delay = DELAY,
...props
}) => {
const anchorRef = useRef<HTMLLinkElement | null>();
const router = useRouter();
const leaveTimer = useRef<number>();

View File

@ -4,12 +4,12 @@ import { useDebouncedCallback } from 'use-debounce';
import useI18n from 'libs/web/hooks/use-i18n';
const FilterModalInput: FC<{
doFilter: (keyword: string) => void
keyword?: string
placeholder: string
onClose: () => void
}> = ({doFilter, keyword, placeholder, onClose}) => {
const {t} = useI18n();
doFilter: (keyword: string) => void;
keyword?: string;
placeholder: string;
onClose: () => void;
}> = ({ doFilter, keyword, placeholder, onClose }) => {
const { t } = useI18n();
const inputRef = useRef<HTMLInputElement>(null);
const debouncedFilter = useDebouncedCallback((value: string) => {
doFilter(value);
@ -21,7 +21,7 @@ const FilterModalInput: FC<{
return (
<div className="flex py-2 px-4">
<SearchIcon width="20"/>
<SearchIcon width="20" />
<input
ref={inputRef}
defaultValue={keyword}

View File

@ -14,12 +14,12 @@ interface Props<T> {
}
export default function FilterModalList<T>({
ItemComponent,
items,
onEnter,
}: Props<T>) {
ItemComponent,
items,
onEnter,
}: Props<T>) {
const {
ua: {isMobileOnly},
ua: { isMobileOnly },
} = UIState.useContainer();
const height = use100vh() || 0;
const calcHeight = isMobileOnly ? height : (height * 2) / 3;
@ -68,10 +68,12 @@ export default function FilterModalList<T>({
</ul>
) : null}
<style jsx>{`
.list {
max-height: calc(${calcHeight ? calcHeight + 'px' : '100vh'} - 40px);
}
`}</style>
.list {
max-height: calc(
${calcHeight ? calcHeight + 'px' : '100vh'} - 40px
);
}
`}</style>
</>
);
}

View File

@ -5,12 +5,12 @@ import classNames from 'classnames';
import router from 'next/router';
const FilterModal: FC<{
open: ModalProps['open']
onClose: () => void
onOpen?: () => void
}> = ({open, onClose, onOpen, children}) => {
open: ModalProps['open'];
onClose: () => void;
onOpen?: () => void;
}> = ({ open, onClose, onOpen, children }) => {
const {
ua: {isMobileOnly},
ua: { isMobileOnly },
} = UIState.useContainer();
useEffect(() => {
@ -28,14 +28,14 @@ const FilterModal: FC<{
const props: Partial<DialogProps> = isMobileOnly
? {
fullScreen: true,
}
fullScreen: true,
}
: {
style: {
inset: '0 0 auto 0',
marginTop: '10vh',
},
};
style: {
inset: '0 0 auto 0',
marginTop: '10vh',
},
};
return (
<Dialog

View File

@ -2,14 +2,14 @@ import { FC, memo, ReactNode } from 'react';
import { searchRangeText } from 'libs/web/utils/search';
const MarkText: FC<{
text?: string
keyword?: string
maxLen?: number
}> = memo(({text = '', keyword = '', maxLen = 80}) => {
text?: string;
keyword?: string;
maxLen?: number;
}> = memo(({ text = '', keyword = '', maxLen = 80 }) => {
if (!text || !keyword) return <span>{text}</span>;
const texts: ReactNode[] = [];
const {re, match} = searchRangeText({
const { re, match } = searchRangeText({
text,
keyword,
maxLen,
@ -20,7 +20,10 @@ const MarkText: FC<{
while ((block = re.exec(match))) {
texts.push(
match.slice(index, block.index),
<mark className="font-bold text-gray-800 bg-transparent" key={index}>
<mark
className="font-bold text-gray-800 bg-transparent"
key={index}
>
{block[0]}
</mark>
);

View File

@ -8,9 +8,9 @@ import { useCallback } from 'react';
import { findPlaceholderLink } from 'libs/web/editor/link';
const LinkToolbar = () => {
const {t} = useI18n();
const { t } = useI18n();
const {
linkToolbar: {anchor, open, close, visible, data, setAnchor},
linkToolbar: { anchor, open, close, visible, data, setAnchor },
} = PortalState.useContainer();
const openLink = useCallback(() => {
@ -21,17 +21,19 @@ const LinkToolbar = () => {
const createEmbed = useCallback(
(type: 'bookmark' | 'embed') => {
const {view, href} = data ?? {};
const { view, href } = data ?? {};
if (!view || !href) {
return;
}
const {dispatch, state} = view;
const { dispatch, state } = view;
const result = findPlaceholderLink(state.doc, href);
if (!result) {
return;
}
const bookmarkUrl = `/api/extract?type=${type}&url=${encodeURIComponent(href)}`;
const bookmarkUrl = `/api/extract?type=${type}&url=${encodeURIComponent(
href
)}`;
const transaction = state.tr.replaceWith(
result.pos,
result.pos + result.node.nodeSize,
@ -60,7 +62,10 @@ const LinkToolbar = () => {
>
<Paper className="relative bg-gray-50 flex p-1 space-x-1">
<HotkeyTooltip text={t('Open link')}>
<IconButton onClick={openLink} icon={'ExternalLink'}></IconButton>
<IconButton
onClick={openLink}
icon={'ExternalLink'}
></IconButton>
</HotkeyTooltip>
<HotkeyTooltip text={t('Create bookmark')}>
<IconButton

View File

@ -11,12 +11,12 @@ import useI18n from 'libs/web/hooks/use-i18n';
import Popover from 'components/popover';
const PreviewModal: FC = () => {
const {t} = useI18n();
const { t } = useI18n();
const {
preview: {anchor, open, close, visible, data, setAnchor},
preview: { anchor, open, close, visible, data, setAnchor },
} = PortalState.useContainer();
const router = useRouter();
const {fetch: fetchNote} = useNoteAPI();
const { fetch: fetchNote } = useNoteAPI();
const [note, setNote] = useState<NoteCacheItem>();
const findNote = useCallback(
@ -28,7 +28,7 @@ const PreviewModal: FC = () => {
const gotoLink = useCallback(() => {
if (note?.id) {
router.push(note.id, undefined, {shallow: true});
router.push(note.id, undefined, { shallow: true });
}
}, [note?.id, router]);
@ -48,7 +48,7 @@ const PreviewModal: FC = () => {
setAnchor={setAnchor}
transition
>
{({TransitionProps}) => (
{({ TransitionProps }) => (
<Fade
{...TransitionProps}
timeout={{
@ -58,11 +58,17 @@ const PreviewModal: FC = () => {
<Paper className="relative bg-gray-50 text-gray-800 w-full h-96 md:w-96 dark:bg-gray-800">
<div className="absolute right-2 top-2">
<HotkeyTooltip text={t('Open link')}>
<IconButton onClick={gotoLink} icon="ExternalLink"></IconButton>
<IconButton
onClick={gotoLink}
icon="ExternalLink"
></IconButton>
</HotkeyTooltip>
</div>
<div className="overflow-y-scroll h-full p-4">
<PostContainer isPreview note={note}></PostContainer>
<PostContainer
isPreview
note={note}
></PostContainer>
</div>
</Paper>
</Fade>

View File

@ -10,14 +10,14 @@ import NoteTreeState from 'libs/web/state/tree';
import { Breadcrumbs } from '@material-ui/core';
const SearchItem: FC<{
note: NoteCacheItem
keyword?: string
selected?: boolean
}> = ({note, keyword, selected}) => {
note: NoteCacheItem;
keyword?: string;
selected?: boolean;
}> = ({ note, keyword, selected }) => {
const {
search: {close},
search: { close },
} = PortalState.useContainer();
const {getPaths} = NoteTreeState.useContainer();
const { getPaths } = NoteTreeState.useContainer();
const ref = useRef<HTMLLIElement>(null);
useScrollView(ref, selected);
@ -30,9 +30,12 @@ const SearchItem: FC<{
})}
>
<Link href={`/${note.id}`} shallow>
<a className="py-2 px-4 block text-xs text-gray-500" onClick={close}>
<a
className="py-2 px-4 block text-xs text-gray-500"
onClick={close}
>
<h4 className="text-sm font-bold">
<MarkText text={note.title} keyword={keyword}/>
<MarkText text={note.title} keyword={keyword} />
</h4>
<Breadcrumbs
maxItems={4}
@ -44,15 +47,21 @@ const SearchItem: FC<{
{getPaths(note)
.reverse()
.map((path) => (
<span key={path.id} className="px-0.5 py-0.5 text-xs truncate">
{path.title}
</span>
<span
key={path.id}
className="px-0.5 py-0.5 text-xs truncate"
>
{path.title}
</span>
))}
</Breadcrumbs>
<p className="mt-1">
<MarkText text={note.rawContent} keyword={keyword}/>
<MarkText text={note.rawContent} keyword={keyword} />
</p>
<time className="text-gray-500 mt-2 block" dateTime={note.date}>
<time
className="text-gray-500 mt-2 block"
dateTime={note.date}
>
{dayjs(note.date).format('DD/MM/YYYY HH:mm')}
</time>
</a>

View File

@ -10,16 +10,16 @@ import useI18n from 'libs/web/hooks/use-i18n';
import { useRouter } from 'next/router';
const SearchModal: FC = () => {
const {t} = useI18n();
const {filterNotes, keyword, list} = SearchState.useContainer();
const { t } = useI18n();
const { filterNotes, keyword, list } = SearchState.useContainer();
const {
search: {visible, close},
search: { visible, close },
} = PortalState.useContainer();
const router = useRouter();
const onEnter = useCallback(
(item: NoteModel) => {
router.push(`/${item.id}`, `/${item.id}`, {shallow: true});
router.push(`/${item.id}`, `/${item.id}`, { shallow: true });
close();
},
[router, close]
@ -37,7 +37,12 @@ const SearchModal: FC = () => {
onEnter={onEnter}
items={list ?? []}
ItemComponent={(item, props) => (
<SearchItem note={item} keyword={keyword} key={item.id} {...props} />
<SearchItem
note={item}
keyword={keyword}
key={item.id}
{...props}
/>
)}
/>
</FilterModal>

View File

@ -10,13 +10,13 @@ import useI18n from 'libs/web/hooks/use-i18n';
import UIState from 'libs/web/state/ui';
const ShareModal: FC = () => {
const {t} = useI18n();
const {share} = PortalState.useContainer();
const { t } = useI18n();
const { share } = PortalState.useContainer();
const [url, setUrl] = useState<string>();
const [copied, setCopied] = useState(false);
const {note, updateNote} = NoteState.useContainer();
const { note, updateNote } = NoteState.useContainer();
const router = useRouter();
const {disablePassword} = UIState.useContainer();
const { disablePassword } = UIState.useContainer();
const handleCopy = useCallback(() => {
url && navigator.clipboard.writeText(url);

View File

@ -26,11 +26,13 @@ interface ItemProps {
}
export const SidebarMenuItem = forwardRef<HTMLLIElement, ItemProps>(
({item}, ref) => {
const {settings: {settings}} = UIState.useContainer();
const {removeNote, mutateNote} = NoteState.useContainer();
({ item }, ref) => {
const {
menu: {close, data},
settings: { settings },
} = UIState.useContainer();
const { removeNote, mutateNote } = NoteState.useContainer();
const {
menu: { close, data },
} = PortalState.useContainer();
const doRemoveNote = useCallback(() => {
@ -70,7 +72,8 @@ export const SidebarMenuItem = forwardRef<HTMLLIElement, ItemProps>(
const toggleWidth = useCallback(() => {
close();
if (data?.id) {
const resolvedNoteWidth = data.editorsize ?? settings.editorsize;
const resolvedNoteWidth =
data.editorsize ?? settings.editorsize;
const editorSizesCount = Object.values(EDITOR_SIZE).length / 2; // contains both string & int values
mutateNote(data.id, {

View File

@ -13,26 +13,26 @@ import { NoteModel } from 'libs/shared/note';
import { Item, SidebarMenuItem, MENU_HANDLER_NAME } from './sidebar-menu-item';
const SidebarMenu: FC = () => {
const {t} = useI18n();
const { t } = useI18n();
const {
menu: {close, anchor, data, visible},
menu: { close, anchor, data, visible },
} = PortalState.useContainer();
const MENU_LIST: Item[] = useMemo(
() => [
{
text: t('Remove'),
icon: <TrashIcon/>,
icon: <TrashIcon />,
handler: MENU_HANDLER_NAME.REMOVE_NOTE,
},
{
text: t('Copy Link'),
icon: <ClipboardCopyIcon/>,
icon: <ClipboardCopyIcon />,
handler: MENU_HANDLER_NAME.COPY_LINK,
},
{
text: t('Add to Favorites'),
icon: <StarIcon/>,
icon: <StarIcon />,
handler: MENU_HANDLER_NAME.ADD_TO_FAVORITES,
enable(item?: NoteModel) {
return item?.pinned !== NOTE_PINNED.PINNED;
@ -40,7 +40,7 @@ const SidebarMenu: FC = () => {
},
{
text: t('Remove from Favorites'),
icon: <StarIcon/>,
icon: <StarIcon />,
handler: MENU_HANDLER_NAME.REMOVE_FROM_FAVORITES,
enable(item?: NoteModel) {
return item?.pinned === NOTE_PINNED.PINNED;
@ -49,7 +49,7 @@ const SidebarMenu: FC = () => {
{
text: t('Toggle width'),
// TODO: or SwitchHorizontal?
icon: <SelectorIcon style={{transform: "rotate(90deg)"}}/>,
icon: <SelectorIcon style={{ transform: 'rotate(90deg)' }} />,
handler: MENU_HANDLER_NAME.TOGGLE_WIDTH,
},
],
@ -67,9 +67,11 @@ const SidebarMenu: FC = () => {
>
{MENU_LIST.map((item) =>
item.enable ? (
item.enable(data) && <SidebarMenuItem key={item.text} item={item}/>
item.enable(data) && (
<SidebarMenuItem key={item.text} item={item} />
)
) : (
<SidebarMenuItem key={item.text} item={item}/>
<SidebarMenuItem key={item.text} item={item} />
)
)}
</Menu>

View File

@ -11,14 +11,14 @@ import classNames from 'classnames';
import useScrollView from 'libs/web/hooks/use-scroll-view';
const TrashItem: FC<{
note: NoteCacheItem
keyword?: string
selected?: boolean
}> = ({note, keyword, selected}) => {
const {t} = useI18n();
const {restoreNote, deleteNote, filterNotes} = TrashState.useContainer();
note: NoteCacheItem;
keyword?: string;
selected?: boolean;
}> = ({ note, keyword, selected }) => {
const { t } = useI18n();
const { restoreNote, deleteNote, filterNotes } = TrashState.useContainer();
const {
trash: {close},
trash: { close },
} = PortalState.useContainer();
const ref = useRef<HTMLLIElement>(null);
@ -37,14 +37,20 @@ const TrashItem: FC<{
return (
<li
ref={ref}
className={classNames('hover:bg-gray-200 cursor-pointer py-2 px-4 flex', {
'bg-gray-300': selected,
})}
className={classNames(
'hover:bg-gray-200 cursor-pointer py-2 px-4 flex',
{
'bg-gray-300': selected,
}
)}
>
<Link href={`/${note.id}`} shallow>
<a className=" block text-xs text-gray-500 flex-grow" onClick={close}>
<a
className=" block text-xs text-gray-500 flex-grow"
onClick={close}
>
<h4 className="text-sm font-bold">
<MarkText text={note.title} keyword={keyword}/>
<MarkText text={note.title} keyword={keyword} />
</h4>
</a>
</Link>

View File

@ -10,16 +10,16 @@ import { useRouter } from 'next/router';
import useI18n from 'libs/web/hooks/use-i18n';
const TrashModal: FC = () => {
const {t} = useI18n();
const {filterNotes, keyword, list} = TrashState.useContainer();
const { t } = useI18n();
const { filterNotes, keyword, list } = TrashState.useContainer();
const {
trash: {visible, close},
trash: { visible, close },
} = PortalState.useContainer();
const router = useRouter();
const onEnter = useCallback(
(item: NoteModel) => {
router.push(`/${item.id}`, `/${item.id}`, {shallow: true});
router.push(`/${item.id}`, `/${item.id}`, { shallow: true });
close();
},
[router, close]
@ -43,7 +43,12 @@ const TrashModal: FC = () => {
onEnter={onEnter}
items={list ?? []}
ItemComponent={(item, props) => (
<TrashItem note={item} keyword={keyword} key={item.id} {...props} />
<TrashItem
note={item}
keyword={keyword}
key={item.id}
{...props}
/>
)}
/>
</FilterModal>

View File

@ -14,12 +14,12 @@ const renderGutter = () => {
return gutter;
};
const Resizable: FC<{ width: number }> = ({width, children}) => {
const Resizable: FC<{ width: number }> = ({ width, children }) => {
const splitRef = useRef<typeof Split>(null);
const {
split: {saveSizes, resize, sizes},
ua: {isMobileOnly},
sidebar: {isFold},
split: { saveSizes, resize, sizes },
ua: { isMobileOnly },
sidebar: { isFold },
} = UIState.useContainer();
const lastWidthRef = useRef(width);

View File

@ -8,10 +8,10 @@ import useI18n from 'libs/web/hooks/use-i18n';
import { useTreeOptions, TreeOption } from 'libs/web/hooks/use-tree-options';
export const DailyNotes: FC = () => {
const {t} = useI18n();
const {tree} = NoteTreeState.useContainer();
const { t } = useI18n();
const { tree } = NoteTreeState.useContainer();
const {
settings: {settings, updateSettings},
settings: { settings, updateSettings },
} = UIState.useContainer();
const options = useTreeOptions(tree);
const defaultSelected = useMemo(
@ -23,7 +23,7 @@ export const DailyNotes: FC = () => {
const handleChange = useCallback(
(_event, item: TreeOption | null) => {
if (item) {
updateSettings({daily_root_id: item.id});
updateSettings({ daily_root_id: item.id });
setSelected(item);
}
},
@ -48,7 +48,9 @@ export const DailyNotes: FC = () => {
{...params}
{...defaultFieldConfig}
label={t('Daily notes are saved in')}
helperText={t('Daily notes will be created under this page')}
helperText={t(
'Daily notes will be created under this page'
)}
></TextField>
)}
></Autocomplete>

View File

@ -7,14 +7,14 @@ import UIState from 'libs/web/state/ui';
import { EDITOR_SIZE } from 'libs/shared/meta';
export const EditorWidth: FC = () => {
const {t} = useI18n();
const { t } = useI18n();
const {
settings: {settings, updateSettings},
settings: { settings, updateSettings },
} = UIState.useContainer();
const handleChange = useCallback(
async (event: ChangeEvent<HTMLInputElement>) => {
await updateSettings({editorsize: parseInt(event.target.value)});
await updateSettings({ editorsize: parseInt(event.target.value) });
router.reload();
},
[updateSettings]
@ -28,7 +28,9 @@ export const EditorWidth: FC = () => {
onChange={handleChange}
select
>
<MenuItem value={EDITOR_SIZE.SMALL}>{t('Small (default)')}</MenuItem>
<MenuItem value={EDITOR_SIZE.SMALL}>
{t('Small (default)')}
</MenuItem>
<MenuItem value={EDITOR_SIZE.LARGE}>{t('Large')}</MenuItem>
</TextField>
);

View File

@ -5,8 +5,8 @@ import { ROOT_ID } from 'libs/shared/tree';
import Link from 'next/link';
import { ButtonProgress } from 'components/button-progress';
export const ExportButton: FC<ButtonProps> = ({parentId = ROOT_ID}) => {
const {t} = useI18n();
export const ExportButton: FC<ButtonProps> = ({ parentId = ROOT_ID }) => {
const { t } = useI18n();
const [loading, setLoading] = useState(false);
// Fake waiting time

View File

@ -8,9 +8,9 @@ import { IMPORT_FILE_LIMIT_SIZE } from 'libs/shared/const';
import { useRouter } from 'next/router';
import { ROOT_ID } from 'libs/shared/tree';
export const ImportButton: FC<ButtonProps> = ({parentId = ROOT_ID}) => {
const {t} = useI18n();
const {request, loading, error} = useFetcher();
export const ImportButton: FC<ButtonProps> = ({ parentId = ROOT_ID }) => {
const { t } = useI18n();
const { request, loading, error } = useFetcher();
const toast = useToast();
const router = useRouter();
@ -35,8 +35,10 @@ export const ImportButton: FC<ButtonProps> = ({parentId = ROOT_ID}) => {
data.append('file', file);
const result = await request<FormData,
{ total: number; imported: number }>(
const result = await request<
FormData,
{ total: number; imported: number }
>(
{
method: 'POST',
url: '/api/import?pid=' + parentId,

View File

@ -9,8 +9,8 @@ import { ImportButton } from './import-button';
import { useTreeOptions, TreeOption } from 'libs/web/hooks/use-tree-options';
export const ImportOrExport: FC = () => {
const {t} = useI18n();
const {tree} = NoteTreeState.useContainer();
const { t } = useI18n();
const { tree } = NoteTreeState.useContainer();
const options = useTreeOptions(tree);
const [selected, setSelected] = useState(options[0]);

View File

@ -8,14 +8,14 @@ import { configLocale, Locale } from 'locales';
import { map } from 'lodash';
export const Language: FC = () => {
const {t, activeLocale} = useI18n();
const { t, activeLocale } = useI18n();
const {
settings: {settings, updateSettings},
settings: { settings, updateSettings },
} = UIState.useContainer();
const handleChange = useCallback(
async (event: ChangeEvent<HTMLInputElement>) => {
await updateSettings({locale: event.target.value as Locale});
await updateSettings({ locale: event.target.value as Locale });
router.reload();
},
[updateSettings]

View File

@ -14,7 +14,11 @@ export const SettingFooter = () => {
</div>
<div className="space-x-1">
<span>MIT &copy;</span>
<a href="https://github.com/qingwei-li" target="_blank" rel="noreferrer">
<a
href="https://github.com/qingwei-li"
target="_blank"
rel="noreferrer"
>
Cinwell
</a>
<span>2021-2022</span>

View File

@ -23,11 +23,11 @@ export const defaultFieldConfig: TextFieldProps = {
};
const HR = () => {
return <hr className="my-10 border-gray-200"/>;
return <hr className="my-10 border-gray-200" />;
};
export const SettingsContainer: FC = () => {
const {t} = useI18n();
const { t } = useI18n();
return (
<section>
@ -36,7 +36,7 @@ export const SettingsContainer: FC = () => {
<Language></Language>
<Theme></Theme>
<EditorWidth></EditorWidth>
<HR/>
<HR />
<SettingsHeader
id="import-and-export"
title={t('Import & Export')}
@ -46,7 +46,7 @@ export const SettingsContainer: FC = () => {
></SettingsHeader>
<ImportOrExport></ImportOrExport>
<HR/>
<HR />
<SettingsHeader id="sharing" title={t('Sharing')}></SettingsHeader>
<SnippetInjection></SnippetInjection>
</section>

View File

@ -1,10 +1,10 @@
import { FC } from 'react';
export const SettingsHeader: FC<{
title: string
id: string
description?: string
}> = ({title, id, description}) => {
title: string;
id: string;
description?: string;
}> = ({ title, id, description }) => {
return (
<>
<h3 className="my-2" id={id}>

View File

@ -8,9 +8,9 @@ import Link from 'next/link';
import { QuestionMarkCircleIcon } from '@heroicons/react/outline';
export const SnippetInjection: FC = () => {
const {t} = useI18n();
const { t } = useI18n();
const {
settings: {settings, updateSettings, setSettings},
settings: { settings, updateSettings, setSettings },
IS_DEMO,
} = UIState.useContainer();
@ -25,15 +25,15 @@ export const SnippetInjection: FC = () => {
const updateValue = useCallback(
(event: ChangeEvent<HTMLTextAreaElement>) => {
setSettings((prev) => ({...prev, injection: event.target.value}));
setSettings((prev) => ({ ...prev, injection: event.target.value }));
},
[setSettings]
);
useEffect(() => {
if (IS_DEMO && settings.injection !== DEMO_INJECTION) {
updateSettings({injection: DEMO_INJECTION});
setSettings((prev) => ({...prev, injection: DEMO_INJECTION}));
updateSettings({ injection: DEMO_INJECTION });
setSettings((prev) => ({ ...prev, injection: DEMO_INJECTION }));
}
}, [settings.injection, IS_DEMO, updateSettings, setSettings]);
@ -51,17 +51,20 @@ export const SnippetInjection: FC = () => {
rows={8}
helperText={
<span className="flex items-center">
<span>
{t(
'Inject analytics or other scripts into the HTML of your sharing page. '
) + (IS_DEMO ? t('Disable editing in the demo.') : '')}
</span>
<Link href="https://github.com/QingWei-Li/notea/wiki/Snippet-Injection">
<a target="_blank" rel="noreferrer">
<QuestionMarkCircleIcon className="w-4 text-gray-500 hover:text-gray-700"/>
</a>
</Link>
</span>
<span>
{t(
'Inject analytics or other scripts into the HTML of your sharing page. '
) +
(IS_DEMO
? t('Disable editing in the demo.')
: '')}
</span>
<Link href="https://github.com/QingWei-Li/notea/wiki/Snippet-Injection">
<a target="_blank" rel="noreferrer">
<QuestionMarkCircleIcon className="w-4 text-gray-500 hover:text-gray-700" />
</a>
</Link>
</span>
}
></TextField>
</div>

View File

@ -6,8 +6,8 @@ import { useTheme } from 'next-themes';
import useMounted from 'libs/web/hooks/use-mounted';
export const Theme: FC = () => {
const {t} = useI18n();
const {theme, setTheme} = useTheme();
const { t } = useI18n();
const { theme, setTheme } = useTheme();
const mounted = useMounted();
const handleChange = useCallback(

View File

@ -9,30 +9,36 @@ import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import SidebarListItem from './sidebar-list-item';
export const Favorites: FC = () => {
const {t} = useI18n();
const {pinnedTree} = NoteTreeState.useContainer();
const { t } = useI18n();
const { pinnedTree } = NoteTreeState.useContainer();
const [tree, setTree] = useState(pinnedTree);
const [isFold, setFold] = useState(false);
const hasPinned = useMemo(() => tree.items[ROOT_ID].children.length, [tree]);
const hasPinned = useMemo(() => tree.items[ROOT_ID].children.length, [
tree,
]);
const onCollapse = useCallback((id) => {
setTree((prev) => TreeActions.mutateItem(prev, id, {isExpanded: false}));
setTree((prev) =>
TreeActions.mutateItem(prev, id, { isExpanded: false })
);
}, []);
const onExpand = useCallback((id) => {
setTree((prev) => TreeActions.mutateItem(prev, id, {isExpanded: true}));
setTree((prev) =>
TreeActions.mutateItem(prev, id, { isExpanded: true })
);
}, []);
useEffect(() => {
const items = cloneDeep(pinnedTree.items);
setTree((prev) => {
if (!prev) return {...pinnedTree, items};
if (!prev) return { ...pinnedTree, items };
forEach(items, (item) => {
item.isExpanded = prev.items[item.id]?.isExpanded ?? false;
});
return {...pinnedTree, items};
return { ...pinnedTree, items };
});
}, [pinnedTree]);
@ -62,12 +68,12 @@ export const Favorites: FC = () => {
tree={tree}
offsetPerLevel={10}
renderItem={({
provided,
item,
onExpand,
onCollapse,
snapshot,
}) => (
provided,
item,
onExpand,
onCollapse,
snapshot,
}) => (
<SidebarListItem
{...provided.draggableProps}
{...provided.dragHandleProps}

View File

@ -23,39 +23,39 @@ const TextSkeleton = () => (
);
const SidebarListItem: FC<{
item: NoteModel
innerRef: (el: HTMLElement | null) => void
onExpand: (itemId?: ReactText) => void
onCollapse: (itemId?: ReactText) => void
isExpanded: boolean
hasChildren: boolean
item: NoteModel;
innerRef: (el: HTMLElement | null) => void;
onExpand: (itemId?: ReactText) => void;
onCollapse: (itemId?: ReactText) => void;
isExpanded: boolean;
hasChildren: boolean;
snapshot: {
isDragging: boolean
}
isDragging: boolean;
};
style?: {
paddingLeft: number
}
paddingLeft: number;
};
}> = ({
item,
innerRef,
onExpand,
onCollapse,
isExpanded,
snapshot,
hasChildren,
...attrs
}) => {
const {t} = useI18n();
const {query} = useRouter();
const {mutateItem, initLoaded} = NoteTreeState.useContainer();
item,
innerRef,
onExpand,
onCollapse,
isExpanded,
snapshot,
hasChildren,
...attrs
}) => {
const { t } = useI18n();
const { query } = useRouter();
const { mutateItem, initLoaded } = NoteTreeState.useContainer();
const {
menu: {open, setData, setAnchor},
menu: { open, setData, setAnchor },
} = PortalState.useContainer();
const onAddNote = useCallback(
(e: MouseEvent) => {
e.preventDefault();
router.push(`/new?pid=` + item.id, undefined, {shallow: true});
router.push(`/new?pid=` + item.id, undefined, { shallow: true });
mutateItem(item.id, {
isExpanded: true,
});
@ -109,8 +109,8 @@ const SidebarListItem: FC<{
'block p-0.5 cursor-pointer w-7 h-7 md:w-6 md:h-6 rounded hover:bg-gray-400 mr-1 text-center'
)}
>
{emoji}
</span>
{emoji}
</span>
) : (
<IconButton
className="mr-1"
@ -118,22 +118,25 @@ const SidebarListItem: FC<{
hasChildren || isExpanded
? 'ChevronRight'
: item.title
? 'DocumentText'
: 'Document'
? 'DocumentText'
: 'Document'
}
iconClassName={classNames('transition-transform transform', {
'rotate-90': isExpanded,
})}
iconClassName={classNames(
'transition-transform transform',
{
'rotate-90': isExpanded,
}
)}
onClick={handleClickIcon}
></IconButton>
)}
<span className="flex-1 truncate" dir="auto">
{(emoji
? item.title.replace(emoji, '').trimLeft()
: item.title) ||
(initLoaded ? t('Untitled') : <TextSkeleton/>)}
</span>
{(emoji
? item.title.replace(emoji, '').trimLeft()
: item.title) ||
(initLoaded ? t('Untitled') : <TextSkeleton />)}
</span>
</a>
</Link>
@ -161,7 +164,7 @@ const SidebarListItem: FC<{
paddingLeft: attrs.style?.paddingLeft,
}}
>
{initLoaded ? t('No notes inside') : <TextSkeleton/>}
{initLoaded ? t('No notes inside') : <TextSkeleton />}
</div>
)}
</>

View File

@ -10,13 +10,13 @@ import { CircularProgress, Tooltip } from '@material-ui/core';
import { Favorites } from './favorites';
const SideBarList = () => {
const {t} = useI18n();
const { t } = useI18n();
const {
tree,
moveItem,
mutateItem,
initLoaded,
collapseAllItems
collapseAllItems,
} = NoteTreeState.useContainer();
const onExpand = useCallback(
@ -51,20 +51,24 @@ const SideBarList = () => {
);
const onCreateNote = useCallback(() => {
router.push('/new', undefined, {shallow: true});
router.push('/new', undefined, { shallow: true });
}, []);
return (
<section className="h-full flex text-sm flex-col flex-grow bg-gray-100 overflow-y-auto">
{/* Favorites */}
<Favorites/>
<Favorites />
{/* My Pages */}
<div className="p-2 text-gray-500 flex items-center sticky top-0 bg-gray-100 z-10">
<div className="flex-auto flex items-center">
<span>{t('My Pages')}</span>
{initLoaded ? null : (
<CircularProgress className="ml-4" size="14px" color="inherit"/>
<CircularProgress
className="ml-4"
size="14px"
color="inherit"
/>
)}
</div>
<Tooltip title={t('Collapse all pages')}>
@ -96,7 +100,13 @@ const SideBarList = () => {
isDragEnabled
isNestingEnabled
offsetPerLevel={10}
renderItem={({provided, item, onExpand, onCollapse, snapshot}) => (
renderItem={({
provided,
item,
onExpand,
onCollapse,
snapshot,
}) => (
<SidebarListItem
{...provided.draggableProps}
{...provided.dragHandleProps}

View File

@ -19,7 +19,7 @@ import { useRouter } from 'next/router';
const ButtonItem = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
(props, ref) => {
const {children, className, ...attrs} = props;
const { children, className, ...attrs } = props;
return (
<div
{...attrs}
@ -36,9 +36,9 @@ const ButtonItem = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
);
const ButtonMenu = () => {
const {t} = useI18n();
const { t } = useI18n();
const {
sidebar: {toggle, isFold},
sidebar: { toggle, isFold },
} = UIState.useContainer();
const onFold = useCallback(() => {
toggle();
@ -63,8 +63,8 @@ const ButtonMenu = () => {
};
const ButtonSearch = () => {
const {t} = useI18n();
const {search} = PortalState.useContainer();
const { t } = useI18n();
const { search } = PortalState.useContainer();
return (
<HotkeyTooltip
@ -74,15 +74,15 @@ const ButtonSearch = () => {
keys={['P']}
>
<ButtonItem onClick={search.open} aria-label="search">
<SearchIcon/>
<SearchIcon />
</ButtonItem>
</HotkeyTooltip>
);
};
const ButtonTrash = () => {
const {t} = useI18n();
const {trash} = PortalState.useContainer();
const { t } = useI18n();
const { trash } = PortalState.useContainer();
return (
<HotkeyTooltip
@ -93,14 +93,14 @@ const ButtonTrash = () => {
keys={['T']}
>
<ButtonItem onClick={trash.open} aria-label="trash">
<TrashIcon/>
<TrashIcon />
</ButtonItem>
</HotkeyTooltip>
);
};
const ButtonDailyNotes = () => {
const {t} = useI18n();
const { t } = useI18n();
const href = `/${dayjs().format('YYYY-MM-DD')}`;
const router = useRouter();
@ -110,11 +110,11 @@ const ButtonDailyNotes = () => {
<HotkeyTooltip
text={t('Daily Notes')}
commandKey
onHotkey={() => router.push(href, href, {shallow: true})}
onHotkey={() => router.push(href, href, { shallow: true })}
keys={['shift', 'O']}
>
<ButtonItem aria-label="daily notes">
<InboxIcon/>
<InboxIcon />
</ButtonItem>
</HotkeyTooltip>
</a>
@ -123,14 +123,14 @@ const ButtonDailyNotes = () => {
};
const ButtonSettings = () => {
const {t} = useI18n();
const { t } = useI18n();
return (
<Link href="/settings" shallow>
<a>
<HotkeyTooltip text={t('Settings')}>
<ButtonItem aria-label="settings">
<CogIcon/>
<CogIcon />
</ButtonItem>
</HotkeyTooltip>
</a>
@ -143,9 +143,9 @@ const SidebarTool = () => {
return (
<aside className="h-full flex flex-col w-12 md:w-11 flex-none bg-gray-200">
<ButtonSearch/>
<ButtonTrash/>
<ButtonDailyNotes/>
<ButtonSearch />
<ButtonTrash />
<ButtonDailyNotes />
<div className="tool mt-auto">
{mounted ? (
@ -156,10 +156,10 @@ const SidebarTool = () => {
<ButtonMenu></ButtonMenu>
<ButtonSettings></ButtonSettings>
<style jsx>{`
.tool :global(.HW_softHidden) {
display: none;
}
`}</style>
.tool :global(.HW_softHidden) {
display: none;
}
`}</style>
</div>
</aside>
);

View File

@ -5,20 +5,20 @@ import { FC, useEffect } from 'react';
import NoteTreeState from 'libs/web/state/tree';
const Sidebar: FC = () => {
const {ua} = UIState.useContainer();
const {initTree} = NoteTreeState.useContainer();
const { ua } = UIState.useContainer();
const { initTree } = NoteTreeState.useContainer();
useEffect(() => {
initTree();
}, [initTree]);
return ua?.isMobileOnly ? <MobileSidebar/> : <BrowserSidebar/>;
return ua?.isMobileOnly ? <MobileSidebar /> : <BrowserSidebar />;
};
const BrowserSidebar: FC = () => {
const {
sidebar,
split: {sizes},
split: { sizes },
} = UIState.useContainer();
return (
@ -28,17 +28,17 @@ const BrowserSidebar: FC = () => {
width: `calc(${sizes[0]}% - 5px)`,
}}
>
<SidebarTool/>
{sidebar.isFold ? null : <SideBarList/>}
<SidebarTool />
{sidebar.isFold ? null : <SideBarList />}
</section>
);
};
const MobileSidebar: FC = () => {
return (
<section className="flex h-full" style={{width: '80vw'}}>
<SidebarTool/>
<SideBarList/>
<section className="flex h-full" style={{ width: '80vw' }}>
<SidebarTool />
<SideBarList />
</section>
);
};

View File

@ -1,12 +1,12 @@
version: '2'
services:
minio:
image: minio/minio
ports:
- '9000:9000'
environment:
MINIO_ACCESS_KEY: minio
MINIO_SECRET_KEY: minio123
entrypoint: sh
command: -c 'mkdir -p /data/notea && mkdir -p /data/notea-test && /usr/bin/minio server /data'
minio:
image: minio/minio
ports:
- '9000:9000'
environment:
MINIO_ACCESS_KEY: minio
MINIO_SECRET_KEY: minio123
entrypoint: sh
command: -c 'mkdir -p /data/notea && mkdir -p /data/notea-test && /usr/bin/minio server /data'

View File

@ -1,4 +1,4 @@
require('dotenv').config({path: '.env.test'});
require('dotenv').config({ path: '.env.test' });
module.exports = {
collectCoverageFrom: ['**/*.{ts}', '!**/*.d.ts', '!**/node_modules/**'],
@ -7,7 +7,7 @@ module.exports = {
transform: {
/* Use babel-jest to transpile tests with the next/babel preset
https://jestjs.io/docs/configuration#transform-objectstring-pathtotransformer--pathtotransformer-object */
'^.+\\.(ts)$': ['babel-jest', {presets: ['next/babel']}],
'^.+\\.(ts)$': ['babel-jest', { presets: ['next/babel'] }],
},
transformIgnorePatterns: [
'/node_modules/',

View File

@ -1,15 +1,22 @@
import { NextApiRequest } from "next";
import { BasicAuthConfiguration, config, Configuration } from "libs/server/config";
import { NextApiRequest } from 'next';
import {
BasicAuthConfiguration,
config,
Configuration,
} from 'libs/server/config';
export const NO_UID = '' as const;
export type AuthenticationData = { uid: string; };
export function basicAuthenticate(request: NextApiRequest): AuthenticationData | false {
export type AuthenticationData = { uid: string };
export function basicAuthenticate(
request: NextApiRequest
): AuthenticationData | false {
const cfg = config().auth as BasicAuthConfiguration;
if (cfg.users) {
// Multi-user configuration takes precedence over single-user
const { username, password } = request.body;
if (!username || !password) {
throw new Error("Username and password must be specified");
throw new Error('Username and password must be specified');
}
for (const user of cfg.users) {
if (user.username !== username) {
@ -20,7 +27,7 @@ export function basicAuthenticate(request: NextApiRequest): AuthenticationData |
}
return { uid: username };
}
return false
return false;
} else {
if (!!cfg.username && cfg.username !== request.body.username) {
return false;
@ -31,8 +38,11 @@ export function basicAuthenticate(request: NextApiRequest): AuthenticationData |
return { uid: NO_UID };
}
}
// NOTE(tecc): It's async in case we want to expand later
export async function authenticate(request: NextApiRequest): Promise<false | AuthenticationData> {
export async function authenticate(
request: NextApiRequest
): Promise<false | AuthenticationData> {
const cfg = config();
switch (cfg.auth.type) {
case 'none':
@ -42,6 +52,10 @@ export async function authenticate(request: NextApiRequest): Promise<false | Aut
default:
// NOTE(tecc): Weird hack here to get around type restrictions
throw new Error(`Cannot authenticate against authentication type ${(cfg as Configuration).auth.type}`);
throw new Error(
`Cannot authenticate against authentication type ${
(cfg as Configuration).auth.type
}`
);
}
}

View File

@ -1,13 +1,20 @@
import yaml from 'js-yaml';
import { getEnv } from "libs/shared/env";
import { existsSync, readFileSync } from "fs";
import { getEnv } from 'libs/shared/env';
import { existsSync, readFileSync } from 'fs';
export type BasicUser = { username: string; password: string };
type BasicMultiUserConfiguration = { username?: never; password?: never; users: BasicUser[] };
type BasicSingleUserConfiguration = ({ username?: string; password: string }) & { users?: never };
export type BasicAuthConfiguration =
{ type: 'basic' }
& (BasicSingleUserConfiguration | BasicMultiUserConfiguration)
type BasicMultiUserConfiguration = {
username?: never;
password?: never;
users: BasicUser[];
};
type BasicSingleUserConfiguration = { username?: string; password: string } & {
users?: never;
};
export type BasicAuthConfiguration = { type: 'basic' } & (
| BasicSingleUserConfiguration
| BasicMultiUserConfiguration
);
export type AuthConfiguration = { type: 'none' } | BasicAuthConfiguration;
export interface S3StoreConfiguration {
@ -38,30 +45,30 @@ export function loadConfig() {
if (existsSync(configFile)) {
const data = readFileSync(configFile, 'utf-8');
baseConfig = yaml.load(data) as Configuration;
}
const disablePassword = getEnv<boolean>("DISABLE_PASSWORD", undefined);
const disablePassword = getEnv<boolean>('DISABLE_PASSWORD', undefined);
let auth: AuthConfiguration;
if (disablePassword === undefined || !disablePassword) {
const envPassword = getEnv<string>("PASSWORD", undefined, false);
const envPassword = getEnv<string>('PASSWORD', undefined, false);
if (baseConfig.auth === undefined) {
if (envPassword === undefined) {
throw new Error("Authentication undefined");
throw new Error('Authentication undefined');
} else {
auth = {
type: 'basic',
password: envPassword
}
password: envPassword,
};
}
} else {
auth = baseConfig.auth;
if (envPassword !== undefined) {
throw new Error("Cannot specify PASSWORD when auth config section is present")
throw new Error(
'Cannot specify PASSWORD when auth config section is present'
);
}
}
} else {
auth = { type: 'none' };
}
@ -75,19 +82,47 @@ export function loadConfig() {
}
// for now, this works
{
store.accessKey = getEnv<string>("STORE_ACCESS_KEY", store.accessKey, !store.accessKey).toString();
store.secretKey = getEnv<string>("STORE_SECRET_KEY", store.secretKey, !store.secretKey).toString();
store.bucket = getEnv<string>("STORE_BUCKET", store.bucket ?? "notea", false).toString();
store.forcePathStyle = getEnv<boolean>("STORE_FORCE_PATH_STYLE", store.forcePathStyle ?? false, !store.forcePathStyle);
store.endpoint = getEnv<string>("STORE_END_POINT", store.endpoint, !store.endpoint);
store.region = getEnv<string>("STORE_REGION", store.region ?? 'us-east-1', false).toString();
store.prefix = getEnv<string>("STORE_PREFIX", store.prefix ?? '', false);
store.accessKey = getEnv<string>(
'STORE_ACCESS_KEY',
store.accessKey,
!store.accessKey
).toString();
store.secretKey = getEnv<string>(
'STORE_SECRET_KEY',
store.secretKey,
!store.secretKey
).toString();
store.bucket = getEnv<string>(
'STORE_BUCKET',
store.bucket ?? 'notea',
false
).toString();
store.forcePathStyle = getEnv<boolean>(
'STORE_FORCE_PATH_STYLE',
store.forcePathStyle ?? false,
!store.forcePathStyle
);
store.endpoint = getEnv<string>(
'STORE_END_POINT',
store.endpoint,
!store.endpoint
);
store.region = getEnv<string>(
'STORE_REGION',
store.region ?? 'us-east-1',
false
).toString();
store.prefix = getEnv<string>(
'STORE_PREFIX',
store.prefix ?? '',
false
);
}
loaded = {
auth,
store,
baseUrl: getEnv<string>("BASE_URL")?.toString() ?? baseConfig.baseUrl
baseUrl: getEnv<string>('BASE_URL')?.toString() ?? baseConfig.baseUrl,
};
}
@ -97,4 +132,4 @@ export function config(): Configuration {
}
return loaded as Configuration;
}
}

View File

@ -39,17 +39,17 @@ export interface ServerProps {
}
export type ApiRequest = NextApiRequest & {
session: Session
state: ServerState
props: ServerProps
redirect: Redirect
}
session: Session;
state: ServerState;
props: ServerProps;
redirect: Redirect;
};
export type ApiResponse = NextApiResponse & {
APIError: typeof API
}
APIError: typeof API;
};
export type ApiNext = () => void
export type ApiNext = () => void;
export const api = () =>
nc<ApiRequest, ApiResponse>({
@ -74,8 +74,8 @@ export const ssr = () =>
.use(useStore);
export type SSRContext = GetServerSidePropsContext & {
req: ApiRequest
res: ApiResponse
}
req: ApiRequest;
res: ApiResponse;
};
export type SSRMiddleware = Middleware<ApiRequest, ApiResponse>
export type SSRMiddleware = Middleware<ApiRequest, ApiResponse>;

View File

@ -1,6 +1,6 @@
import { PageMode } from 'libs/shared/page';
import { ApiRequest, ApiResponse, ApiNext, SSRMiddleware } from '../connect';
import { config } from "libs/server/config";
import { config } from 'libs/server/config';
export async function useAuth(
req: ApiRequest,

View File

@ -47,8 +47,8 @@ export class APIError {
throw(message?: string) {
const error = new Error(message === undefined ? this.message : message);
error.name = this.prefix + this.name
;(error as any).status = this.status;
error.name = this.prefix + this.name;
(error as any).status = this.status;
throw error;
}

View File

@ -3,7 +3,7 @@ import { NOTE_SHARED } from 'libs/shared/meta';
import { getNote } from 'pages/api/notes/[id]';
import { SSRMiddleware } from '../connect';
import { NoteModel } from 'libs/shared/note';
import { config } from "libs/server/config";
import { config } from 'libs/server/config';
const RESERVED_ROUTES = ['new', 'settings', 'login'];
@ -13,8 +13,8 @@ export const applyNote: (id: string) => SSRMiddleware = (id: string) => async (
next
) => {
const props: {
note?: NoteModel
pageMode: PageMode
note?: NoteModel;
pageMode: PageMode;
} = {
pageMode: PageMode.NOTE,
};

View File

@ -1,6 +1,6 @@
import { SSRMiddleware } from '../connect';
const {resetServerContext} = require('react-beautiful-dnd-next');
const { resetServerContext } = require('react-beautiful-dnd-next');
export const applyReset: SSRMiddleware = async (_req, _res, next) => {
resetServerContext();

View File

@ -13,7 +13,7 @@ export const applySettings: SSRMiddleware = async (req, _res, next) => {
req.props = {
...req.props,
...{settings, lngDict},
...{ settings, lngDict },
};
next();
};

View File

@ -14,7 +14,7 @@ export const applyTree: SSRMiddleware = async (req, res, next) => {
req.props = {
...req.props,
...(tree && {tree}),
...(tree && { tree }),
};
next();

View File

@ -5,7 +5,7 @@ import { getPathNoteById } from 'libs/server/note-path';
import { ServerState } from './connect';
export const createNote = async (note: NoteModel, state: ServerState) => {
const {content = '\n', ...meta} = note;
const { content = '\n', ...meta } = note;
if (!note.id) {
note.id = genId();

View File

@ -1,6 +1,6 @@
import { StoreS3 } from './providers/s3';
import { StoreProvider } from "./providers/base";
import { config } from "libs/server/config";
import { StoreProvider } from './providers/base';
import { config } from 'libs/server/config';
export function createStore(): StoreProvider {
const cfg = config().store;

View File

@ -6,14 +6,14 @@ export interface ObjectOptions {
meta?: { [key: string]: string };
contentType?: string;
headers?: {
cacheControl?: string
contentDisposition?: string
contentEncoding?: string
cacheControl?: string;
contentDisposition?: string;
contentEncoding?: string;
};
}
export abstract class StoreProvider {
constructor({prefix}: StoreProviderConfig) {
constructor({ prefix }: StoreProviderConfig) {
this.prefix = prefix?.replace(/\/$/, '');
if (this.prefix) {
@ -30,12 +30,12 @@ export abstract class StoreProvider {
/**
* URL
*/
abstract getSignUrl(path: string, expires: number): Promise<string | null>
abstract getSignUrl(path: string, expires: number): Promise<string | null>;
/**
*
*/
abstract hasObject(path: string): Promise<boolean>
abstract hasObject(path: string): Promise<boolean>;
/**
*
@ -44,7 +44,7 @@ export abstract class StoreProvider {
abstract getObject(
path: string,
isCompressed?: boolean
): Promise<string | undefined>
): Promise<string | undefined>;
/**
* Meta
@ -52,7 +52,7 @@ export abstract class StoreProvider {
*/
abstract getObjectMeta(
path: string
): Promise<{ [key: string]: string } | undefined>
): Promise<{ [key: string]: string } | undefined>;
/**
* Meta
@ -62,11 +62,11 @@ export abstract class StoreProvider {
path: string,
isCompressed?: boolean
): Promise<{
content?: string
meta?: { [key: string]: string }
contentType?: string
buffer?: Buffer
}>
content?: string;
meta?: { [key: string]: string };
contentType?: string;
buffer?: Buffer;
}>;
/**
*
@ -76,12 +76,12 @@ export abstract class StoreProvider {
raw: string | Buffer,
headers?: ObjectOptions,
isCompressed?: boolean
): Promise<void>
): Promise<void>;
/**
*
*/
abstract deleteObject(path: string): Promise<void>
abstract deleteObject(path: string): Promise<void>;
/**
* meta
@ -90,5 +90,5 @@ export abstract class StoreProvider {
fromPath: string,
toPath: string,
options: ObjectOptions
): Promise<void>
): Promise<void>;
}

View File

@ -65,7 +65,10 @@ export class StoreS3 extends StoreProvider {
port: toNumber(url.port),
});
return client.presignedGetObject(this.config.bucket, this.getPath(path));
return client.presignedGetObject(
this.config.bucket,
this.getPath(path)
);
}
}
@ -75,7 +78,7 @@ export class StoreS3 extends StoreProvider {
Bucket: this.config.bucket,
Key: this.getPath(path),
}),
{expiresIn: expires}
{ expiresIn: expires }
);
}

View File

@ -22,7 +22,9 @@ import { getPathTree } from './note-path';
function fixedTree(tree: TreeModel) {
forEach(tree.items, (item) => {
if (
item.children.find((i) => i === null || i === item.id || !tree.items[i])
item.children.find(
(i) => i === null || i === item.id || !tree.items[i]
)
) {
console.log('item.children error', item);
tree.items[item.id] = {

View File

@ -12,7 +12,7 @@ type AllowedEnvs =
| 'DIRECT_RESPONSE_ATTACHMENT'
| 'IS_DEMO'
| 'STORE_PREFIX'
| 'CONFIG_FILE'
| 'CONFIG_FILE';
export function getEnv<T>(
env: AllowedEnvs,

View File

@ -5,11 +5,11 @@ export const parseMarkdownTitle = (markdown: string) => {
const matches = markdown.match(/^#[^#][\s]*(.+?)#*?$/m);
if (matches && matches.length) {
const title = matches[1]
const title = matches[1];
return {
content: markdown.replace(matches[0], ''),
title: title.length > 0 ? title : undefined,
};
}
return {content: markdown, title: undefined};
return { content: markdown, title: undefined };
};

View File

@ -30,6 +30,11 @@ export const PAGE_META_KEY = <const>[
'editorsize',
];
export type metaKey = typeof PAGE_META_KEY[number]
export type metaKey = typeof PAGE_META_KEY[number];
export const NUMBER_KEYS: metaKey[] = ['deleted', 'shared', 'pinned', 'editorsize'];
export const NUMBER_KEYS: metaKey[] = [
'deleted',
'shared',
'pinned',
'editorsize',
];

View File

@ -22,7 +22,7 @@ export const DEFAULT_SETTINGS: Settings = Object.freeze({
});
export function formatSettings(body: Record<string, any> = {}) {
const settings: Settings = {...DEFAULT_SETTINGS};
const settings: Settings = { ...DEFAULT_SETTINGS };
if (isString(body.daily_root_id)) {
settings.daily_root_id = body.daily_root_id;

View File

@ -121,10 +121,14 @@ const flattenTree = (
);
};
export type HierarchicalTreeItemModel = Omit<TreeItemModel, "children"> & {
export type HierarchicalTreeItemModel = Omit<TreeItemModel, 'children'> & {
children: HierarchicalTreeItemModel[];
}
export function makeHierarchy(tree: TreeModel, rootId = tree.rootId): HierarchicalTreeItemModel | false {
};
export function makeHierarchy(
tree: TreeModel,
rootId = tree.rootId
): HierarchicalTreeItemModel | false {
if (!tree.items[rootId]) {
return false;
}
@ -133,7 +137,9 @@ export function makeHierarchy(tree: TreeModel, rootId = tree.rootId): Hierarchic
return {
...root,
children: root.children.map((v) => makeHierarchy(tree, v)).filter((v) => !!v) as HierarchicalTreeItemModel[]
children: root.children
.map((v) => makeHierarchy(tree, v))
.filter((v) => !!v) as HierarchicalTreeItemModel[],
};
}
@ -145,7 +151,7 @@ const TreeActions = {
restoreItem,
deleteItem,
flattenTree,
makeHierarchy
makeHierarchy,
};
export default TreeActions;

View File

@ -31,7 +31,7 @@ export default function useFetcher() {
};
init.headers = {
...(csrfToken && {[CSRF_HEADER_KEY]: csrfToken}),
...(csrfToken && { [CSRF_HEADER_KEY]: csrfToken }),
};
if (payload instanceof FormData) {
@ -72,5 +72,5 @@ export default function useFetcher() {
abortRef.current?.abort();
}, []);
return {loading, request, abort, error};
return { loading, request, abort, error };
}

View File

@ -4,7 +4,7 @@ import noteCache from '../cache/note';
import useFetcher from './fetcher';
export default function useNoteAPI() {
const {loading, request, abort, error} = useFetcher();
const { loading, request, abort, error } = useFetcher();
const find = useCallback(
async (id: string) => {
@ -33,19 +33,19 @@ export default function useNoteAPI() {
async (id: string, body: Partial<NoteModel>) => {
const data = body.content
? await request<Partial<NoteModel>, NoteModel>(
{
method: 'POST',
url: `/api/notes/${id}`,
},
body
)
{
method: 'POST',
url: `/api/notes/${id}`,
},
body
)
: await request<Partial<NoteModel>, NoteModel>(
{
method: 'POST',
url: `/api/notes/${id}/meta`,
},
body
);
{
method: 'POST',
url: `/api/notes/${id}/meta`,
},
body
);
return data;
},

View File

@ -3,7 +3,7 @@ import useFetcher from './fetcher';
import { Settings } from 'libs/shared/settings';
export default function useSettingsAPI() {
const {request} = useFetcher();
const { request } = useFetcher();
const mutate = useCallback(
async (body: Partial<Settings>) => {

View File

@ -7,7 +7,7 @@ interface MutateBody {
}
export default function useTrashAPI() {
const {loading, request, abort} = useFetcher();
const { loading, request, abort } = useFetcher();
const mutate = useCallback(
async (body: MutateBody) => {

View File

@ -8,7 +8,7 @@ interface MutateBody {
}
export default function useTreeAPI() {
const {loading, request, abort} = useFetcher();
const { loading, request, abort } = useFetcher();
const mutate = useCallback(
async (body: MutateBody) => {

View File

@ -1,6 +1,6 @@
import RichMarkdownEditor from 'rich-markdown-editor';
type Node = RichMarkdownEditor['view']['state']['doc']
type Node = RichMarkdownEditor['view']['state']['doc'];
/**
* From https://github.com/outline/rich-markdown-editor/blob/3540af9f811a687c46ea82e0274a6286181da4f2/src/commands/createAndInsertLink.ts#L5-L33
@ -17,7 +17,7 @@ export function findPlaceholderLink(doc: Node, href: string) {
if (mark.type.name === 'link') {
// any of the links to other docs?
if (mark.attrs.href === href) {
result = {node, pos};
result = { node, pos };
if (result) return false;
}
}

View File

@ -8,19 +8,19 @@ import { useCallback } from 'react';
import UIState from '../state/ui';
const defaultOptions: OptionsObject = {
anchorOrigin: {horizontal: 'center', vertical: 'bottom'},
anchorOrigin: { horizontal: 'center', vertical: 'bottom' },
};
const defaultOptionsForMobile: OptionsObject = {
anchorOrigin: {horizontal: 'left', vertical: 'bottom'},
anchorOrigin: { horizontal: 'left', vertical: 'bottom' },
};
export const useToast = () => {
const {
ua: {isMobileOnly},
ua: { isMobileOnly },
} = UIState.useContainer();
const {enqueueSnackbar} = useSnackbar();
const { enqueueSnackbar } = useSnackbar();
const toast = useCallback(
(text: SnackbarMessage, variant?: VariantType) => {
enqueueSnackbar(text, {

View File

@ -10,7 +10,7 @@ export interface TreeOption {
}
export const useTreeOptions = (tree: TreeModel) => {
const {t} = useI18n();
const { t } = useI18n();
const options: TreeOption[] = useMemo(
() =>
reduce<TreeItemModel, TreeOption[]>(
@ -21,7 +21,9 @@ export const useTreeOptions = (tree: TreeModel) => {
id: cur.id,
label:
cur.data?.title ||
(cur.id === tree.rootId ? t('Root Page') : t('Untitled')),
(cur.id === tree.rootId
? t('Root Page')
: t('Untitled')),
});
}
return items;

View File

@ -45,10 +45,10 @@ const useEditor = (initNote?: NoteModel) => {
} = NoteState.useContainer();
const note = initNote ?? noteProp;
const {
ua: {isBrowser},
ua: { isBrowser },
} = UIState.useContainer();
const router = useRouter();
const {request, error} = useFetcher();
const { request, error } = useFetcher();
const toast = useToast();
const editorEl = useRef<MarkdownEditor>(null);
@ -58,11 +58,11 @@ const useEditor = (initNote?: NoteModel) => {
if (isNew) {
data.pid = (router.query.pid as string) || ROOT_ID;
const item = await createNote({...note, ...data});
const item = await createNote({ ...note, ...data });
const noteUrl = `/${item?.id}`;
if (router.asPath !== noteUrl) {
await router.replace(noteUrl, undefined, {shallow: true});
await router.replace(noteUrl, undefined, { shallow: true });
}
} else {
await updateNote(data);
@ -87,7 +87,7 @@ const useEditor = (initNote?: NoteModel) => {
const onClickLink = useCallback(
(href: string) => {
if (isNoteLink(href.replace(location.origin, ''))) {
router.push(href, undefined, {shallow: true});
router.push(href, undefined, { shallow: true });
} else {
window.open(href, '_blank');
}
@ -115,7 +115,7 @@ const useEditor = (initNote?: NoteModel) => {
[error, request, toast]
);
const {preview, linkToolbar} = PortalState.useContainer();
const { preview, linkToolbar } = PortalState.useContainer();
const onHoverLink = useCallback(
(event: MouseEvent | ReactMouseEvent) => {
@ -130,14 +130,14 @@ const useEditor = (initNote?: NoteModel) => {
if (href) {
if (isNoteLink(href)) {
preview.close();
preview.setData({id: href.slice(1)});
preview.setData({ id: href.slice(1) });
preview.setAnchor(link);
} else {
linkToolbar.setData({href, view: editorEl.current?.view});
linkToolbar.setData({ href, view: editorEl.current?.view });
linkToolbar.setAnchor(link);
}
} else {
preview.setData({id: undefined});
preview.setData({ id: undefined });
}
return true;
},
@ -161,7 +161,7 @@ const useEditor = (initNote?: NoteModel) => {
const onEditorChange = useCallback(
(value: () => string): void => {
onNoteChange.callback({content: value()});
onNoteChange.callback({ content: value() });
},
[onNoteChange]
);

View File

@ -10,9 +10,9 @@ import { isEmpty, map } from 'lodash';
const useNote = (initData?: NoteModel) => {
const [note, setNote] = useState<NoteModel | undefined>(initData);
const {find, abort: abortFindNote} = useNoteAPI();
const {create, error: createError} = useNoteAPI();
const {mutate, loading, abort} = useNoteAPI();
const { find, abort: abortFindNote } = useNoteAPI();
const { create, error: createError } = useNoteAPI();
const { mutate, loading, abort } = useNoteAPI();
const {
addItem,
removeItem,
@ -50,7 +50,7 @@ const useNote = (initData?: NoteModel) => {
setNote((prev) => {
if (prev?.id === id) {
return {...prev, ...payload};
return { ...prev, ...payload };
}
return prev;
});
@ -84,7 +84,7 @@ const useNote = (initData?: NoteModel) => {
setNote((prev) => {
if (prev?.id === id) {
return {...prev, ...payload};
return { ...prev, ...payload };
}
return prev;
});
@ -135,7 +135,7 @@ const useNote = (initData?: NoteModel) => {
await noteCache.setItem(result.id, result);
addItem(result);
return {id};
return { id };
},
[addItem, create, genNewId]
);

View File

@ -14,7 +14,7 @@ const useModalInstance = () => {
setVisible(false);
}, []);
return {visible, open, close};
return { visible, open, close };
};
const useAnchorInstance = <T>() => {
@ -30,7 +30,7 @@ const useAnchorInstance = <T>() => {
setVisible(false);
}, []);
return {anchor, open, close, data, setData, visible, setAnchor};
return { anchor, open, close, data, setData, visible, setAnchor };
};
const useModal = () => {
@ -41,8 +41,8 @@ const useModal = () => {
share: useAnchorInstance<NoteModel>(),
preview: useAnchorInstance<{ id?: string }>(),
linkToolbar: useAnchorInstance<{
href: string
view?: RichMarkdownEditor['view']
href: string;
view?: RichMarkdownEditor['view'];
}>(),
};
};

View File

@ -12,7 +12,7 @@ function useSearch() {
setList(keyword ? await searchNote(keyword, NOTE_DELETED.NORMAL) : []);
}, []);
return {list, keyword, filterNotes};
return { list, keyword, filterNotes };
}
const SearchState = createContainer(useSearch);

View File

@ -12,8 +12,8 @@ import { ROOT_ID } from 'libs/shared/tree';
function useTrash() {
const [keyword, setKeyword] = useState<string>();
const [list, setList] = useState<NoteCacheItem[]>();
const {restoreItem, deleteItem} = NoteTreeState.useContainer();
const {mutate, loading} = useTrashAPI();
const { restoreItem, deleteItem } = NoteTreeState.useContainer();
const { mutate, loading } = useTrashAPI();
const filterNotes = useCallback(async (keyword = '') => {
const data = await searchNote(keyword, NOTE_DELETED.DELETED);
@ -26,7 +26,11 @@ function useTrash() {
async (note: NoteModel) => {
// 父页面被删除时,恢复页面的 parent 改成 root
const pNote = note.pid && (await noteCache.getItem(note.pid));
if (!note.pid || !pNote || pNote?.deleted === NOTE_DELETED.DELETED) {
if (
!note.pid ||
!pNote ||
pNote?.deleted === NOTE_DELETED.DELETED
) {
note.pid = ROOT_ID;
}

View File

@ -37,10 +37,10 @@ const findParentTreeItems = (tree: TreeModel, note: NoteModel) => {
};
const useNoteTree = (initData: TreeModel = DEFAULT_TREE) => {
const {mutate, loading, fetch: fetchTree} = useTreeAPI();
const { mutate, loading, fetch: fetchTree } = useTreeAPI();
const [tree, setTree] = useState<TreeModel>(initData);
const [initLoaded, setInitLoaded] = useState<boolean>(false);
const {fetch: fetchNote} = useNoteAPI();
const { fetch: fetchNote } = useNoteAPI();
const treeRef = useRef(tree);
const toast = useToast();
@ -100,7 +100,9 @@ const useNoteTree = (initData: TreeModel = DEFAULT_TREE) => {
map(
TreeActions.flattenTree(tree, id),
async (item) =>
await noteCache.mutateItem(item.id, {deleted: NOTE_DELETED.DELETED})
await noteCache.mutateItem(item.id, {
deleted: NOTE_DELETED.DELETED,
})
)
);
}, []);
@ -116,7 +118,11 @@ const useNoteTree = (initData: TreeModel = DEFAULT_TREE) => {
const moveItem = useCallback(
async (data: { source: movePosition; destination: movePosition }) => {
setTree(
TreeActions.moveItem(treeRef.current, data.source, data.destination)
TreeActions.moveItem(
treeRef.current,
data.source,
data.destination
)
);
await mutate({
action: 'move',
@ -152,7 +158,9 @@ const useNoteTree = (initData: TreeModel = DEFAULT_TREE) => {
map(
TreeActions.flattenTree(tree, id),
async (item) =>
await noteCache.mutateItem(item.id, {deleted: NOTE_DELETED.NORMAL})
await noteCache.mutateItem(item.id, {
deleted: NOTE_DELETED.NORMAL,
})
)
);
}, []);
@ -163,7 +171,9 @@ const useNoteTree = (initData: TreeModel = DEFAULT_TREE) => {
const getPaths = useCallback((note: NoteModel) => {
const tree = treeRef.current;
return findParentTreeItems(tree, note).map((listItem) => listItem.data!);
return findParentTreeItems(tree, note).map(
(listItem) => listItem.data!
);
}, []);
const setItemsExpandState = useCallback(
@ -171,7 +181,9 @@ const useNoteTree = (initData: TreeModel = DEFAULT_TREE) => {
const newTree = reduce(
items,
(tempTree, item) =>
TreeActions.mutateItem(tempTree, item.id, {isExpanded: newValue}),
TreeActions.mutateItem(tempTree, item.id, {
isExpanded: newValue,
}),
treeRef.current
);
setTree(newTree);
@ -199,7 +211,11 @@ const useNoteTree = (initData: TreeModel = DEFAULT_TREE) => {
const checkItemIsShown = useCallback((note: NoteModel) => {
const parents = findParentTreeItems(treeRef.current, note);
return reduce(parents, (value, item) => value && !!item.isExpanded, true);
return reduce(
parents,
(value, item) => value && !!item.isExpanded,
true
);
}, []);
const collapseAllItems = useCallback(() => {

View File

@ -23,11 +23,11 @@ interface Props {
}
function useUI({
ua = DEFAULT_UA,
settings,
disablePassword,
IS_DEMO,
}: Props = {}) {
ua = DEFAULT_UA,
settings,
disablePassword,
IS_DEMO,
}: Props = {}) {
return {
ua,
sidebar: useSidebar(

View File

@ -4,7 +4,7 @@ import { useState, useCallback } from 'react';
export default function useSettings(initData = {} as Settings) {
const [settings, setSettings] = useState<Settings>(initData);
const {mutate} = useSettingsAPI();
const { mutate } = useSettingsAPI();
const updateSettings = useCallback(
async (body: Partial<Settings>) => {
@ -20,5 +20,5 @@ export default function useSettings(initData = {} as Settings) {
[mutate]
);
return {settings, updateSettings, setSettings};
return { settings, updateSettings, setSettings };
}

View File

@ -4,7 +4,7 @@ import { useState, useCallback } from 'react';
export default function useSidebar(initState = false, isMobileOnly = false) {
const [isFold, setIsFold] = useState(initState);
const {mutate} = useSettingsAPI();
const { mutate } = useSettingsAPI();
const toggle = useCallback(
async (state?: boolean) => {
@ -31,5 +31,5 @@ export default function useSidebar(initState = false, isMobileOnly = false) {
toggle(false);
}, [toggle]);
return {isFold, toggle, open, close};
return { isFold, toggle, open, close };
}

View File

@ -5,7 +5,7 @@ import useSettingsAPI from 'libs/web/api/settings';
export default function useSplit(initData = DEFAULT_SETTINGS.split_sizes) {
const [sizes, setSizes] = useState(initData);
const sizesRef = useRef(sizes);
const {mutate} = useSettingsAPI();
const { mutate } = useSettingsAPI();
useEffect(() => {
sizesRef.current = sizes;

View File

@ -7,5 +7,5 @@ export default function useTitle() {
setTitle(text ? `${text} - Notea` : 'Notea');
}, []);
return {value, updateTitle};
return { value, updateTitle };
}

View File

@ -27,7 +27,7 @@ interface Props {
lngDict: Record<string, string>;
}
export default function I18nProvider({children, locale, lngDict}: Props) {
export default function I18nProvider({ children, locale, lngDict }: Props) {
const activeLocaleRef = useRef(locale || defaultLanguage);
const [, setTick] = useState(0);
const firstRender = useRef(true);
@ -36,7 +36,10 @@ export default function I18nProvider({children, locale, lngDict}: Props) {
activeLocale: activeLocaleRef.current,
t: (key, ...args) => {
if (activeLocaleRef.current === defaultLanguage) {
return pupa(Array.isArray(key) ? key.join('') : key, args[0] ?? {});
return pupa(
Array.isArray(key) ? key.join('') : key,
args[0] ?? {}
);
}
return i18n.t(Array.isArray(key) ? key : [key], ...args);
},
@ -66,6 +69,8 @@ export default function I18nProvider({children, locale, lngDict}: Props) {
}, [lngDict, locale]);
return (
<I18nContext.Provider value={i18nWrapper}>{children}</I18nContext.Provider>
<I18nContext.Provider value={i18nWrapper}>
{children}
</I18nContext.Provider>
);
}

View File

@ -22,13 +22,13 @@ export async function searchNote(keyword: string, deleted: NOTE_DELETED) {
}
export function searchRangeText({
text,
keyword,
maxLen = 80,
}: {
text: string
keyword: string
maxLen: number
text,
keyword,
maxLen = 80,
}: {
text: string;
keyword: string;
maxLen: number;
}) {
let start = 0;
let end = 0;

View File

@ -1,119 +1,119 @@
{
"Add a page inside": "أضف صفحة داخلها",
"Add to Favorites": "أضف إلى المفضلة",
"Align center": "اصطفاف مركزي",
"Align left": "اصطفاف يساري",
"Align right": "اصطفاف يميني",
"Anyone can visit the page via the link": "يمكن لأي شخص زيارة الصفحة عبر الرابط",
"Back": "الرجوع",
"Basic": "أساسي",
"Big heading": "عنوان كبير",
"Bold": "خط عريض",
"Bulleted list": "قائمة نقطية",
"Cancel": "إلغاء",
"Code": "كود",
"Code block": "كتلة الكود",
"Collapse all pages": "Collapse all pages",
"Copied to clipboard": "تم النسخ في الحافظة",
"Copied!": "تم النسخ!",
"Copy Link": "انسخ الرابط",
"Copy to clipboard": "انسخ في الحافظة",
"Create a new note": "تدوينة جديدة",
"Create bookmark": "مرجعية جديدة",
"Create embed": "تضمين جديد",
"Create link": "رابط جديد",
"Create page": "صفحة جديدة",
"Daily Notes": "تدوينات يومية",
"Daily notes are saved in": "يتم حفز التدوينات اليومية في",
"Daily notes will be created under this page": "ستكون التدوينات اليومية داخل هذه الصفحة",
"Dark": "داكن",
"Default editor width": "Default editor width",
"Delete": "احذف",
"Delete column": "احذف العمود",
"Delete image": "احذف الصورة",
"Delete row": "احذف الصف",
"Delete table": "احذف الجدول",
"Disable editing in the demo.": "عطل التحرير في العرض التجريبي.",
"Divider": "مقسم",
"Export": "تصدير البيانات",
"Favorites": "المفضلة",
"File size must be less than {{n}}mb": "يجب أن يكون حجم الملف أقل من {{n}}mb",
"Find or create a note…": "أنشئ تدوينة أو ابحث عنها…",
"Fold Favorites": "طوي المفضلة",
"Fold sidebar": "طوي الشريط الجانبي",
"Forward": "تقدم",
"Heading": "العنوان",
"Highlight": "بارز",
"Image": "صورة",
"Import": "استيراد البيانات",
"Import & Export": "تصدير واستيراد البيانات",
"Import a zip file containing markdown files to this location, or export all pages from this location.": "قم باستيراد ملف مضغوط ZIP يحتوي على ملفات Markdown إلى هذا الموقع ، أو قم بتصدير جميع الصفحات من هذا الموقع.",
"Info": "معلومات",
"Info notice": "إشعار",
"Inject analytics or other scripts into the HTML of your sharing page. ": "قم بإدخال التحليلات أو البرامج النصية الأخرى في HTML في صفحة المشاركة الخاصة بك.",
"Insert column after": "أدخل العمود بعد",
"Insert column before": "أدخل العمود قبل",
"Insert row after": "أدخل الصف بعد",
"Insert row before": "أدخل الصف قبل",
"Italic": "خط مائل",
"Keep typing to filter…": "استمر في الكتابة لتصفية النتائج ...",
"Language": "اللغة",
"Large": "Large",
"Light": "فاتح",
"Link": "رابط",
"Link copied to clipboard": "تم النسخ إلى الحافظة",
"Linked to this page": "مربوط بهذه الصفحة",
"Location": "المكان الجغرافي",
"Medium heading": "عنوان متوسط",
"My Pages": "صفحاتي",
"New Page": "صفحة جديدة",
"No notes inside": "لا توجد مدونات داخلها",
"No results": "لا توجد نتائج",
"Not a public page": "ليست صفحة عامة",
"Not found markdown file": "لا يوجد ملف Markdown",
"Open link": "افتح الرابط",
"Ordered list": "قائمة مرتبة",
"Page break": "فاصل الصفحة",
"Paste a link…": "انسخ رابط…",
"Paste a {{title}} link…": "انسخ رابط {{title}} …",
"Placeholder": "النص النائب",
"Please select zip file": "المرجو اختيار الملف المضغوط ZIP",
"Quote": "اقتباس",
"Recovery": "استعادة",
"Remove": "احذف",
"Remove from Favorites": "احذف من المفضلة",
"Remove link": "احذف الرابط",
"Remove, Copy Link, etc": "احذف، انسخ رابط، إلخ",
"Root Page": "الصفحة الأصلية",
"Search note": "ابحث عن تدوينة",
"Search note in trash": "ابحث عن تدوينة في القمامة",
"Search or paste a link…": "ابحث أو قم بلصق رابط …",
"Settings": "الإعدادات",
"Share page": "شارك الصفحة",
"Share to web": "انشر على الويب",
"Sharing": "النشر",
"Show note in tree": "Show note in tree",
"Small (default)": "Small (default)",
"Small heading": "عنوان صغير",
"Snippet injection": "حقن مقتطف الكود",
"Sorry, an error occurred creating the link": "عذرًا، حدث خطأ أثناء إنشاء الرابط",
"Sorry, an error occurred uploading the image": "عذرًا، حدث خطأ أثناء تحميل الصورة",
"Sorry, that link wont work for this embed type": "عذرًا، هذا الرابط لن يعمل مع هذا النوع من التضمين",
"Strikethrough": "يتوسطه خط",
"Subheading": "العنوان الفرعي",
"Successfully imported {{n}} markdown files": "تم استيراد {{n}} من ملفات Markdown",
"Sync with system": "تزامن مع النظام",
"Table": "الجدول",
"Theme mode": "وضع الثيم",
"This page is in trash": "توجد هذه الصفحة في القمامة",
"Tip": "تلميح",
"Tip notice": "إشعار التلميح",
"Todo list": "قائمة ما يجب فعله",
"Toggle width": "Toggle width",
"Trash": "قمامة",
"Type '/' to insert…": "اكتب ' / ' للإدراج …",
"Untitled": "بدون عنوان",
"Warning": "تحذير",
"Warning notice": "إشعار التحذير",
"Write something nice…": "اكتب شيئًا لطيفًا …"
}
"Add a page inside": "أضف صفحة داخلها",
"Add to Favorites": "أضف إلى المفضلة",
"Align center": "اصطفاف مركزي",
"Align left": "اصطفاف يساري",
"Align right": "اصطفاف يميني",
"Anyone can visit the page via the link": "يمكن لأي شخص زيارة الصفحة عبر الرابط",
"Back": "الرجوع",
"Basic": "أساسي",
"Big heading": "عنوان كبير",
"Bold": "خط عريض",
"Bulleted list": "قائمة نقطية",
"Cancel": "إلغاء",
"Code": "كود",
"Code block": "كتلة الكود",
"Collapse all pages": "Collapse all pages",
"Copied to clipboard": "تم النسخ في الحافظة",
"Copied!": "تم النسخ!",
"Copy Link": "انسخ الرابط",
"Copy to clipboard": "انسخ في الحافظة",
"Create a new note": "تدوينة جديدة",
"Create bookmark": "مرجعية جديدة",
"Create embed": "تضمين جديد",
"Create link": "رابط جديد",
"Create page": "صفحة جديدة",
"Daily Notes": "تدوينات يومية",
"Daily notes are saved in": "يتم حفز التدوينات اليومية في",
"Daily notes will be created under this page": "ستكون التدوينات اليومية داخل هذه الصفحة",
"Dark": "داكن",
"Default editor width": "Default editor width",
"Delete": "احذف",
"Delete column": "احذف العمود",
"Delete image": "احذف الصورة",
"Delete row": "احذف الصف",
"Delete table": "احذف الجدول",
"Disable editing in the demo.": "عطل التحرير في العرض التجريبي.",
"Divider": "مقسم",
"Export": "تصدير البيانات",
"Favorites": "المفضلة",
"File size must be less than {{n}}mb": "يجب أن يكون حجم الملف أقل من {{n}}mb",
"Find or create a note…": "أنشئ تدوينة أو ابحث عنها…",
"Fold Favorites": "طوي المفضلة",
"Fold sidebar": "طوي الشريط الجانبي",
"Forward": "تقدم",
"Heading": "العنوان",
"Highlight": "بارز",
"Image": "صورة",
"Import": "استيراد البيانات",
"Import & Export": "تصدير واستيراد البيانات",
"Import a zip file containing markdown files to this location, or export all pages from this location.": "قم باستيراد ملف مضغوط ZIP يحتوي على ملفات Markdown إلى هذا الموقع ، أو قم بتصدير جميع الصفحات من هذا الموقع.",
"Info": "معلومات",
"Info notice": "إشعار",
"Inject analytics or other scripts into the HTML of your sharing page. ": "قم بإدخال التحليلات أو البرامج النصية الأخرى في HTML في صفحة المشاركة الخاصة بك.",
"Insert column after": "أدخل العمود بعد",
"Insert column before": "أدخل العمود قبل",
"Insert row after": "أدخل الصف بعد",
"Insert row before": "أدخل الصف قبل",
"Italic": "خط مائل",
"Keep typing to filter…": "استمر في الكتابة لتصفية النتائج ...",
"Language": "اللغة",
"Large": "Large",
"Light": "فاتح",
"Link": "رابط",
"Link copied to clipboard": "تم النسخ إلى الحافظة",
"Linked to this page": "مربوط بهذه الصفحة",
"Location": "المكان الجغرافي",
"Medium heading": "عنوان متوسط",
"My Pages": "صفحاتي",
"New Page": "صفحة جديدة",
"No notes inside": "لا توجد مدونات داخلها",
"No results": "لا توجد نتائج",
"Not a public page": "ليست صفحة عامة",
"Not found markdown file": "لا يوجد ملف Markdown",
"Open link": "افتح الرابط",
"Ordered list": "قائمة مرتبة",
"Page break": "فاصل الصفحة",
"Paste a link…": "انسخ رابط…",
"Paste a {{title}} link…": "انسخ رابط {{title}} …",
"Placeholder": "النص النائب",
"Please select zip file": "المرجو اختيار الملف المضغوط ZIP",
"Quote": "اقتباس",
"Recovery": "استعادة",
"Remove": "احذف",
"Remove from Favorites": "احذف من المفضلة",
"Remove link": "احذف الرابط",
"Remove, Copy Link, etc": "احذف، انسخ رابط، إلخ",
"Root Page": "الصفحة الأصلية",
"Search note": "ابحث عن تدوينة",
"Search note in trash": "ابحث عن تدوينة في القمامة",
"Search or paste a link…": "ابحث أو قم بلصق رابط …",
"Settings": "الإعدادات",
"Share page": "شارك الصفحة",
"Share to web": "انشر على الويب",
"Sharing": "النشر",
"Show note in tree": "Show note in tree",
"Small (default)": "Small (default)",
"Small heading": "عنوان صغير",
"Snippet injection": "حقن مقتطف الكود",
"Sorry, an error occurred creating the link": "عذرًا، حدث خطأ أثناء إنشاء الرابط",
"Sorry, an error occurred uploading the image": "عذرًا، حدث خطأ أثناء تحميل الصورة",
"Sorry, that link wont work for this embed type": "عذرًا، هذا الرابط لن يعمل مع هذا النوع من التضمين",
"Strikethrough": "يتوسطه خط",
"Subheading": "العنوان الفرعي",
"Successfully imported {{n}} markdown files": "تم استيراد {{n}} من ملفات Markdown",
"Sync with system": "تزامن مع النظام",
"Table": "الجدول",
"Theme mode": "وضع الثيم",
"This page is in trash": "توجد هذه الصفحة في القمامة",
"Tip": "تلميح",
"Tip notice": "إشعار التلميح",
"Todo list": "قائمة ما يجب فعله",
"Toggle width": "Toggle width",
"Trash": "قمامة",
"Type '/' to insert…": "اكتب ' / ' للإدراج …",
"Untitled": "بدون عنوان",
"Warning": "تحذير",
"Warning notice": "إشعار التحذير",
"Write something nice…": "اكتب شيئًا لطيفًا …"
}

View File

@ -1,119 +1,119 @@
{
"Add a page inside": "Seite innerhalb hinzufügen",
"Add to Favorites": "Add to Favorites",
"Align center": "Align center",
"Align left": "Align left",
"Align right": "Align right",
"Anyone can visit the page via the link": "Jeder mit den Link kann diese Seite besuchen",
"Back": "Back",
"Basic": "Basic",
"Big heading": "Big heading",
"Bold": "Bold",
"Bulleted list": "Bulleted list",
"Cancel": "Abbrechen",
"Code": "Code",
"Code block": "Code block",
"Collapse all pages": "Collapse all pages",
"Copied to clipboard": "Copied to clipboard",
"Copied!": "Kopiert!",
"Copy Link": "Link kopieren",
"Copy to clipboard": "In die Zwischenablage kopieren",
"Create a new note": "Create a new note",
"Create bookmark": "Create bookmark",
"Create embed": "Create embed",
"Create link": "Create link",
"Create page": "Seite erstellen",
"Daily Notes": "Tägliche Notizen",
"Daily notes are saved in": "Tägliche Notizen gespeichert in",
"Daily notes will be created under this page": "Tägliche Notizen werden unter dieser Seite erstellt",
"Dark": "Dunkel",
"Default editor width": "Default editor width",
"Delete": "Löschen",
"Delete column": "Delete column",
"Delete image": "Delete image",
"Delete row": "Delete row",
"Delete table": "Delete table",
"Disable editing in the demo.": "Disable editing in the demo.",
"Divider": "Divider",
"Export": "Export",
"Favorites": "Favorites",
"File size must be less than {{n}}mb": "File size must be less than {{n}}mb",
"Find or create a note…": "Find or create a note…",
"Fold Favorites": "Fold Favorites",
"Fold sidebar": "Seitenleiste einklappen",
"Forward": "Forward",
"Heading": "Heading",
"Highlight": "Highlight",
"Image": "Image",
"Import": "Import",
"Import & Export": "Import & Export",
"Import a zip file containing markdown files to this location, or export all pages from this location.": "Import a zip file containing markdown files to this location, or export all pages from this location.",
"Info": "Info",
"Info notice": "Info notice",
"Inject analytics or other scripts into the HTML of your sharing page. ": "Inject analytics or other scripts into the HTML of your sharing page. ",
"Insert column after": "Insert column after",
"Insert column before": "Insert column before",
"Insert row after": "Insert row after",
"Insert row before": "Insert row before",
"Italic": "Italic",
"Keep typing to filter…": "Keep typing to filter…",
"Language": "Sprache",
"Large": "Large",
"Light": "Hell",
"Link": "Link",
"Link copied to clipboard": "Link copied to clipboard",
"Linked to this page": "Linked to this page",
"Location": "Location",
"Medium heading": "Medium heading",
"My Pages": "Meine Seiten",
"New Page": "Neue Seite",
"No notes inside": "Keine Notizen innerhalb",
"No results": "No results",
"Not a public page": "Not a public page",
"Not found markdown file": "Not found markdown file",
"Open link": "Open link",
"Ordered list": "Ordered list",
"Page break": "Page break",
"Paste a link…": "Paste a link…",
"Paste a {{title}} link…": "Paste a {{title}} link…",
"Placeholder": "Placeholder",
"Please select zip file": "Please select zip file",
"Quote": "Quote",
"Recovery": "Wiederherstellung",
"Remove": "Entfernen",
"Remove from Favorites": "Remove from Favorites",
"Remove link": "Remove link",
"Remove, Copy Link, etc": "Entfernen, Link kopieren, etc",
"Root Page": "Ursprungsseite",
"Search note": "Notiz durchsuchen",
"Search note in trash": "Search note in trash",
"Search or paste a link…": "Search or paste a link…",
"Settings": "Einstellungen",
"Share page": "Seite teilen",
"Share to web": "Per Link teilen",
"Sharing": "Sharing",
"Show note in tree": "Show note in tree",
"Small (default)": "Small (default)",
"Small heading": "Small heading",
"Snippet injection": "Snippet injection",
"Sorry, an error occurred creating the link": "Sorry, an error occurred creating the link",
"Sorry, an error occurred uploading the image": "Sorry, an error occurred uploading the image",
"Sorry, that link wont work for this embed type": "Sorry, that link wont work for this embed type",
"Strikethrough": "Strikethrough",
"Subheading": "Subheading",
"Successfully imported {{n}} markdown files": "Successfully imported {{n}} markdown files",
"Sync with system": "mit System synchronisieren",
"Table": "Table",
"Theme mode": "Theme mode",
"This page is in trash": "This page is in trash",
"Tip": "Tip",
"Tip notice": "Tip notice",
"Todo list": "Todo list",
"Toggle width": "Toggle width",
"Trash": "Papierkorb",
"Type '/' to insert…": "Type '/' to insert…",
"Untitled": "Unbenannt",
"Warning": "Warning",
"Warning notice": "Warning notice",
"Write something nice…": "Write something nice…"
}
"Add a page inside": "Seite innerhalb hinzufügen",
"Add to Favorites": "Add to Favorites",
"Align center": "Align center",
"Align left": "Align left",
"Align right": "Align right",
"Anyone can visit the page via the link": "Jeder mit den Link kann diese Seite besuchen",
"Back": "Back",
"Basic": "Basic",
"Big heading": "Big heading",
"Bold": "Bold",
"Bulleted list": "Bulleted list",
"Cancel": "Abbrechen",
"Code": "Code",
"Code block": "Code block",
"Collapse all pages": "Collapse all pages",
"Copied to clipboard": "Copied to clipboard",
"Copied!": "Kopiert!",
"Copy Link": "Link kopieren",
"Copy to clipboard": "In die Zwischenablage kopieren",
"Create a new note": "Create a new note",
"Create bookmark": "Create bookmark",
"Create embed": "Create embed",
"Create link": "Create link",
"Create page": "Seite erstellen",
"Daily Notes": "Tägliche Notizen",
"Daily notes are saved in": "Tägliche Notizen gespeichert in",
"Daily notes will be created under this page": "Tägliche Notizen werden unter dieser Seite erstellt",
"Dark": "Dunkel",
"Default editor width": "Default editor width",
"Delete": "Löschen",
"Delete column": "Delete column",
"Delete image": "Delete image",
"Delete row": "Delete row",
"Delete table": "Delete table",
"Disable editing in the demo.": "Disable editing in the demo.",
"Divider": "Divider",
"Export": "Export",
"Favorites": "Favorites",
"File size must be less than {{n}}mb": "File size must be less than {{n}}mb",
"Find or create a note…": "Find or create a note…",
"Fold Favorites": "Fold Favorites",
"Fold sidebar": "Seitenleiste einklappen",
"Forward": "Forward",
"Heading": "Heading",
"Highlight": "Highlight",
"Image": "Image",
"Import": "Import",
"Import & Export": "Import & Export",
"Import a zip file containing markdown files to this location, or export all pages from this location.": "Import a zip file containing markdown files to this location, or export all pages from this location.",
"Info": "Info",
"Info notice": "Info notice",
"Inject analytics or other scripts into the HTML of your sharing page. ": "Inject analytics or other scripts into the HTML of your sharing page. ",
"Insert column after": "Insert column after",
"Insert column before": "Insert column before",
"Insert row after": "Insert row after",
"Insert row before": "Insert row before",
"Italic": "Italic",
"Keep typing to filter…": "Keep typing to filter…",
"Language": "Sprache",
"Large": "Large",
"Light": "Hell",
"Link": "Link",
"Link copied to clipboard": "Link copied to clipboard",
"Linked to this page": "Linked to this page",
"Location": "Location",
"Medium heading": "Medium heading",
"My Pages": "Meine Seiten",
"New Page": "Neue Seite",
"No notes inside": "Keine Notizen innerhalb",
"No results": "No results",
"Not a public page": "Not a public page",
"Not found markdown file": "Not found markdown file",
"Open link": "Open link",
"Ordered list": "Ordered list",
"Page break": "Page break",
"Paste a link…": "Paste a link…",
"Paste a {{title}} link…": "Paste a {{title}} link…",
"Placeholder": "Placeholder",
"Please select zip file": "Please select zip file",
"Quote": "Quote",
"Recovery": "Wiederherstellung",
"Remove": "Entfernen",
"Remove from Favorites": "Remove from Favorites",
"Remove link": "Remove link",
"Remove, Copy Link, etc": "Entfernen, Link kopieren, etc",
"Root Page": "Ursprungsseite",
"Search note": "Notiz durchsuchen",
"Search note in trash": "Search note in trash",
"Search or paste a link…": "Search or paste a link…",
"Settings": "Einstellungen",
"Share page": "Seite teilen",
"Share to web": "Per Link teilen",
"Sharing": "Sharing",
"Show note in tree": "Show note in tree",
"Small (default)": "Small (default)",
"Small heading": "Small heading",
"Snippet injection": "Snippet injection",
"Sorry, an error occurred creating the link": "Sorry, an error occurred creating the link",
"Sorry, an error occurred uploading the image": "Sorry, an error occurred uploading the image",
"Sorry, that link wont work for this embed type": "Sorry, that link wont work for this embed type",
"Strikethrough": "Strikethrough",
"Subheading": "Subheading",
"Successfully imported {{n}} markdown files": "Successfully imported {{n}} markdown files",
"Sync with system": "mit System synchronisieren",
"Table": "Table",
"Theme mode": "Theme mode",
"This page is in trash": "This page is in trash",
"Tip": "Tip",
"Tip notice": "Tip notice",
"Todo list": "Todo list",
"Toggle width": "Toggle width",
"Trash": "Papierkorb",
"Type '/' to insert…": "Type '/' to insert…",
"Untitled": "Unbenannt",
"Warning": "Warning",
"Warning notice": "Warning notice",
"Write something nice…": "Write something nice…"
}

Some files were not shown because too many files have changed in this diff Show More