mirror of
https://github.com/notea-org/notea.git
synced 2024-10-04 02:17:10 +03:00
refactor(style): The Big Reformat, part 2
reformat: All files have been reformatted.
This commit is contained in:
parent
5622cc7c16
commit
f1898cc133
4
.babelrc
4
.babelrc
@ -1,4 +1,4 @@
|
||||
{
|
||||
"presets": ["next/babel"],
|
||||
"plugins": ["lodash"]
|
||||
"presets": ["next/babel"],
|
||||
"plugins": ["lodash"]
|
||||
}
|
||||
|
6
.github/pull.yml
vendored
6
.github/pull.yml
vendored
@ -1,5 +1,5 @@
|
||||
version: '1'
|
||||
rules:
|
||||
- base: main
|
||||
upstream: qingwei-li:main
|
||||
mergeMethod: hardreset
|
||||
- base: main
|
||||
upstream: qingwei-li:main
|
||||
mergeMethod: hardreset
|
||||
|
92
.github/workflows/docker-publish.yml
vendored
92
.github/workflows/docker-publish.yml
vendored
@ -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
10
additional.d.ts
vendored
@ -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';
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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 />;
|
||||
}
|
||||
|
@ -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 won’t work for this embed type'),
|
||||
embedInvalidLink: t(
|
||||
'Sorry, that link won’t 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'),
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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 />;
|
||||
};
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -17,7 +17,7 @@ export const lightTheme: typeof theme = {
|
||||
};
|
||||
|
||||
export const useEditorTheme = () => {
|
||||
const {resolvedTheme} = useTheme();
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
return resolvedTheme === 'dark' ? darkTheme : lightTheme;
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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')}
|
||||
|
@ -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
|
||||
|
@ -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>();
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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, {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -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]
|
||||
|
@ -14,7 +14,11 @@ export const SettingFooter = () => {
|
||||
</div>
|
||||
<div className="space-x-1">
|
||||
<span>MIT ©</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>
|
||||
|
@ -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>
|
||||
|
@ -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}>
|
||||
|
@ -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>
|
||||
|
@ -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(
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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'
|
||||
|
@ -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/',
|
||||
|
@ -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
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>;
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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();
|
||||
|
@ -13,7 +13,7 @@ export const applySettings: SSRMiddleware = async (req, _res, next) => {
|
||||
|
||||
req.props = {
|
||||
...req.props,
|
||||
...{settings, lngDict},
|
||||
...{ settings, lngDict },
|
||||
};
|
||||
next();
|
||||
};
|
||||
|
@ -14,7 +14,7 @@ export const applyTree: SSRMiddleware = async (req, res, next) => {
|
||||
|
||||
req.props = {
|
||||
...req.props,
|
||||
...(tree && {tree}),
|
||||
...(tree && { tree }),
|
||||
};
|
||||
|
||||
next();
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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 }
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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] = {
|
||||
|
@ -12,7 +12,7 @@ type AllowedEnvs =
|
||||
| 'DIRECT_RESPONSE_ATTACHMENT'
|
||||
| 'IS_DEMO'
|
||||
| 'STORE_PREFIX'
|
||||
| 'CONFIG_FILE'
|
||||
| 'CONFIG_FILE';
|
||||
|
||||
export function getEnv<T>(
|
||||
env: AllowedEnvs,
|
||||
|
@ -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 };
|
||||
};
|
||||
|
@ -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',
|
||||
];
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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;
|
||||
},
|
||||
|
@ -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>) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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, {
|
||||
|
@ -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;
|
||||
|
@ -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]
|
||||
);
|
||||
|
@ -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]
|
||||
);
|
||||
|
@ -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'];
|
||||
}>(),
|
||||
};
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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(() => {
|
||||
|
@ -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(
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -7,5 +7,5 @@ export default function useTitle() {
|
||||
setTitle(text ? `${text} - Notea` : 'Notea');
|
||||
}, []);
|
||||
|
||||
return {value, updateTitle};
|
||||
return { value, updateTitle };
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
236
locales/ar.json
236
locales/ar.json
@ -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 won’t 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 won’t 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…": "اكتب شيئًا لطيفًا …"
|
||||
}
|
||||
|
@ -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 won’t work for this embed type": "Sorry, that link won’t 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 won’t work for this embed type": "Sorry, that link won’t 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
Loading…
Reference in New Issue
Block a user