Compare commits

...

19 Commits

Author SHA1 Message Date
boojack 9c479d5e56 chore: add tg group link 2022-07-15 22:43:57 +08:00
boojack 8a2008d6d7 chore: release v0.2.1 2022-07-15 22:43:41 +08:00
boojack 9c842d0a40 fix: remove axios withCredentials 2022-07-15 22:38:50 +08:00
boojack 0dc377550f chore: fix hover heatmap 2022-07-15 22:29:47 +08:00
boojack 8a91b0ad9d chore: add github badge 2022-07-15 22:17:11 +08:00
boojack 1b50ab5dca chore: use echo static middleware to serve dist 2022-07-15 21:25:29 +08:00
boojack 6053df050c chore: update create memo with visibility 2022-07-15 21:25:07 +08:00
boojack 3517c6181d chore: update vite 2022-07-14 19:33:20 +08:00
boojack 2e126c71f0 chore: update button elements 2022-07-10 12:02:36 +08:00
boojack 46d7ecca88 feat: use go embed 2022-07-10 09:02:56 +08:00
boojack 48d8c6ee0f chore: update lock file 2022-07-10 08:43:46 +08:00
boojack 5fd3cfdb61 chore: update user store 2022-07-10 08:36:10 +08:00
boojack 10d710cf03 chore: fix editor z-index 2022-07-10 08:35:36 +08:00
boojack 21702b615a chore: update seed data 2022-07-10 08:15:34 +08:00
boojack d75338b6e9 chore: fix z-index 2022-07-09 23:58:04 +08:00
boojack b85af714f5 feat: fullscreen editor 2022-07-09 23:16:20 +08:00
boojack a2b32e0b75 chore: update demo.webp 2022-07-09 21:04:53 +08:00
boojack 0505598509 fix: data desensitize 2022-07-09 21:01:09 +08:00
boojack 91d45d6d46 chore: release v0.2.0 (#114) 2022-07-09 14:09:40 +08:00
58 changed files with 1189 additions and 911 deletions
+1 -20
View File
@@ -1,29 +1,10 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Temp output
*.out
*.log
tmp
# Air (hot reload) generated
.air
# Frontend asset
dist
# Dev database
data
web/dist
# build folder
build
memos-build
.DS_Store
+1 -1
View File
@@ -15,6 +15,7 @@ RUN apk update
RUN apk --no-cache add gcc musl-dev
COPY . .
COPY --from=frontend /frontend-build/dist ./server/dist
RUN go build \
-o memos \
@@ -25,7 +26,6 @@ FROM alpine:3.16.0 AS monolithic
WORKDIR /usr/local/memos
COPY --from=backend /backend-build/memos /usr/local/memos/
COPY --from=frontend /frontend-build/dist /usr/local/memos/web/dist
# Directory to store the data, which can be referenced as the mounting point.
RUN mkdir -p /var/opt/memos
+13 -4
View File
@@ -4,16 +4,17 @@
<p align="center">
<img alt="GitHub stars" src="https://img.shields.io/github/stars/usememos/memos" />
<img alt="Docker pull" src="https://img.shields.io/docker/pulls/neosmemo/memos.svg" />
<a href="https://hub.docker.com/r/neosmemo/memos"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/neosmemo/memos.svg" /></a>
<img alt="Go report" src="https://goreportcard.com/badge/github.com/usememos/memos" />
</p>
<p align="center">
<a href="https://memos.onrender.com/">Live Demo</a> •
<a href="https://github.com/usememos/memos/discussions">Discussions</a>
<a href="https://t.me/+-_tNF1k70UU4ZTc9">Discuss in Telegram</a>
</p>
![demo](https://raw.githubusercontent.com/usememos/memos/main/resources/demo.png)
![demo](https://raw.githubusercontent.com/usememos/memos/main/resources/demo.webp)
## 🎯 Intentions
@@ -31,11 +32,19 @@
## ⚓️ Deploy with Docker
```docker
docker run --name memos --publish 5230:5230 --volume ~/.memos/:/var/opt/memos -e mode=prod -e port=5230 neosmemo/memos:0.1.3
docker run \
--name memos \
--publish 5230:5230 \
--volume ~/.memos/:/var/opt/memos \
neosmemo/memos:latest \
--mode prod \
--port 5230
```
Memos should now be running at [http://localhost:5230](http://localhost:5230). If the `~/.memos/` does not have a `memos_prod.db` file, then `memos` will auto generate it.
⚠️ Please DO NOT use `dev` of docker image if you have no experience.
## 🏗 Development
Memos is built with a curated tech stack. It is optimized for developer experience and is very easy to start working on the code:
+2 -2
View File
@@ -42,8 +42,8 @@ type MemoCreate struct {
CreatedTs *int64 `json:"createdTs"`
// Domain specific fields
Content string `json:"content"`
Visibility Visibility `json:"visibility"`
Content string `json:"content"`
Visibility *Visibility `json:"visibility"`
}
type MemoPatch struct {
+2 -2
View File
@@ -7,10 +7,10 @@ import (
// Version is the service current released version.
// Semantic versioning: https://semver.org/
var Version = "0.1.3"
var Version = "0.2.1"
// DevVersion is the service current development version.
var DevVersion = "0.2.0"
var DevVersion = "0.2.1"
func GetCurrentVersion(mode string) string {
if mode == "dev" {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

+13
View File
@@ -0,0 +1,13 @@
<!-- THIS FILE IS A PLACEHOLDER AND SHOULD NOT BE CHANGED -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Memos</title>
</head>
<body>
<p>No frontend embeded.</p>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
package server
import (
"embed"
"io/fs"
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
//go:embed dist
var embeddedFiles embed.FS
func getFileSystem() http.FileSystem {
fs, err := fs.Sub(embeddedFiles, "dist")
if err != nil {
panic(err)
}
return http.FS(fs)
}
func embedFrontend(e *echo.Echo) {
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
HTML5: true,
Filesystem: getFileSystem(),
}))
}
+4 -2
View File
@@ -22,8 +22,10 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo request").SetInternal(err)
}
// TODO(steven): remove this line after frontend is ready
memoCreate.Visibility = api.Privite
if memoCreate.Visibility == nil || *memoCreate.Visibility == "" {
private := api.Privite
memoCreate.Visibility = &private
}
memo, err := s.Store.CreateMemo(memoCreate)
if err != nil {
+1 -6
View File
@@ -38,12 +38,7 @@ func NewServer(profile *profile.Profile) *Server {
Timeout: 30 * time.Second,
}))
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Skipper: middleware.DefaultSkipper,
Root: "web/dist",
Browse: true,
HTML5: true,
}))
embedFrontend(e)
// In dev mode, set the const secret key to make signin session persistence.
secret := []byte("usememos")
+5
View File
@@ -64,6 +64,11 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user").SetInternal(err)
}
if user != nil {
// data desensitize
user.OpenID = ""
}
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
+4 -4
View File
@@ -51,7 +51,7 @@ CREATE TABLE memo (
INSERT INTO
sqlite_sequence (name, seq)
VALUES
('memo', 100);
('memo', 1000);
CREATE TRIGGER IF NOT EXISTS `trigger_update_memo_modification_time`
AFTER
@@ -79,7 +79,7 @@ CREATE TABLE memo_organizer (
INSERT INTO
sqlite_sequence (name, seq)
VALUES
('memo_organizer', 100);
('memo_organizer', 1000);
-- shortcut
CREATE TABLE shortcut (
@@ -96,7 +96,7 @@ CREATE TABLE shortcut (
INSERT INTO
sqlite_sequence (name, seq)
VALUES
('shortcut', 100);
('shortcut', 10000);
CREATE TRIGGER IF NOT EXISTS `trigger_update_shortcut_modification_time`
AFTER
@@ -126,7 +126,7 @@ CREATE TABLE resource (
INSERT INTO
sqlite_sequence (name, seq)
VALUES
('resource', 100);
('resource', 10000);
CREATE TRIGGER IF NOT EXISTS `trigger_update_resource_modification_time`
AFTER
+11 -8
View File
@@ -6,8 +6,9 @@ INSERT INTO
)
VALUES
(
101,
'#Hello 👋 Welcome to memos',
1001,
"#Hello 👋 Welcome to memos.
And here is old Jack's Page: [/u/102](/u/102)",
101
);
@@ -19,7 +20,7 @@ INSERT INTO
)
VALUES
(
102,
1002,
'#TODO
- [ ] Take more photos about **🌄 sunset**;
- [x] Clean the room;
@@ -37,7 +38,7 @@ INSERT INTO
)
VALUES
(
103,
1003,
'好好学习,天天向上。🤜🤛',
101,
'PUBLIC'
@@ -51,7 +52,7 @@ INSERT INTO
)
VALUES
(
104,
1004,
'#TODO
- [x] Take more photos about **🌄 sunset**;
- [ ] Clean the classroom;
@@ -65,11 +66,13 @@ INSERT INTO
memo (
`id`,
`content`,
`creator_id`
`creator_id`,
`visibility`
)
VALUES
(
105,
1005,
'三人行,必有我师焉!👨‍🏫',
102
102,
'PUBLIC'
);
+1 -1
View File
@@ -6,7 +6,7 @@ INSERT INTO
)
VALUES
(
101,
1001,
101,
1
);
+6 -3
View File
@@ -118,10 +118,13 @@ func (s *Store) DeleteMemo(delete *api.MemoDelete) error {
}
func createMemoRaw(db *sql.DB, create *api.MemoCreate) (*memoRaw, error) {
set := []string{"creator_id", "content", "visibility"}
placeholder := []string{"?", "?", "?"}
args := []interface{}{create.CreatorID, create.Content, create.Visibility}
set := []string{"creator_id", "content"}
placeholder := []string{"?", "?"}
args := []interface{}{create.CreatorID, create.Content}
if v := create.Visibility; v != nil {
set, placeholder, args = append(set, "visibility"), append(placeholder, "?"), append(args, *v)
}
if v := create.CreatedTs; v != nil {
set, placeholder, args = append(set, "created_ts"), append(placeholder, "?"), append(args, *v)
}
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/logo.svg" sizes="64x64" type="image/*" />
<link rel="icon" href="/favicon.svg" sizes="64x64" type="image/*" />
<meta name="theme-color" content="#f6f5f4" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
<title>Memos</title>
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "memos",
"version": "0.1.3",
"version": "0.2.1",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
@@ -18,12 +18,13 @@
},
"devDependencies": {
"@types/lodash-es": "^4.17.5",
"@types/node": "^18.0.3",
"@types/qs": "^6.9.7",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.4",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"@vitejs/plugin-react": "^1.0.0",
"@vitejs/plugin-react": "^2.0.0",
"autoprefixer": "^10.4.2",
"eslint": "^8.4.1",
"eslint-config-prettier": "^8.3.0",
@@ -34,6 +35,6 @@
"prettier": "2.5.1",
"tailwindcss": "^3.0.18",
"typescript": "^4.3.2",
"vite": "^2.9.0"
"vite": "^3.0.0"
}
}

Before

Width:  |  Height:  |  Size: 121 B

After

Width:  |  Height:  |  Size: 121 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M6.1 44 4 41.9 18.9 27H9v-3h15v15h-3v-9.9ZM24 24V9h3v9.9L41.9 4 44 6.1 29.1 21H39v3Z"/></svg>

After

Width:  |  Height:  |  Size: 165 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M6 42V27h3v9.9L36.9 9H27V6h15v15h-3v-9.9L11.1 39H21v3Z"/></svg>

After

Width:  |  Height:  |  Size: 135 B

+4 -5
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import * as api from "../helpers/api";
import Only from "./common/OnlyWhen";
import { showDialog } from "./Dialog";
import GitHubBadge from "./GitHubBadge";
import "../less/about-site-dialog.less";
interface Props extends DialogProps {}
@@ -44,10 +45,8 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
Memos is an <i>open source</i>, <i>self-hosted</i> knowledge base that works with a SQLite db file.
</p>
<br />
<p>
<a className="mr-2" href="https://github.com/usememos/memos">
🏗 Source code
</a>
<div className="addtion-info-container">
<GitHubBadge />
<Only when={profile !== undefined}>
<>
version:
@@ -57,7 +56,7 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
🎉
</>
</Only>
</p>
</div>
</div>
</>
);
+8 -8
View File
@@ -63,18 +63,18 @@ const DailyReviewDialog: React.FC<Props> = (props: Props) => {
<span className="icon-text">📅</span> Daily Review
</p>
<div className="btns-container">
<span className="btn-text" onClick={() => setCurrentDateStamp(currentDateStamp - DAILY_TIMESTAMP)}>
<button className="btn-text" onClick={() => setCurrentDateStamp(currentDateStamp - DAILY_TIMESTAMP)}>
<img className="icon-img" src="/icons/arrow-left.svg" />
</span>
<span className="btn-text" onClick={() => setCurrentDateStamp(currentDateStamp + DAILY_TIMESTAMP)}>
</button>
<button className="btn-text" onClick={() => setCurrentDateStamp(currentDateStamp + DAILY_TIMESTAMP)}>
<img className="icon-img" src="/icons/arrow-right.svg" />
</span>
<span className="btn-text share-btn" onClick={handleShareBtnClick}>
</button>
<button className="btn-text share-btn" onClick={handleShareBtnClick}>
<img className="icon-img" src="/icons/share.svg" />
</span>
<span className="btn-text" onClick={() => props.destroy()}>
</button>
<button className="btn-text" onClick={() => props.destroy()}>
<img className="icon-img" src="/icons/close.svg" />
</span>
</button>
</div>
<DatePicker
className={`date-picker ${showDatePicker ? "" : "!hidden"}`}
+4 -2
View File
@@ -15,6 +15,7 @@ interface EditorProps {
className: string;
initialContent: string;
placeholder: string;
fullscreen: boolean;
showConfirmBtn: boolean;
showCancelBtn: boolean;
tools?: ReactNode;
@@ -29,6 +30,7 @@ const Editor = forwardRef((props: EditorProps, ref: React.ForwardedRef<EditorRef
className,
initialContent,
placeholder,
fullscreen,
showConfirmBtn,
showCancelBtn,
onConfirmBtnClick: handleConfirmBtnClickCallback,
@@ -45,11 +47,11 @@ const Editor = forwardRef((props: EditorProps, ref: React.ForwardedRef<EditorRef
}, []);
useEffect(() => {
if (editorRef.current) {
if (editorRef.current && !fullscreen) {
editorRef.current.style.height = "auto";
editorRef.current.style.height = (editorRef.current.scrollHeight ?? 0) + "px";
}
}, [editorRef.current?.value]);
}, [editorRef.current?.value, fullscreen]);
useImperativeHandle(
ref,
+31
View File
@@ -0,0 +1,31 @@
import { useEffect, useState } from "react";
import * as api from "../helpers/api";
import "../less/github-badge.less";
interface Props {}
const GitHubBadge: React.FC<Props> = () => {
const [starCount, setStarCount] = useState(0);
useEffect(() => {
api.getRepoStarCount().then((data) => {
setStarCount(data);
});
}, []);
const handleClick = () => {
window.location.href = "https://github.com/usememos/memos";
};
return (
<div className="github-badge-container" onClick={handleClick}>
<div className="github-icon">
<img className="icon-img" src="/github.webp" alt="" />
Star
</div>
<span className={`count-text ${starCount || "pulse"}`}>{starCount || "🌟"}</span>
</div>
);
};
export default GitHubBadge;
+4 -4
View File
@@ -1,6 +1,6 @@
import { memo, useEffect, useRef, useState } from "react";
import { escape, indexOf } from "lodash-es";
import { IMAGE_URL_REG, LINK_REG, MEMO_LINK_REG, TAG_REG, UNKNOWN_ID } from "../helpers/consts";
import { IMAGE_URL_REG, LINK_URL_REG, MEMO_LINK_REG, TAG_REG, UNKNOWN_ID } from "../helpers/consts";
import * as utils from "../helpers/utils";
import { DONE_BLOCK_REG, parseMarkedToHtml, TODO_BLOCK_REG } from "../helpers/marked";
import { editorStateService, locationService, memoService, userService } from "../services";
@@ -229,9 +229,9 @@ export function formatMemoContent(content: string) {
return tempElement.innerHTML
.replace(IMAGE_URL_REG, "")
.replace(TAG_REG, "<span class='tag-span'>#$1</span> ")
.replace(LINK_REG, "<a class='link' target='_blank' rel='noreferrer' href='$1'>$1</a>")
.replace(MEMO_LINK_REG, "<span class='memo-link-text' data-value='$2'>$1</span>");
.replace(MEMO_LINK_REG, "<span class='memo-link-text' data-value='$2'>$1</span>")
.replace(LINK_URL_REG, "<a class='link' target='_blank' rel='noreferrer' href='$2'>$1</a>")
.replace(TAG_REG, "<span class='tag-span'>#$1</span> ");
}
export default memo(Memo);
-1
View File
@@ -9,7 +9,6 @@ import { showDialog } from "./Dialog";
import Image from "./Image";
import { formatMemoContent } from "./Memo";
import "../less/memo-card-dialog.less";
import "../less/memo-content.less";
interface LinkedMemo extends Memo {
createdAtStr: string;
+44 -17
View File
@@ -1,19 +1,26 @@
import React, { useCallback, useEffect, useMemo, useRef } from "react";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { UNKNOWN_ID } from "../helpers/consts";
import { editorStateService, locationService, memoService, resourceService } from "../services";
import { useAppSelector } from "../store";
import * as storage from "../helpers/storage";
import useToggle from "../hooks/useToggle";
import toastHelper from "./Toast";
import Editor, { EditorRefActions } from "./Editor/Editor";
import "../less/memo-editor.less";
interface Props {}
interface State {
isUploadingResource: boolean;
fullscreen: boolean;
}
const MemoEditor: React.FC<Props> = () => {
const editorState = useAppSelector((state) => state.editor);
const tags = useAppSelector((state) => state.memo.tags);
const [isUploadingResource, setIsUploadingResource] = useToggle(false);
const [state, setState] = useState<State>({
isUploadingResource: false,
fullscreen: false,
});
const editorRef = useRef<EditorRefActions>(null);
const prevGlobalStateRef = useRef(editorState);
const tagSeletorRef = useRef<HTMLDivElement>(null);
@@ -21,7 +28,7 @@ const MemoEditor: React.FC<Props> = () => {
useEffect(() => {
if (editorState.markMemoId && editorState.markMemoId !== UNKNOWN_ID) {
const editorCurrentValue = editorRef.current?.getContent();
const memoLinkText = `${editorCurrentValue ? "\n" : ""}Mark: [@MEMO](${editorState.markMemoId})`;
const memoLinkText = `${editorCurrentValue ? "\n" : ""}Mark: @[MEMO](${editorState.markMemoId})`;
editorRef.current?.insertText(memoLinkText);
editorStateService.clearMarkMemo();
}
@@ -89,11 +96,14 @@ const MemoEditor: React.FC<Props> = () => {
const handleUploadFile = useCallback(
async (file: File) => {
if (isUploadingResource) {
if (state.isUploadingResource) {
return;
}
setIsUploadingResource(true);
setState({
...state,
isUploadingResource: true,
});
const { type } = file;
if (!type.startsWith("image")) {
@@ -108,21 +118,23 @@ const MemoEditor: React.FC<Props> = () => {
} catch (error: any) {
toastHelper.error(error);
} finally {
setIsUploadingResource(false);
setState({
...state,
isUploadingResource: false,
});
}
},
[isUploadingResource]
[state]
);
const handleSaveBtnClick = useCallback(async (content: string) => {
const handleSaveBtnClick = async (content: string) => {
if (content === "") {
toastHelper.error("Content can't be empty");
return;
}
const { editMemoId } = editorStateService.getState();
try {
const { editMemoId } = editorStateService.getState();
if (editMemoId && editMemoId !== UNKNOWN_ID) {
const prevMemo = memoService.getMemoById(editMemoId ?? UNKNOWN_ID);
@@ -143,8 +155,12 @@ const MemoEditor: React.FC<Props> = () => {
toastHelper.error(error.message);
}
setState({
...state,
fullscreen: false,
});
setEditorContentCache("");
}, []);
};
const handleCancelBtnClick = useCallback(() => {
editorStateService.clearEditMemo();
@@ -175,6 +191,13 @@ const MemoEditor: React.FC<Props> = () => {
inputEl.click();
}, []);
const handleFullscreenBtnClick = () => {
setState({
...state,
fullscreen: !state.fullscreen,
});
};
const handleTagSeletorClick = useCallback((event: React.MouseEvent) => {
if (tagSeletorRef.current !== event.target && tagSeletorRef.current?.contains(event.target as Node)) {
editorRef.current?.insertText(`#${(event.target as HTMLElement).textContent} ` ?? "");
@@ -189,17 +212,18 @@ const MemoEditor: React.FC<Props> = () => {
className: "memo-editor",
initialContent: getEditorContentCache(),
placeholder: "Any thoughts...",
fullscreen: state.fullscreen,
showConfirmBtn: true,
showCancelBtn: isEditing,
onConfirmBtnClick: handleSaveBtnClick,
onCancelBtnClick: handleCancelBtnClick,
onContentChange: handleContentChange,
}),
[isEditing]
[isEditing, state.fullscreen]
);
return (
<div className={"memo-editor-container " + (isEditing ? "edit-ing" : "")}>
<div className={`memo-editor-container ${isEditing ? "edit-ing" : ""} ${state.fullscreen ? "fullscreen" : ""}`}>
<p className={"tip-text " + (isEditing ? "" : "hidden")}>Editting...</p>
<Editor
ref={editorRef}
@@ -214,10 +238,13 @@ const MemoEditor: React.FC<Props> = () => {
})}
</div>
</div>
<div className="action-btn">
<button className="action-btn">
<img className="icon-img" src="/icons/image.svg" onClick={handleUploadFileBtnClick} />
<span className={`tip-text ${isUploadingResource ? "!block" : ""}`}>Uploading</span>
</div>
<span className={`tip-text ${state.isUploadingResource ? "!block" : ""}`}>Uploading</span>
</button>
<button className="action-btn" onClick={handleFullscreenBtnClick}>
<img className="icon-img" src={`/icons/${state.fullscreen ? "close" : "open"}-fullscreen.svg`} alt="" />
</button>
</>
}
/>
+2 -2
View File
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from "react";
import { memoService, shortcutService } from "../services";
import { useAppSelector } from "../store";
import { IMAGE_URL_REG, LINK_REG, MEMO_LINK_REG, TAG_REG } from "../helpers/consts";
import { IMAGE_URL_REG, LINK_URL_REG, MEMO_LINK_REG, TAG_REG } from "../helpers/consts";
import * as utils from "../helpers/utils";
import { checkShouldShowMemoWithFilters } from "../helpers/filter";
import toastHelper from "./Toast";
@@ -58,7 +58,7 @@ const MemoList: React.FC<Props> = () => {
if (memoType) {
if (memoType === "NOT_TAGGED" && memo.content.match(TAG_REG) !== null) {
shouldShow = false;
} else if (memoType === "LINKED" && memo.content.match(LINK_REG) === null) {
} else if (memoType === "LINKED" && memo.content.match(LINK_URL_REG) === null) {
shouldShow = false;
} else if (memoType === "IMAGED" && memo.content.match(IMAGE_URL_REG) === null) {
shouldShow = false;
+2 -2
View File
@@ -38,9 +38,9 @@ const ShortcutList: React.FC<Props> = () => {
<div className="shortcuts-wrapper">
<p className="title-text">
<span className="normal-text">Shortcuts</span>
<span className="btn" onClick={() => showCreateShortcutDialog()}>
<button className="btn" onClick={() => showCreateShortcutDialog()}>
<img src="/icons/add.svg" alt="add shortcut" />
</span>
</button>
</p>
<div className="shortcuts-container">
{sortedShortcuts.map((s) => {
+7 -15
View File
@@ -37,10 +37,8 @@ const UsageHeatMap: React.FC<Props> = () => {
const { memos } = useAppSelector((state) => state.memo);
const [allStat, setAllStat] = useState<DailyUsageStat[]>(getInitialUsageStat(usedDaysAmount, beginDayTimestemp));
const [popupStat, setPopupStat] = useState<DailyUsageStat | null>(null);
const [currentStat, setCurrentStat] = useState<DailyUsageStat | null>(null);
const containerElRef = useRef<HTMLDivElement>(null);
const popupRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const newStat: DailyUsageStat[] = getInitialUsageStat(usedDaysAmount, beginDayTimestemp);
@@ -54,18 +52,17 @@ const UsageHeatMap: React.FC<Props> = () => {
}, [memos]);
const handleUsageStatItemMouseEnter = useCallback((event: React.MouseEvent, item: DailyUsageStat) => {
setPopupStat(item);
if (!popupRef.current) {
return;
}
const tempDiv = document.createElement("div");
tempDiv.className = "usage-detail-container pop-up";
const bounding = utils.getElementBounding(event.target as HTMLElement);
popupRef.current.style.left = bounding.left + "px";
popupRef.current.style.top = bounding.top - 4 + "px";
tempDiv.style.left = bounding.left + "px";
tempDiv.style.top = bounding.top - 2 + "px";
tempDiv.innerHTML = `${item.count} memos on <span className="date-text">${new Date(item.timestamp as number).toDateString()}</span>`;
document.body.appendChild(tempDiv);
}, []);
const handleUsageStatItemMouseLeave = useCallback(() => {
setPopupStat(null);
document.body.querySelectorAll("div.usage-detail-container.pop-up").forEach((node) => node.remove());
}, []);
const handleUsageStatItemClick = useCallback((item: DailyUsageStat) => {
@@ -89,11 +86,6 @@ const UsageHeatMap: React.FC<Props> = () => {
<span className="tip-text"></span>
<span className="tip-text">Sat</span>
</div>
<div ref={popupRef} className={"usage-detail-container pop-up " + (popupStat ? "" : "hidden")}>
{popupStat?.count} memos on <span className="date-text">{new Date(popupStat?.timestamp as number).toDateString()}</span>
</div>
<div className="usage-heat-map">
{allStat.map((v, i) => {
const count = v.count;
+10 -20
View File
@@ -3,14 +3,13 @@ import * as utils from "../helpers/utils";
import userService from "../services/userService";
import { locationService } from "../services";
import { useAppSelector } from "../store";
import toastHelper from "./Toast";
import MenuBtnsPopup from "./MenuBtnsPopup";
import "../less/user-banner.less";
interface Props {}
const UserBanner: React.FC<Props> = () => {
const user = useAppSelector((state) => state.user.user);
const { user, owner } = useAppSelector((state) => state.user);
const { memos, tags } = useAppSelector((state) => state.memo);
const [shouldShowPopupBtns, setShouldShowPopupBtns] = useState(false);
const [username, setUsername] = useState("Memos");
@@ -18,24 +17,15 @@ const UserBanner: React.FC<Props> = () => {
const isVisitorMode = userService.isVisitorMode();
useEffect(() => {
const currentUserId = userService.getUserIdFromPath();
if (isVisitorMode && currentUserId) {
userService
.getUserById(currentUserId)
.then((user) => {
if (user) {
setUsername(user.name);
setCreatedDays(user ? Math.ceil((Date.now() - utils.getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24) : 0);
} else {
toastHelper.error("User not found");
}
})
.catch(() => {
// do nth
});
if (isVisitorMode) {
if (!owner) {
return;
}
setUsername(owner.name);
setCreatedDays(Math.ceil((Date.now() - utils.getTimeStampByDate(owner.createdTs)) / 1000 / 3600 / 24));
} else if (user) {
setUsername(user.name);
setCreatedDays(user ? Math.ceil((Date.now() - utils.getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24) : 0);
setCreatedDays(Math.ceil((Date.now() - utils.getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24));
}
}, []);
@@ -54,9 +44,9 @@ const UserBanner: React.FC<Props> = () => {
<span className="username-text">{username}</span>
{!isVisitorMode && user?.role === "HOST" ? <span className="tag">MOD</span> : null}
</div>
<span className="action-btn menu-popup-btn" onClick={handlePopupBtnClick}>
<button className="action-btn menu-popup-btn" onClick={handlePopupBtnClick}>
<img src="/icons/more.svg" className="icon-img" />
</span>
</button>
<MenuBtnsPopup shownStatus={shouldShowPopupBtns} setShownStatus={setShouldShowPopupBtns} />
</div>
<div className="status-text-container">
+10 -2
View File
@@ -1,7 +1,5 @@
import axios from "axios";
axios.defaults.withCredentials = true;
type ResponseObject<T> = {
data: T;
error?: string;
@@ -118,3 +116,13 @@ export function getTagList(tagFind?: TagFind) {
}
return axios.get<ResponseObject<string[]>>(`/api/tag?${queryList.join("&")}`);
}
export async function getRepoStarCount() {
const { data } = await axios.get(`https://api.github.com/repos/usememos/memos`, {
headers: {
Accept: "application/vnd.github.v3.star+json",
Authorization: "",
},
});
return data.stargazers_count as number;
}
+6 -6
View File
@@ -11,13 +11,13 @@ export const TOAST_ANIMATION_DURATION = 400;
export const DAILY_TIMESTAMP = 3600 * 24 * 1000;
// tag regex
export const TAG_REG = /#(.+?) /g;
export const TAG_REG = /#(\S+?) /g;
// URL regex
export const LINK_REG = /(https?:\/\/[^\s<\\*>']+)/g;
// image regex
// markdown image regex
export const IMAGE_URL_REG = /!\[.*?\]\((.+?)\)/g;
// markdown link regex
export const LINK_URL_REG = /\[(.*?)\]\((.+?)\)/g;
// linked memo regex
export const MEMO_LINK_REG = /\[@(.+?)\]\((.+?)\)/g;
export const MEMO_LINK_REG = /@\[(.+?)\]\((.+?)\)/g;
+2 -2
View File
@@ -1,4 +1,4 @@
import { IMAGE_URL_REG, LINK_REG, MEMO_LINK_REG, TAG_REG } from "./consts";
import { IMAGE_URL_REG, LINK_URL_REG, MEMO_LINK_REG, TAG_REG } from "./consts";
export const relationConsts = [
{ text: "And", value: "AND" },
@@ -142,7 +142,7 @@ export const checkShouldShowMemo = (memo: Memo, filter: Filter) => {
let matched = false;
if (value === "NOT_TAGGED" && memo.content.match(TAG_REG) === null) {
matched = true;
} else if (value === "LINKED" && memo.content.match(LINK_REG) !== null) {
} else if (value === "LINKED" && memo.content.match(LINK_URL_REG) !== null) {
matched = true;
} else if (value === "IMAGED" && memo.content.match(IMAGE_URL_REG) !== null) {
matched = true;
+6 -2
View File
@@ -17,8 +17,12 @@
@apply font-mono mx-1;
}
a {
@apply cursor-pointer underline-offset-2 underline text-blue-600;
> .addtion-info-container {
@apply flex flex-row justify-start items-center;
> .github-badge-container {
@apply mr-2;
}
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
.dialog-wrapper {
.flex(column, flex-start, center);
@apply fixed top-0 left-0 w-full h-full pt-16 z-100 overflow-x-hidden overflow-y-scroll bg-transparent transition-all;
@apply fixed top-0 left-0 w-full h-full pt-16 z-1000 overflow-x-hidden overflow-y-scroll bg-transparent transition-all;
.hide-scroll-bar();
&.showup {
-18
View File
@@ -37,24 +37,6 @@
> .tip-text {
@apply hidden ml-1 text-xs leading-5 text-gray-700 border border-gray-300 rounded-xl px-2;
}
&.tag-action {
@apply relative;
&:hover {
> .tag-list {
@apply flex;
}
}
> .tag-list {
@apply hidden flex-col justify-start items-start absolute top-6 left-0 mt-1 p-1 z-1 rounded w-32 max-h-52 overflow-auto bg-black;
> span {
@apply w-full text-white cursor-pointer rounded text-sm leading-6 px-2 hover:bg-gray-700;
}
}
}
}
}
+38
View File
@@ -0,0 +1,38 @@
.github-badge-container {
@apply h-7 flex flex-row justify-start items-center border rounded cursor-pointer hover:opacity-80;
> .github-icon {
@apply w-auto h-full px-2 border-r rounded-l flex flex-row justify-center items-center text-xs font-bold text-gray-800 bg-gray-100;
> .icon-img {
@apply w-4 h-auto mr-1;
}
}
> .count-text {
@apply px-3 text-xs font-bold text-gray-800;
&.pulse {
@apply text-sm;
animation: 1s linear 0s infinite pulse;
}
@keyframes pulse {
0% {
margin-bottom: 0px;
}
30% {
margin-bottom: 2px;
}
60% {
margin-bottom: 6px;
}
70% {
margin-bottom: 3px;
}
100% {
margin-bottom: 0px;
}
}
}
}
+2 -2
View File
@@ -12,10 +12,10 @@
}
> .memos-wrapper {
@apply relative w-full max-w-2xl min-h-full flex flex-col justify-start items-start px-4 sm:pr-6;
@apply relative flex-grow max-w-2xl min-h-full flex flex-col justify-start items-start px-4 sm:pr-6;
> .memos-editor-wrapper {
@apply sticky top-0 w-full h-full flex flex-col justify-start items-start z-1;
@apply sticky top-0 w-full h-full flex flex-col justify-start items-start z-10;
background-color: #f6f5f4;
}
}
+2 -2
View File
@@ -1,10 +1,10 @@
@import "./mixin.less";
.memo-content-text {
@apply w-full whitespace-pre-wrap break-words text-base leading-7;
@apply w-full whitespace-pre-wrap break-all text-base leading-7;
> p {
@apply inline-block w-full h-auto mb-1 last:mb-0 text-base leading-7 whitespace-pre-wrap break-words;
@apply inline-block w-full h-auto mb-1 last:mb-0 text-base leading-7 whitespace-pre-wrap break-all;
}
.tag-span {
+37 -1
View File
@@ -1,7 +1,25 @@
@import "./mixin.less";
.memo-editor-container {
@apply relative w-full max-h-full flex flex-col justify-start items-start bg-white p-4 rounded-lg border-2 border-gray-200;
@apply transition-all relative w-full max-h-full flex flex-col justify-start items-start bg-white p-4 rounded-lg border-2 border-gray-200;
&.fullscreen {
@apply fixed w-full h-full top-0 left-0 z-1000 border-none rounded-none sm:p-8;
background-color: #f6f5f4;
> .memo-editor {
@apply p-4 rounded-lg border shadow-lg flex flex-col flex-grow justify-start items-start relative w-full h-full bg-white;
> .common-editor-inputer {
@apply flex-grow w-full !h-full max-h-full;
}
.tag-action > .tag-list {
@apply bottom-7;
top: unset;
}
}
}
&.edit-ing {
border-color: @text-blue;
@@ -13,5 +31,23 @@
> .memo-editor {
@apply flex flex-col justify-start items-start relative w-full h-auto bg-white;
.tag-action {
@apply relative;
&:hover {
> .tag-list {
@apply flex;
}
}
> .tag-list {
@apply hidden flex-col justify-start items-start absolute top-6 left-0 mt-1 p-1 z-1 rounded w-32 max-h-52 overflow-auto bg-black;
> span {
@apply w-full text-white cursor-pointer rounded text-sm leading-6 px-2 hover:bg-gray-700;
}
}
}
}
}
+1 -1
View File
@@ -26,7 +26,7 @@
@apply hidden flex-col justify-start items-center absolute top-2 -right-4 flex-nowrap hover:flex p-3;
> .more-action-btns-container {
@apply w-28 h-auto p-1 whitespace-nowrap rounded-lg bg-white;
@apply w-28 h-auto p-1 z-1 whitespace-nowrap rounded-lg bg-white;
box-shadow: 0 0 8px 0 rgb(0 0 0 / 20%);
> .btns-container {
+2 -1
View File
@@ -86,7 +86,8 @@
@apply flex-col justify-start items-start absolute top-6 right-0 w-auto h-auto px-4 pt-3 hidden z-1;
> .action-btns-container {
@apply flex flex-col justify-start items-start w-24 h-auto p-1 whitespace-nowrap rounded-md bg-white shadow;
@apply flex flex-col justify-start items-start w-24 h-auto p-1 whitespace-nowrap rounded-md bg-white;
box-shadow: 0 0 8px 0 rgb(0 0 0 / 20%);
> .btn {
@apply w-full text-sm leading-6 py-1 px-3 rounded text-left hover:bg-gray-100;
+1 -1
View File
@@ -1,7 +1,7 @@
@import "./mixin.less";
.sidebar-wrapper {
@apply fixed sm:sticky top-0 left-0 hidden sm:!flex flex-col justify-start items-start w-64 h-screen py-4 pl-2 z-10 bg-white sm:bg-transparent shadow-2xl sm:shadow-none overflow-x-hidden overflow-y-auto transition-all;
@apply fixed sm:sticky top-0 left-0 z-20 sm:z-0 hidden sm:!flex flex-col justify-start items-start w-64 h-screen py-4 pl-2 bg-white sm:bg-transparent shadow-2xl sm:shadow-none overflow-x-hidden overflow-y-auto transition-all;
.hide-scroll-bar();
> .close-container {
+9 -5
View File
@@ -9,16 +9,20 @@
> .page-header-container {
@apply flex flex-col justify-start items-start w-full mb-4;
> .title-text {
@apply text-2xl;
> .title-container {
@apply w-full flex flex-row justify-between items-center;
> .icon-text {
@apply text-4xl;
> .title-text {
@apply text-2xl mb-2;
> .icon-text {
@apply text-4xl;
}
}
}
> .slogan-text {
@apply mt-2 text-sm text-gray-700;
@apply text-sm text-gray-700;
}
}
+3 -3
View File
@@ -12,7 +12,7 @@
@apply flex flex-col justify-start items-start relative w-full h-auto flex-nowrap mb-2 mt-1;
.subtags-container {
@apply flex flex-col justify-start items-start h-auto mt-1 ml-4 pl-1;
@apply flex flex-col justify-start items-start h-auto mt-1 ml-5 pl-1;
width: calc(100% - 18px);
min-width: 80px;
border-left: 2px solid @bg-gray;
@@ -32,7 +32,7 @@
}
> .tag-text-container {
@apply flex flex-row justify-start items-center overflow-hidden text-ellipsis shrink-0 leading-5 text-black;
@apply flex flex-row justify-start items-center overflow-hidden shrink-0 leading-5 text-black;
max-width: calc(100% - 24px);
> .icon-text {
@@ -40,7 +40,7 @@
}
> .tag-text {
@apply shrink-0;
@apply truncate;
}
}
+16 -18
View File
@@ -6,8 +6,7 @@
@stat-day-L4-bg: #216e39;
.usage-heat-map-wrapper {
.flex(row, flex-start, center);
@apply w-full h-32 flex-wrap pr-6 pb-3;
@apply flex flex-row justify-start items-center w-full h-32 flex-wrap pr-6 pb-3;
> .day-tip-text-container {
@apply w-6 h-full grid grid-rows-7;
@@ -56,24 +55,23 @@
}
}
}
}
> .usage-detail-container {
@apply fixed left-0 top-0 ml-2 -mt-9 p-2 -translate-x-1/2 select-none text-white text-xs rounded whitespace-nowrap;
background-color: rgba(0, 0, 0, 0.8);
z-index: 2;
.usage-detail-container {
@apply fixed left-0 top-0 ml-2 -mt-9 p-2 z-100 -translate-x-1/2 select-none text-white text-xs rounded whitespace-nowrap;
background-color: rgba(0, 0, 0, 0.8);
> .date-text {
@apply text-gray-300;
}
> .date-text {
@apply text-gray-300;
}
&::before {
content: "";
position: absolute;
bottom: -4px;
left: calc(50% - 5px);
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid rgba(0, 0, 0, 0.8);
}
&::before {
content: "";
position: absolute;
bottom: -4px;
left: calc(50% - 5px);
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid rgba(0, 0, 0, 0.8);
}
}
+14 -11
View File
@@ -15,19 +15,22 @@ function Home() {
useEffect(() => {
userService
.doSignIn()
.initialState()
.catch()
.finally(async () => {
if (!userService.getState().user) {
if (userService.isVisitorMode()) {
const currentUserId = userService.getUserIdFromPath() as number;
const user = await userService.getUserById(currentUserId);
if (!user) {
toastHelper.error("User not found");
}
} else {
locationService.replaceHistory("/signin");
return;
const { host, owner, user } = userService.getState();
if (!host) {
locationService.replaceHistory("/signin");
return;
}
if (userService.isVisitorMode()) {
if (!owner) {
toastHelper.error("User not found");
}
} else {
if (!user) {
locationService.replaceHistory(`/u/${host.id}`);
}
}
loadingState.setFinish();
+7 -3
View File
@@ -4,6 +4,7 @@ import { validate, ValidatorConfig } from "../helpers/validator";
import useLoading from "../hooks/useLoading";
import { locationService, userService } from "../services";
import toastHelper from "../components/Toast";
import GitHubBadge from "../components/GitHubBadge";
import "../less/signin.less";
interface Props {}
@@ -114,9 +115,12 @@ const Signin: React.FC<Props> = () => {
<div className="page-wrapper signin">
<div className="page-container">
<div className="page-header-container">
<p className="title-text">
<span className="icon-text"></span> Memos
</p>
<div className="title-container">
<p className="title-text">
<span className="icon-text"></span> Memos
</p>
<GitHubBadge />
</div>
<p className="slogan-text">
An <i>open source</i>, <i>self-hosted</i> knowledge base that works with a SQLite db file.
</p>
+11 -7
View File
@@ -17,9 +17,10 @@ const memoService = {
},
fetchAllMemos: async () => {
const memoFind: MemoFind = {
creatorId: userService.getUserIdFromPath(),
};
const memoFind: MemoFind = {};
if (userService.isVisitorMode()) {
memoFind.creatorId = userService.getUserIdFromPath();
}
const { data } = (await api.getMemoList(memoFind)).data;
const memos = data.filter((m) => m.rowStatus !== "ARCHIVED").map((m) => convertResponseModelMemo(m));
store.dispatch(setMemos(memos));
@@ -29,9 +30,11 @@ const memoService = {
fetchArchivedMemos: async () => {
const memoFind: MemoFind = {
creatorId: userService.getUserIdFromPath(),
rowStatus: "ARCHIVED",
};
if (userService.isVisitorMode()) {
memoFind.creatorId = userService.getUserIdFromPath();
}
const { data } = (await api.getMemoList(memoFind)).data;
const archivedMemos = data.map((m) => {
return convertResponseModelMemo(m);
@@ -50,9 +53,10 @@ const memoService = {
},
updateTagsState: async () => {
const tagFind: TagFind = {
creatorId: userService.getUserIdFromPath(),
};
const tagFind: TagFind = {};
if (userService.isVisitorMode()) {
tagFind.creatorId = userService.getUserIdFromPath();
}
const { data } = (await api.getTagList(tagFind)).data;
store.dispatch(setTags(data));
},
+23 -5
View File
@@ -2,7 +2,7 @@ import { isUndefined } from "lodash-es";
import { locationService } from ".";
import * as api from "../helpers/api";
import store from "../store";
import { setUser, patchUser } from "../store/modules/user";
import { setUser, patchUser, setHost, setOwner } from "../store/modules/user";
const convertResponseModelUser = (user: User): User => {
return {
@@ -17,12 +17,30 @@ const userService = {
return store.getState().user;
},
isVisitorMode: () => {
return !isUndefined(userService.getUserIdFromPath());
initialState: async () => {
const {
data: { host },
} = (await api.getSystemStatus()).data;
if (host) {
store.dispatch(setHost(convertResponseModelUser(host)));
}
const ownerUserId = userService.getUserIdFromPath();
if (ownerUserId) {
const { data: owner } = (await api.getUserById(ownerUserId)).data;
if (owner) {
store.dispatch(setOwner(convertResponseModelUser(owner)));
}
}
const { data: user } = (await api.getUser()).data;
if (user) {
store.dispatch(setUser(convertResponseModelUser(user)));
}
},
getCurrentUserId: () => {
return userService.getUserIdFromPath() ?? store.getState().user.user?.id;
isVisitorMode: () => {
return !isUndefined(userService.getUserIdFromPath());
},
getUserIdFromPath: () => {
+18 -1
View File
@@ -1,6 +1,11 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface State {
// host is the user who hist the system
host?: User;
// owner is the user who owns the page. If in `/u/101`, then owner's id is `101`
owner?: User;
// user is the user who is currently logged in
user?: User;
}
@@ -8,6 +13,18 @@ const userSlice = createSlice({
name: "user",
initialState: {} as State,
reducers: {
setHost: (state, action: PayloadAction<User | undefined>) => {
return {
...state,
host: action.payload,
};
},
setOwner: (state, action: PayloadAction<User | undefined>) => {
return {
...state,
owner: action.payload,
};
},
setUser: (state, action: PayloadAction<User | undefined>) => {
return {
...state,
@@ -26,6 +43,6 @@ const userSlice = createSlice({
},
});
export const { setUser, patchUser } = userSlice.actions;
export const { setHost, setOwner, setUser, patchUser } = userSlice.actions;
export default userSlice.reducer;
+1
View File
@@ -17,6 +17,7 @@ interface Memo {
interface MemoCreate {
content: string;
visibility?: Visibility;
createdTs?: TimeStamp;
}
+1
View File
@@ -20,6 +20,7 @@ module.exports = {
},
zIndex: {
1: "1",
20: "20",
100: "100",
1000: "1000",
},
+7
View File
@@ -1,11 +1,13 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { resolve } from "path";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
cors: true,
port: 3000,
proxy: {
"/api": {
target: "http://localhost:8080/",
@@ -17,4 +19,9 @@ export default defineConfig({
},
},
},
resolve: {
alias: {
"@/": `${resolve(__dirname, "src")}/`,
},
},
});
+755 -686
View File
File diff suppressed because it is too large Load Diff