Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c479d5e56 | |||
| 8a2008d6d7 | |||
| 9c842d0a40 | |||
| 0dc377550f | |||
| 8a91b0ad9d | |||
| 1b50ab5dca | |||
| 6053df050c | |||
| 3517c6181d | |||
| 2e126c71f0 | |||
| 46d7ecca88 | |||
| 48d8c6ee0f | |||
| 5fd3cfdb61 | |||
| 10d710cf03 | |||
| 21702b615a | |||
| d75338b6e9 | |||
| b85af714f5 | |||
| a2b32e0b75 | |||
| 0505598509 | |||
| 91d45d6d46 |
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||

|
||||

|
||||
|
||||
## 🎯 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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" {
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 90 KiB |
@@ -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>
|
||||
@@ -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(),
|
||||
}))
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ INSERT INTO
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
101,
|
||||
1001,
|
||||
101,
|
||||
1
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 |
|
After Width: | Height: | Size: 2.5 KiB |
@@ -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 |
@@ -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 |
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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"}`}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -17,6 +17,7 @@ interface Memo {
|
||||
|
||||
interface MemoCreate {
|
||||
content: string;
|
||||
visibility?: Visibility;
|
||||
createdTs?: TimeStamp;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ module.exports = {
|
||||
},
|
||||
zIndex: {
|
||||
1: "1",
|
||||
20: "20",
|
||||
100: "100",
|
||||
1000: "1000",
|
||||
},
|
||||
|
||||
@@ -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")}/`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||