Compare commits

...

30 Commits

Author SHA1 Message Date
boojack eea826dd5c chore: release v0.2.2 2022-07-22 23:42:26 +08:00
boojack 3c5b0ea90a chore: update style 2022-07-22 23:31:25 +08:00
boojack 15e1037433 chore: create backup when migration 2022-07-22 23:21:12 +08:00
boojack 5da4c98f05 chore: update icon button styles 2022-07-19 21:46:38 +08:00
boojack a73ee7aefc chore: update readme (#124)
* chore: update readme

* chore: add space

* chore: add emoji
2022-07-18 23:16:39 +08:00
boojack 6c5bea9caf chore: update html2image 2022-07-17 10:42:35 +08:00
boojack 93ba2f4fab chore: fix icon style 2022-07-17 10:29:12 +08:00
boojack 9417797b99 chore: use fontawesome instead of material icons 2022-07-17 09:58:56 +08:00
boojack 167e5596f2 fix: generate html image in safari (#123) 2022-07-17 01:52:29 +08:00
boojack 2a1e34fe03 chore: update material icons 2022-07-16 11:51:03 +08:00
boojack 3de00cf4a8 chore: add dayjs to parse datetime 2022-07-16 11:50:40 +08:00
boojack 1d55545e30 chore: update github badge style 2022-07-16 09:52:57 +08:00
boojack 9b5a555d1f chore: release v0.2.1 (#120)
* chore: release `v0.2.1`

* chore: add tg group link
2022-07-15 22:49:22 +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
100 changed files with 1415 additions and 1146 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
+17 -13
View File
@@ -4,38 +4,42 @@
<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)
## 🎯 Intentions
- ✍️ Write down the light-card memos very easily;
- 🏗️ Build the fragmented knowledge management tool for yourself;
- 📒 For noting your 📅 daily/weekly plans, 💡 fantastic ideas, 📕 reading thoughts...
![demo](https://raw.githubusercontent.com/usememos/memos/main/resources/demo.webp)
## ✨ Features
- 🦄 Fully open source;
- 👍 Write in the plain textarea without any burden;
- 🤠 Great UI and never miss any detail;
- 🚀 Super quick self-hosted with `Docker` and `SQLite`;
- 📜 Writing in plain textarea without any burden,
- and support some useful markdown syntax 💪.
- 🌄 Share the memo in a pretty image or personal page like Twitter;
- 🚀 Fast self-hosting with `Docker`;
- 🤠 Pleasant UI and UX;
## ⚓️ 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` tag 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.2"
// DevVersion is the service current development version.
var DevVersion = "0.2.0"
var DevVersion = "0.2.2"
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 {
+2 -2
View File
@@ -38,8 +38,7 @@ func checkDSN(dataDir string) (string, error) {
dataDir = strings.TrimRight(dataDir, "/")
if _, err := os.Stat(dataDir); err != nil {
error := fmt.Errorf("unable to access -data %s, err %w", dataDir, err)
return "", error
return "", fmt.Errorf("unable to access data folder %s, err %w", dataDir, err)
}
return dataDir, nil
@@ -67,6 +66,7 @@ func GetProfile() *Profile {
os.Exit(1)
}
profile.Data = dataDir
profile.DSN = fmt.Sprintf("%s/memos_%s.db", dataDir, profile.Mode)
profile.Version = common.GetCurrentVersion(profile.Mode)
+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)
+29 -15
View File
@@ -6,9 +6,11 @@ import (
"errors"
"fmt"
"io/fs"
"io/ioutil"
"os"
"regexp"
"sort"
"time"
"github.com/usememos/memos/common"
"github.com/usememos/memos/server/profile"
@@ -24,37 +26,33 @@ var seedFS embed.FS
type DB struct {
// sqlite db connection instance
Db *sql.DB
// datasource name
DSN string
// mode should be prod or dev
mode string
Db *sql.DB
profile *profile.Profile
}
// NewDB returns a new instance of DB associated with the given datasource name.
func NewDB(profile *profile.Profile) *DB {
db := &DB{
DSN: profile.DSN,
mode: profile.Mode,
profile: profile,
}
return db
}
func (db *DB) Open() (err error) {
// Ensure a DSN is set before attempting to open the database.
if db.DSN == "" {
if db.profile.DSN == "" {
return fmt.Errorf("dsn required")
}
// Connect to the database.
sqlDB, err := sql.Open("sqlite3", db.DSN)
sqlDB, err := sql.Open("sqlite3", db.profile.DSN)
if err != nil {
return fmt.Errorf("failed to open db with dsn: %s, err: %w", db.DSN, err)
return fmt.Errorf("failed to open db with dsn: %s, err: %w", db.profile.DSN, err)
}
db.Db = sqlDB
// If mode is dev, we should migrate and seed the database.
if db.mode == "dev" {
if db.profile.Mode == "dev" {
if err := db.applyLatestSchema(); err != nil {
return fmt.Errorf("failed to apply latest schema: %w", err)
}
@@ -63,7 +61,7 @@ func (db *DB) Open() (err error) {
}
} else {
// If db file not exists, we should migrate the database.
if _, err := os.Stat(db.DSN); errors.Is(err, os.ErrNotExist) {
if _, err := os.Stat(db.profile.DSN); errors.Is(err, os.ErrNotExist) {
err := db.applyLatestSchema()
if err != nil {
return fmt.Errorf("failed to apply latest schema: %w", err)
@@ -74,7 +72,7 @@ func (db *DB) Open() (err error) {
return fmt.Errorf("failed to create migration_history table: %w", err)
}
currentVersion := common.GetCurrentVersion(db.mode)
currentVersion := common.GetCurrentVersion(db.profile.Mode)
migrationHistory, err := findMigrationHistory(db.Db, &MigrationHistoryFind{})
if err != nil {
return err
@@ -90,18 +88,34 @@ func (db *DB) Open() (err error) {
if common.IsVersionGreaterThan(currentVersion, migrationHistory.Version) {
minorVersionList := getMinorVersionList()
// backup the raw database file before migration
rawBytes, err := ioutil.ReadFile(db.profile.DSN)
if err != nil {
return fmt.Errorf("failed to read raw database file, err: %w", err)
}
backupDBFilePath := fmt.Sprintf("%s/memos_%s_%d_backup.db", db.profile.Data, db.profile.Version, time.Now().Unix())
if err := ioutil.WriteFile(backupDBFilePath, rawBytes, 0644); err != nil {
return fmt.Errorf("failed to write raw database file, err: %w", err)
}
println("succeed to copy a backup database file")
println("start migrate")
for _, minorVersion := range minorVersionList {
normalizedVersion := minorVersion + ".0"
if common.IsVersionGreaterThan(normalizedVersion, migrationHistory.Version) && common.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
println("applying migration for", normalizedVersion)
err := db.applyMigrationForMinorVersion(minorVersion)
if err != nil {
if err := db.applyMigrationForMinorVersion(minorVersion); err != nil {
return fmt.Errorf("failed to apply minor version migration: %w", err)
}
}
}
println("end migrate")
// remove the created backup db file after migrate succeed
if err := os.Remove(backupDBFilePath); err != nil {
println(fmt.Sprintf("Failed to remove temp database file, err %v", 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)
}
+2 -1
View File
@@ -2,10 +2,11 @@
<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>
<script src="https://kit.fontawesome.com/41e3aaa6af.js" crossorigin="anonymous"></script>
</head>
<body>
<div id="root"></div>
+5 -3
View File
@@ -1,6 +1,6 @@
{
"name": "memos",
"version": "0.1.3",
"version": "0.2.2",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
@@ -10,6 +10,7 @@
"dependencies": {
"@reduxjs/toolkit": "^1.8.1",
"axios": "^0.27.2",
"dayjs": "^1.11.3",
"lodash-es": "^4.17.21",
"qs": "^6.11.0",
"react": "^18.1.0",
@@ -18,12 +19,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 +36,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

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M22.5 38V25.5H10V22.5H22.5V10H25.5V22.5H38V25.5H25.5V38Z"/></svg>

Before

Width:  |  Height:  |  Size: 137 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M28.05 36 16 23.95 28.05 11.9l2.15 2.15-9.9 9.9 9.9 9.9Z"/></svg>

Before

Width:  |  Height:  |  Size: 137 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="m18.75 36-2.15-2.15 9.9-9.9-9.9-9.9 2.15-2.15L30.8 23.95Z"/></svg>

Before

Width:  |  Height:  |  Size: 138 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#37352f"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zM17.99 9l-1.41-1.42-6.59 6.59-2.58-2.57-1.42 1.41 4 3.99z"/></svg>

Before

Width:  |  Height:  |  Size: 308 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#37352f"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>

Before

Width:  |  Height:  |  Size: 249 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M12.45 37.65 10.35 35.55 21.9 24 10.35 12.45 12.45 10.35 24 21.9 35.55 10.35 37.65 12.45 26.1 24 37.65 35.55 35.55 37.65 24 26.1Z"/></svg>

Before

Width:  |  Height:  |  Size: 210 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M11 40q-1.2 0-2.1-.9Q8 38.2 8 37v-7.15h3V37h26v-7.15h3V37q0 1.2-.9 2.1-.9.9-2.1.9Zm13-7.65-9.65-9.65 2.15-2.15 6 6V8h3v18.55l6-6 2.15 2.15Z"/></svg>

Before

Width:  |  Height:  |  Size: 220 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M9 39H11.2L33.35 16.85L31.15 14.65L9 36.8ZM39.7 14.7 33.3 8.3 35.4 6.2Q36.25 5.35 37.5 5.35Q38.75 5.35 39.6 6.2L41.8 8.4Q42.65 9.25 42.65 10.5Q42.65 11.75 41.8 12.6ZM37.6 16.8 12.4 42H6V35.6L31.2 10.4ZM32.25 15.75 31.15 14.65 33.35 16.85Z"/></svg>

Before

Width:  |  Height:  |  Size: 319 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-4.86 8.86l-3 3.87L9 13.14 6 17h12l-3.86-5.14z"/></svg>

Before

Width:  |  Height:  |  Size: 296 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#37352f"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/></svg>

Before

Width:  |  Height:  |  Size: 204 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#FFFFFF"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>

Before

Width:  |  Height:  |  Size: 306 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#37352f"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>

Before

Width:  |  Height:  |  Size: 306 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M31.7 25.6 36 29.45V32.45H25.5V44.5L24 46L22.5 44.5V32.45H12V29.45L16 25.6V9H13.5V6H34.2V9H31.7ZM16.05 29.45H31.65L28.7 26.7V9H19V26.7ZM23.85 29.45Z"/></svg>

Before

Width:  |  Height:  |  Size: 229 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#37352f"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>

Before

Width:  |  Height:  |  Size: 393 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M36.35 44Q34 44 32.325 42.325Q30.65 40.65 30.65 38.3Q30.65 37.95 30.725 37.475Q30.8 37 30.95 36.6L15.8 27.8Q15.05 28.65 13.95 29.175Q12.85 29.7 11.7 29.7Q9.35 29.7 7.675 28.025Q6 26.35 6 24Q6 21.6 7.675 19.95Q9.35 18.3 11.7 18.3Q12.85 18.3 13.9 18.75Q14.95 19.2 15.8 20.05L30.95 11.35Q30.8 11 30.725 10.55Q30.65 10.1 30.65 9.7Q30.65 7.3 32.325 5.65Q34 4 36.35 4Q38.75 4 40.4 5.65Q42.05 7.3 42.05 9.7Q42.05 12.05 40.4 13.725Q38.75 15.4 36.35 15.4Q35.2 15.4 34.125 15.025Q33.05 14.65 32.3 13.8L17.15 22.2Q17.25 22.6 17.325 23.125Q17.4 23.65 17.4 24Q17.4 24.35 17.325 24.75Q17.25 25.15 17.15 25.55L32.3 34.15Q33.05 33.45 34.05 33.025Q35.05 32.6 36.35 32.6Q38.75 32.6 40.4 34.25Q42.05 35.9 42.05 38.3Q42.05 40.65 40.4 42.325Q38.75 44 36.35 44ZM36.35 12.4Q37.5 12.4 38.275 11.625Q39.05 10.85 39.05 9.7Q39.05 8.55 38.275 7.775Q37.5 7 36.35 7Q35.2 7 34.425 7.775Q33.65 8.55 33.65 9.7Q33.65 10.85 34.425 11.625Q35.2 12.4 36.35 12.4ZM11.7 26.7Q12.85 26.7 13.625 25.925Q14.4 25.15 14.4 24Q14.4 22.85 13.625 22.075Q12.85 21.3 11.7 21.3Q10.55 21.3 9.775 22.075Q9 22.85 9 24Q9 25.15 9.775 25.925Q10.55 26.7 11.7 26.7ZM36.35 41Q37.5 41 38.275 40.225Q39.05 39.45 39.05 38.3Q39.05 37.15 38.275 36.375Q37.5 35.6 36.35 35.6Q35.2 35.6 34.425 36.375Q33.65 37.15 33.65 38.3Q33.65 39.45 34.425 40.225Q35.2 41 36.35 41ZM36.35 9.7Q36.35 9.7 36.35 9.7Q36.35 9.7 36.35 9.7Q36.35 9.7 36.35 9.7Q36.35 9.7 36.35 9.7Q36.35 9.7 36.35 9.7Q36.35 9.7 36.35 9.7Q36.35 9.7 36.35 9.7Q36.35 9.7 36.35 9.7ZM11.7 24Q11.7 24 11.7 24Q11.7 24 11.7 24Q11.7 24 11.7 24Q11.7 24 11.7 24Q11.7 24 11.7 24Q11.7 24 11.7 24Q11.7 24 11.7 24Q11.7 24 11.7 24ZM36.35 38.3Q36.35 38.3 36.35 38.3Q36.35 38.3 36.35 38.3Q36.35 38.3 36.35 38.3Q36.35 38.3 36.35 38.3Q36.35 38.3 36.35 38.3Q36.35 38.3 36.35 38.3Q36.35 38.3 36.35 38.3Q36.35 38.3 36.35 38.3Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><path d="M20,10V8h-4V4h-2v4h-4V4H8v4H4v2h4v4H4v2h4v4h2v-4h4v4h2v-4h4v-2h-4v-4H20z M14,14h-4v-4h4V14z"/></g></svg>

Before

Width:  |  Height:  |  Size: 301 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M24 31.5q3.55 0 6.025-2.475Q32.5 26.55 32.5 23q0-3.55-2.475-6.025Q27.55 14.5 24 14.5q-3.55 0-6.025 2.475Q15.5 19.45 15.5 23q0 3.55 2.475 6.025Q20.45 31.5 24 31.5Zm0-2.9q-2.35 0-3.975-1.625T18.4 23q0-2.35 1.625-3.975T24 17.4q2.35 0 3.975 1.625T29.6 23q0 2.35-1.625 3.975T24 28.6Zm0 9.4q-7.3 0-13.2-4.15Q4.9 29.7 2 23q2.9-6.7 8.8-10.85Q16.7 8 24 8q7.3 0 13.2 4.15Q43.1 16.3 46 23q-2.9 6.7-8.8 10.85Q31.3 38 24 38Zm0-15Zm0 12q6.05 0 11.125-3.275T42.85 23q-2.65-5.45-7.725-8.725Q30.05 11 24 11t-11.125 3.275Q7.8 17.55 5.1 23q2.7 5.45 7.775 8.725Q17.95 35 24 35Z"/></svg>

Before

Width:  |  Height:  |  Size: 638 B

+5 -6
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 {}
@@ -36,7 +37,7 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
<span className="icon-text">🤠</span>About <b>Memos</b>
</p>
<button className="btn close-btn" onClick={handleCloseBtnClick}>
<img className="icon-img" src="/icons/close.svg" />
<i className="fa-solid fa-xmark fa-lg"></i>
</button>
</div>
<div className="dialog-content-container">
@@ -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>
</>
);
+1 -1
View File
@@ -37,7 +37,7 @@ const ArchivedMemoDialog: React.FC<Props> = (props: Props) => {
Archived Memos
</p>
<button className="btn close-btn" onClick={destroy}>
<img className="icon-img" src="/icons/close.svg" />
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
</button>
</div>
<div className="dialog-content-container">
+1 -1
View File
@@ -70,7 +70,7 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
<div className="dialog-header-container">
<p className="title-text">Change Password</p>
<button className="btn close-btn" onClick={handleCloseBtnClick}>
<img className="icon-img" src="/icons/close.svg" />
<i className="fa-solid fa-xmark fa-lg"></i>
</button>
</div>
<div className="dialog-content-container">
@@ -41,7 +41,7 @@ const ConfirmResetOpenIdDialog: React.FC<Props> = ({ destroy }: Props) => {
<div className="dialog-header-container">
<p className="title-text">Reset Open API</p>
<button className="btn close-btn" onClick={handleCloseBtnClick}>
<img className="icon-img" src="/icons/close.svg" />
<i className="fa-solid fa-xmark fa-lg"></i>
</button>
</div>
<div className="dialog-content-container">
+2 -2
View File
@@ -100,7 +100,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
{shortcutId ? "Edit Shortcut" : "Create Shortcut"}
</p>
<button className="btn close-btn" onClick={destroy}>
<img className="icon-img" src="/icons/close.svg" />
<i className="fa-solid fa-xmark fa-lg"></i>
</button>
</div>
<div className="dialog-content-container">
@@ -296,7 +296,7 @@ const FilterInputer: React.FC<MemoFilterInputerProps> = (props: MemoFilterInpute
/>
{inputElements}
<img className="remove-btn" src="/icons/close.svg" onClick={handleRemoveBtnClick} />
<i className="fa-solid fa-xmark remove-btn" onClick={handleRemoveBtnClick}></i>
</div>
);
};
+12 -12
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)}>
<img className="icon-img" src="/icons/arrow-left.svg" />
</span>
<span 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}>
<img className="icon-img" src="/icons/share.svg" />
</span>
<span className="btn-text" onClick={() => props.destroy()}>
<img className="icon-img" src="/icons/close.svg" />
</span>
<button className="btn-text" onClick={() => setCurrentDateStamp(currentDateStamp - DAILY_TIMESTAMP)}>
<i className="fa-solid fa-chevron-left icon-img"></i>
</button>
<button className="btn-text" onClick={() => setCurrentDateStamp(currentDateStamp + DAILY_TIMESTAMP)}>
<i className="fa-solid fa-chevron-right icon-img"></i>
</button>
<button className="btn-text" onClick={handleShareBtnClick}>
<i className="fa-solid fa-share-nodes icon-img"></i>
</button>
<button className="btn-text" onClick={() => props.destroy()}>
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
</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,
+30
View File
@@ -0,0 +1,30 @@
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);
});
}, []);
return (
<a className="github-badge-container" href="https://github.com/usememos/memos">
<div className="github-icon">
<i className="fa-brands fa-github fa-lg icon-img"></i>
Star
</div>
<div className="count-text">
{starCount || ""}
<span className="icon-text">🌟</span>
</div>
</a>
);
};
export default GitHubBadge;
+40 -26
View File
@@ -1,7 +1,8 @@
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 * as utils from "../helpers/utils";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { IMAGE_URL_REG, LINK_URL_REG, MEMO_LINK_REG, TAG_REG, UNKNOWN_ID } from "../helpers/consts";
import { DONE_BLOCK_REG, parseMarkedToHtml, TODO_BLOCK_REG } from "../helpers/marked";
import { editorStateService, locationService, memoService, userService } from "../services";
import Only from "./common/OnlyWhen";
@@ -11,29 +12,37 @@ import showMemoCardDialog from "./MemoCardDialog";
import showShareMemoImageDialog from "./ShareMemoImageDialog";
import "../less/memo.less";
dayjs.extend(relativeTime);
const MAX_MEMO_CONTAINER_HEIGHT = 384;
type ExpandButtonStatus = -1 | 0 | 1;
interface Props {
memo: Memo;
}
type ExpandButtonStatus = -1 | 0 | 1;
interface State {
expandButtonStatus: ExpandButtonStatus;
}
export const getFormatedMemoCreatedAtStr = (createdTs: number): string => {
if (Date.now() - createdTs < 1000 * 60 * 60 * 24) {
return dayjs(createdTs).fromNow();
} else {
return dayjs(createdTs).format("YYYY/MM/DD HH:mm:ss");
}
};
const Memo: React.FC<Props> = (props: Props) => {
const { memo: propsMemo } = props;
const memo = {
...propsMemo,
createdAtStr: utils.getDateTimeString(propsMemo.createdTs),
};
const memo = props.memo;
const [state, setState] = useState<State>({
expandButtonStatus: -1,
});
const [createdAtStr, setCreatedAtStr] = useState<string>(getFormatedMemoCreatedAtStr(memo.createdTs));
const memoContainerRef = useRef<HTMLDivElement>(null);
const imageUrls = Array.from(memo.content.match(IMAGE_URL_REG) ?? []).map((s) => s.replace(IMAGE_URL_REG, "$1"));
const isVisitorMode = userService.isVisitorMode();
useEffect(() => {
if (!memoContainerRef) {
@@ -46,6 +55,12 @@ const Memo: React.FC<Props> = (props: Props) => {
expandButtonStatus: 0,
});
}
if (Date.now() - memo.createdTs < 1000 * 60 * 60 * 24) {
setInterval(() => {
setCreatedAtStr(dayjs(memo.createdTs).fromNow());
}, 1000 * 1);
}
}, []);
const handleShowMemoStoryDialog = () => {
@@ -146,9 +161,8 @@ const Memo: React.FC<Props> = (props: Props) => {
}
};
const handleShowMoreBtnClick = () => {
const handleExpandBtnClick = () => {
setState({
...state,
expandButtonStatus: Number(Boolean(!state.expandButtonStatus)) as ExpandButtonStatus,
});
};
@@ -156,32 +170,32 @@ const Memo: React.FC<Props> = (props: Props) => {
return (
<div className={`memo-wrapper ${"memos-" + memo.id} ${memo.pinned ? "pinned" : ""}`}>
<div className="memo-top-wrapper">
<span className="time-text" onClick={handleShowMemoStoryDialog}>
{memo.createdAtStr}
<div className="status-text-container" onClick={handleShowMemoStoryDialog}>
<span className="time-text">{createdAtStr}</span>
<Only when={memo.pinned}>
<span className="ml-2">PINNED</span>
<span className="status-text">PINNED</span>
</Only>
<Only when={memo.visibility === "PUBLIC"}>
<span className="ml-2">PUBLIC</span>
<Only when={memo.visibility === "PUBLIC" && !isVisitorMode}>
<span className="status-text">PUBLIC</span>
</Only>
</span>
</div>
<div className={`btns-container ${userService.isVisitorMode() ? "!hidden" : ""}`}>
<span className="btn more-action-btn">
<img className="icon-img" src="/icons/more.svg" />
<i className="fa-solid fa-ellipsis icon-img"></i>
</span>
<div className="more-action-btns-wrapper">
<div className="more-action-btns-container">
<div className="btns-container">
<div className="btn" onClick={handleTogglePinMemoBtnClick}>
<img className="icon-img" src="/icons/pin.svg" alt="" />
<i className={`fa-solid fa-thumbtack icon-img ${memo.pinned ? "" : "opacity-20"}`}></i>
<span className="tip-text">{memo.pinned ? "Unpin" : "Pin"}</span>
</div>
<div className="btn" onClick={handleEditMemoClick}>
<img className="icon-img" src="/icons/edit.svg" alt="" />
<i className="fa-solid fa-pen-to-square icon-img"></i>
<span className="tip-text">Edit</span>
</div>
<div className="btn" onClick={handleGenMemoImageBtnClick}>
<img className="icon-img" src="/icons/share.svg" alt="" />
<i className="fa-solid fa-share-nodes icon-img"></i>
<span className="tip-text">Share</span>
</div>
</div>
@@ -206,9 +220,9 @@ const Memo: React.FC<Props> = (props: Props) => {
></div>
{state.expandButtonStatus !== -1 && (
<div className="expand-btn-container">
<span className={`btn ${state.expandButtonStatus === 0 ? "expand-btn" : "fold-btn"}`} onClick={handleShowMoreBtnClick}>
<span className={`btn ${state.expandButtonStatus === 0 ? "expand-btn" : "fold-btn"}`} onClick={handleExpandBtnClick}>
{state.expandButtonStatus === 0 ? "Expand" : "Fold"}
<img className="icon-img" src="/icons/arrow-right.svg" alt="" />
<i className="fa-solid fa-chevron-right icon-img"></i>
</span>
</div>
)}
@@ -229,9 +243,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);
+20 -6
View File
@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from "react";
import { editorStateService, memoService, userService } from "../services";
import { IMAGE_URL_REG, MEMO_LINK_REG, UNKNOWN_ID } from "../helpers/consts";
import * as utils from "../helpers/utils";
import { editorStateService, memoService, userService } from "../services";
import { parseHtmlToRawText } from "../helpers/marked";
import Only from "./common/OnlyWhen";
import toastHelper from "./Toast";
@@ -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;
@@ -104,6 +103,18 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
editorStateService.setEditMemoWithId(memo.id);
}, [memo.id]);
const handlePinClick = async () => {
if (memo.pinned) {
await memoService.unpinMemo(memo.id);
} else {
await memoService.pinMemo(memo.id);
}
setMemo({
...memo,
pinned: !memo.pinned,
});
};
const handleVisibilityClick = async () => {
const visibility = memo.visibility === "PRIVATE" ? "PUBLIC" : "PRIVATE";
await memoService.patchMemo({
@@ -124,16 +135,19 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
<div className="btns-container">
<Only when={!userService.isVisitorMode()}>
<>
<button className="btn edit-btn" onClick={handleVisibilityClick}>
<img className={`icon-img ${memo.visibility === "PRIVATE" ? "opacity-30" : ""}`} src="/icons/visibility.svg" />
<button className="btn" onClick={handlePinClick}>
<i className={`fa-solid fa-thumbtack icon-img ${memo.pinned ? "" : "opacity-20"}`}></i>
</button>
<button className="btn" onClick={handleVisibilityClick}>
<i className={`fa-solid fa-eye icon-img ${memo.visibility === "PUBLIC" ? "" : "opacity-20"}`}></i>
</button>
<button className="btn edit-btn" onClick={handleEditMemoBtnClick}>
<img className="icon-img" src="/icons/edit.svg" />
<i className="fa-solid fa-pen-to-square icon-img"></i>
</button>
</>
</Only>
<button className="btn close-btn" onClick={props.destroy}>
<img className="icon-img" src="/icons/close.svg" />
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
</button>
</div>
</div>
+46 -19
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}
@@ -207,17 +231,20 @@ const MemoEditor: React.FC<Props> = () => {
tools={
<>
<div className="action-btn tag-action">
<img className="icon-img" src="/icons/tag.svg" />
<i className="fa-solid fa-hashtag icon-img"></i>
<div ref={tagSeletorRef} className="tag-list" onClick={handleTagSeletorClick}>
{tags.map((t) => {
return <span key={t}>{t}</span>;
})}
</div>
</div>
<div className="action-btn">
<img className="icon-img" src="/icons/image.svg" onClick={handleUploadFileBtnClick} />
<span className={`tip-text ${isUploadingResource ? "!block" : ""}`}>Uploading</span>
</div>
<button className="action-btn">
<i className="fa-solid fa-image icon-img" onClick={handleUploadFileBtnClick}></i>
<span className={`tip-text ${state.isUploadingResource ? "!block" : ""}`}>Uploading</span>
</button>
<button className="action-btn" onClick={handleFullscreenBtnClick}>
<i className={`fa-solid fa-${state.fullscreen ? "compress" : "expand"} icon-img`}></i>
</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;
+1 -1
View File
@@ -40,7 +40,7 @@ const MemosHeader: React.FC<Props> = () => {
<div className="section-header-container memos-header-container">
<div className="title-container">
<div className="action-btn" onClick={toggleSiderbar}>
<img src="/icons/menu.svg" className="icon-img" alt="" />
<i className="fa-solid fa-bars icon-img"></i>
</div>
<span className="title-text" onClick={handleTitleTextClick}>
{titleText}
+2 -2
View File
@@ -22,10 +22,10 @@ const PreviewImageDialog: React.FC<Props> = ({ destroy, imgUrl }: Props) => {
<>
<div className="btns-container">
<button className="btn" onClick={handleCloseBtnClick}>
<img className="icon-img" src="/icons/close.svg" />
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
</button>
<button className="btn" onClick={handleDownloadBtnClick}>
<img className="icon-img" src="/icons/download.svg" />
<i className="fa-solid fa-download icon-img"></i>
</button>
</div>
<div className="img-container">
+1 -1
View File
@@ -24,7 +24,7 @@ const SearchBar: React.FC<Props> = () => {
return (
<div className="search-bar-container">
<div className="search-bar-inputer">
<img className="icon-img" src="/icons/search.svg" />
<i className="fa-solid fa-magnifying-glass fa-sm icon-img"></i>
<input className="text-input" type="text" placeholder="" onChange={handleTextQueryInput} />
</div>
<div className="quickly-action-wrapper">
+1 -1
View File
@@ -30,7 +30,7 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
return (
<div className="dialog-content-container">
<button className="btn close-btn" onClick={destroy}>
<img className="icon-img" src="/icons/close.svg" />
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
</button>
<div className="section-selector-container">
<span className="section-title">Basic</span>
+2 -2
View File
@@ -75,12 +75,12 @@ const ShareMemoImageDialog: React.FC<Props> = (props: Props) => {
<span className="icon-text">🌄</span>Share Memo
</p>
<button className="btn close-btn" onClick={handleCloseBtnClick}>
<img className="icon-img" src="/icons/close.svg" />
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
</button>
</div>
<div className="dialog-content-container">
<div className={`tip-words-container ${shortcutImgUrl ? "finish" : "loading"}`}>
<p className="tip-text">{shortcutImgUrl ? "Click or press to save the image 👇" : "Generating the screenshot..."}</p>
<p className="tip-text">{shortcutImgUrl ? "Click to save the image 👇" : "Generating the screenshot..."}</p>
</div>
<div className="memo-container" ref={memoElRef}>
<Only when={shortcutImgUrl !== ""}>
+4 -4
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()}>
<img src="/icons/add.svg" alt="add shortcut" />
</span>
<button className="btn" onClick={() => showCreateShortcutDialog()}>
<i className="fa-solid fa-plus icon-img fa-xs"></i>
</button>
</p>
<div className="shortcuts-container">
{sortedShortcuts.map((s) => {
@@ -113,7 +113,7 @@ const ShortcutContainer: React.FC<ShortcutContainerProps> = (props: ShortcutCont
</div>
<div className="btns-container">
<span className="action-btn toggle-btn">
<img className="icon-img" src="/icons/more.svg" />
<i className="fa-solid fa-ellipsis fa-sm icon-img"></i>
</span>
<div className="action-btns-wrapper">
<div className="action-btns-container">
+1 -1
View File
@@ -27,7 +27,7 @@ const Sidebar: React.FC<Props> = () => {
<aside className="sidebar-wrapper">
<div className="close-container">
<span className="action-btn" onClick={toggleSiderbar}>
<img src="/icons/close.svg" className="icon-img" alt="" />
<i className="fa-solid fa-xmark fa-lg icon-img"></i>
</span>
</div>
<UserBanner />
+1 -1
View File
@@ -116,7 +116,7 @@ const TagItemContainer: React.FC<TagItemContainerProps> = (props: TagItemContain
<div className="btns-container">
{hasSubTags ? (
<span className={`action-btn toggle-btn ${showSubTags ? "shown" : ""}`} onClick={handleToggleBtnClick}>
<img className="icon-img" src="/icons/arrow-right.svg" />
<i className="fa-solid fa-chevron-right icon-img"></i>
</span>
) : null}
</div>
+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;
+13 -23
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,26 +17,17 @@ 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));
}
}, []);
}, [isVisitorMode, user, owner]);
const handleUsernameClick = useCallback(() => {
locationService.clearQuery();
@@ -54,12 +44,12 @@ 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}>
<img src="/icons/more.svg" className="icon-img" />
</span>
<button className="action-btn menu-popup-btn" onClick={handlePopupBtnClick}>
<i className="fa-solid fa-ellipsis icon-img"></i>
</button>
<MenuBtnsPopup shownStatus={shouldShowPopupBtns} setShownStatus={setShouldShowPopupBtns} />
</div>
<div className="status-text-container">
<div className="amount-text-container">
<div className="status-text memos-text">
<span className="amount-text">{memos.length}</span>
<span className="type-text">MEMO</span>
+2 -2
View File
@@ -55,13 +55,13 @@ const DatePicker: React.FC<DatePickerProps> = (props: DatePickerProps) => {
<div className={`date-picker-wrapper ${className}`}>
<div className="date-picker-header">
<span className="btn-text" onClick={() => handleChangeMonthBtnClick(-1)}>
<img className="icon-img" src="/icons/arrow-left.svg" />
<i className="fa-solid fa-chevron-left icon-img"></i>
</span>
<span className="normal-text">
{firstDate.getFullYear()}/{firstDate.getMonth() + 1}
</span>
<span className="btn-text" onClick={() => handleChangeMonthBtnClick(1)}>
<img className="icon-img" src="/icons/arrow-right.svg" />
<i className="fa-solid fa-chevron-right icon-img"></i>
</span>
</div>
<div className="date-picker-day-container">
+1 -1
View File
@@ -64,7 +64,7 @@ const Selector: React.FC<Props> = (props: Props) => {
<div className={`current-value-container ${showSelector ? "active" : ""}`} onClick={handleCurrentValueClick}>
<span className="value-text">{currentItem.text}</span>
<span className="arrow-text">
<img className="icon-img" src="/icons/arrow-right.svg" />
<i className="fa-solid fa-chevron-down fa-sm icon-img"></i>
</span>
</div>
+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;
@@ -1,34 +1,33 @@
import convertResourceToDataURL from "./convertResourceToDataURL";
const applyStyles = async (sourceElement: HTMLElement, clonedElement: HTMLElement) => {
if (!sourceElement || !clonedElement) {
return;
}
if (sourceElement.tagName === "IMG") {
try {
const url = await convertResourceToDataURL(sourceElement.getAttribute("src") ?? "");
(clonedElement as HTMLImageElement).src = url;
} catch (error) {
// do nth
}
}
const sourceStyles = window.getComputedStyle(sourceElement);
for (const item of sourceStyles) {
clonedElement.style.setProperty(item, sourceStyles.getPropertyValue(item), sourceStyles.getPropertyPriority(item));
}
for (let i = 0; i < clonedElement.childElementCount; i++) {
await applyStyles(sourceElement.children[i] as HTMLElement, clonedElement.children[i] as HTMLElement);
}
};
const getCloneStyledElement = async (element: HTMLElement) => {
const clonedElementContainer = document.createElement(element.tagName);
clonedElementContainer.innerHTML = element.innerHTML;
const applyStyles = async (sourceElement: HTMLElement, clonedElement: HTMLElement) => {
if (!sourceElement || !clonedElement) {
return;
}
const sourceStyles = window.getComputedStyle(sourceElement);
if (sourceElement.tagName === "IMG") {
try {
const url = await convertResourceToDataURL(sourceElement.getAttribute("src") ?? "");
(clonedElement as HTMLImageElement).src = url;
} catch (error) {
// do nth
}
}
for (const item of sourceStyles) {
clonedElement.style.setProperty(item, sourceStyles.getPropertyValue(item), sourceStyles.getPropertyPriority(item));
}
for (let i = 0; i < clonedElement.childElementCount; i++) {
await applyStyles(sourceElement.children[i] as HTMLElement, clonedElement.children[i] as HTMLElement);
}
};
await applyStyles(element, clonedElementContainer);
return clonedElementContainer;
+11 -14
View File
@@ -6,6 +6,7 @@
* 2. <foreignObject>: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject
*/
import getCloneStyledElement from "./getCloneStyledElement";
import waitImageLoaded from "./waitImageLoaded";
type Options = Partial<{
backgroundColor: string;
@@ -51,7 +52,6 @@ const generateSVGElement = (width: number, height: number, element: HTMLElement)
export const toSVG = async (element: HTMLElement, options?: Options) => {
const { width, height } = getElementSize(element);
const clonedElement = await getCloneStyledElement(element);
if (options?.backgroundColor) {
@@ -65,11 +65,6 @@ export const toSVG = async (element: HTMLElement, options?: Options) => {
};
export const toCanvas = async (element: HTMLElement, options?: Options): Promise<HTMLCanvasElement> => {
const url = await toSVG(element, options);
const imageEl = new Image();
imageEl.src = url;
const ratio = options?.pixelRatio || 1;
const { width, height } = getElementSize(element);
@@ -91,18 +86,20 @@ export const toCanvas = async (element: HTMLElement, options?: Options): Promise
context.fillRect(0, 0, canvas.width, canvas.height);
}
return new Promise((resolve) => {
imageEl.onload = () => {
context.drawImage(imageEl, 0, 0, canvas.width, canvas.height);
resolve(canvas);
};
});
const url = await toSVG(element, options);
const imageEl = new Image();
imageEl.style.zIndex = "-1";
imageEl.style.position = "fixed";
imageEl.style.top = "0";
document.body.append(imageEl);
await waitImageLoaded(imageEl, url);
context.drawImage(imageEl, 0, 0, canvas.width, canvas.height);
imageEl.remove();
return canvas;
};
const toImage = async (element: HTMLElement, options?: Options) => {
const canvas = await toCanvas(element, options);
return canvas.toDataURL();
};
@@ -0,0 +1,17 @@
const waitImageLoaded = (image: HTMLImageElement, url: string): Promise<void> => {
return new Promise((resolve, reject) => {
image.loading = "eager";
image.onload = () => {
// NOTE: There is image loading problem in Safari, fix it with some trick
setTimeout(() => {
resolve();
}, 200);
};
image.onerror = () => {
reject("Image load failed");
};
image.src = url;
});
};
export default waitImageLoaded;
+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;
}
}
}
}
+4 -20
View File
@@ -1,33 +1,17 @@
@import "../mixin.less";
.date-picker-wrapper {
.flex(column, flex-start, flex-start);
padding: 16px;
@apply flex flex-col justify-start items-start p-4;
> .date-picker-header {
.flex(row, center, center);
width: 100%;
@apply flex flex-row justify-center items-center w-full mb-2;
> .btn-text {
width: 24px;
height: 24px;
border-radius: 4px;
cursor: pointer;
user-select: none;
> .icon-img {
width: 100%;
height: auto;
}
&:hover {
background-color: @bg-whitegray;
}
@apply w-6 h-6 rounded cursor-pointer select-none flex flex-col justify-center items-center opacity-40 hover:bg-gray-200;
}
> .normal-text {
margin: 0 4px;
line-height: 24px;
@apply mx-1 leading-6 font-mono;
}
}
+6 -11
View File
@@ -1,12 +1,10 @@
@import "../mixin.less";
.selector-wrapper {
.flex(column, flex-start, flex-start);
@apply relative h-7;
@apply flex flex-col justify-start items-start relative h-7;
> .current-value-container {
.flex(row, space-between, center);
@apply w-full h-full rounded px-2 pr-1 bg-white border cursor-pointer select-none;
@apply flex flex-row justify-between items-center w-full h-full rounded px-2 pr-1 bg-white border cursor-pointer select-none;
&:hover,
&.active {
@@ -19,26 +17,23 @@
}
> .arrow-text {
.flex(row, center, center);
@apply w-4 shrink-0;
@apply flex flex-row justify-center items-center w-4 shrink-0;
> .icon-img {
@apply w-4 h-auto opacity-80 rotate-90;
@apply w-4 h-auto opacity-40;
}
}
}
> .items-wrapper {
.flex(column, flex-start, flex-start);
@apply absolute top-full left-0 w-auto p-1 mt-1 -ml-2 bg-white rounded-md overflow-y-auto;
@apply flex flex-col justify-start items-start absolute top-full left-0 w-auto p-1 mt-1 -ml-2 bg-white rounded-md overflow-y-auto z-1;
min-width: calc(100% + 16px);
max-height: 256px;
box-shadow: 0 0 8px 0 rgb(0 0 0 / 20%);
.hide-scroll-bar();
> .item-container {
.flex(column, flex-start, flex-start);
@apply w-full px-3 text-sm select-none leading-8 cursor-pointer rounded whitespace-nowrap hover:bg-gray-100;
@apply flex flex-col justify-start items-start w-full px-3 text-sm select-none leading-8 cursor-pointer rounded whitespace-nowrap hover:bg-gray-100;
&.selected {
color: @text-green;
+1 -1
View File
@@ -89,6 +89,6 @@
}
> .remove-btn {
@apply w-4 h-auto cursor-pointer opacity-80 hover:opacity-60;
@apply w-4 h-auto ml-1 cursor-pointer opacity-60 hover:opacity-80;
}
}
-1
View File
@@ -1,7 +1,6 @@
@import "./mixin.less";
.daily-memo-wrapper {
.flex(row, flex-start, flex-start);
@apply flex flex-row justify-start items-start relative w-full flex-nowrap pb-6;
&:last-child {
+1 -1
View File
@@ -17,7 +17,7 @@
@apply flex flex-row justify-start items-center;
> .btn-text {
@apply w-6 h-6 mr-2 rounded cursor-pointer select-none last:mr-0 hover:bg-gray-200;
@apply w-6 h-6 mr-2 rounded cursor-pointer select-none text-gray-600 last:mr-0 hover:bg-gray-200;
> .icon-img {
@apply w-full h-auto;
+1 -5
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 {
@@ -30,10 +30,6 @@
.btn {
.flex(column, center, center);
@apply w-6 h-6 rounded hover:bg-gray-100 hover:shadow;
> .icon-img {
@apply w-5 h-5;
}
}
}
+2 -20
View File
@@ -28,33 +28,15 @@
@apply flex flex-row justify-start items-center;
> .action-btn {
@apply flex flex-row justify-start items-center p-1 w-auto h-auto mr-1 select-none rounded cursor-pointer opacity-60 hover:opacity-90 hover:bg-gray-300 hover:shadow;
@apply flex flex-row justify-center items-center p-1 w-auto h-auto mr-1 select-none rounded cursor-pointer opacity-60 hover:opacity-90 hover:bg-gray-300 hover:shadow;
> .icon-img {
@apply w-5 h-auto;
@apply w-5 h-5 mx-auto flex flex-row justify-center items-center;
}
> .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;
}
}
}
}
}
+34
View File
@@ -0,0 +1,34 @@
.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 mr-1;
}
}
> .count-text {
@apply w-auto h-full flex flex-row justify-center items-center px-3 text-xs font-bold text-gray-800;
> .icon-text {
@apply text-sm ml-1;
animation: 1.6s linear 0s infinite pulse;
}
}
}
@keyframes pulse {
0% {
transform: scale(0.95);
}
70% {
transform: scale(1.2);
}
100% {
transform: scale(0.95);
}
}
+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;
}
}
+1 -5
View File
@@ -27,11 +27,7 @@
> .btn {
.flex(row, center, center);
@apply w-6 h-6 ml-1 rounded hover:bg-white;
> .icon-img {
@apply w-5 h-5;
}
@apply w-6 h-6 ml-2 rounded text-gray-600 hover:bg-white;
}
}
}
+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;
}
}
}
}
}
+18 -7
View File
@@ -15,6 +15,19 @@
> .memo-top-wrapper {
@apply flex flex-row justify-between items-center w-full h-6 mb-1;
> .status-text-container {
@apply flex flex-row justify-start items-center;
> .time-text {
@apply text-xs text-gray-400 cursor-pointer;
font-size: 13px;
}
> .status-text {
@apply text-xs cursor-pointer ml-2 rounded border border-green-600 px-1 text-green-600;
}
}
> .time-text {
@apply text-xs text-gray-400 cursor-pointer;
}
@@ -26,14 +39,14 @@
@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 {
@apply w-full flex flex-row justify-between items-center border-b border-gray-100 p-1 mb-1;
> .btn {
@apply relative w-7 h-7 p-1;
@apply relative w-7 h-7 p-1 text-gray-600;
&:hover > .tip-text {
@apply block;
@@ -60,18 +73,16 @@
}
.btn {
@apply flex flex-row justify-center items-center px-2 leading-6 text-sm rounded hover:bg-gray-100;
@apply flex flex-row justify-center items-center px-2 leading-6 text-sm rounded hover:bg-gray-200;
&.more-action-btn {
@apply w-8 -mr-2 opacity-60 cursor-default;
@apply w-8 -mr-2 opacity-60 cursor-default hover:bg-transparent;
> .icon-img {
@apply w-4 h-auto;
}
&:hover {
background-color: unset;
& + .more-action-btns-wrapper {
display: flex;
}
@@ -117,7 +128,7 @@
}
> .icon-img {
@apply w-4 h-auto transition-all;
@apply w-4 h-auto ml-1 transition-all;
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
@apply w-full py-2 px-4 rounded-lg flex flex-row justify-start items-center bg-zinc-200;
> .icon-img {
@apply mr-2 w-4 h-auto opacity-80;
@apply mr-2 h-auto opacity-30;
}
> .text-input {
+4 -11
View File
@@ -7,21 +7,15 @@
@apply w-180 max-w-full mb-8 p-0;
> .dialog-content-container {
.flex(column, flex-start, flex-start);
@apply relative w-full overflow-y-scroll p-0 flex flex-col sm:flex-row justify-start items-start;
@apply flex flex-col sm:flex-row justify-start items-start relative w-full overflow-y-scroll p-0;
.hide-scroll-bar();
> .close-btn {
.flex(column, center, center);
@apply absolute top-4 right-4 w-6 h-6 rounded hover:bg-gray-200 hover:shadow;
> .icon-img {
@apply w-5 h-5;
}
@apply flex flex-col justify-center items-center absolute top-4 right-4 w-6 h-6 rounded hover:bg-gray-200 hover:shadow;
}
> .section-selector-container {
@apply w-full sm:w-40 h-auto sm:h-full shrink-0 rounded-t-lg sm:rounded-l-lg p-4 border-r bg-gray-100 flex flex-col justify-start items-start;
@apply w-full sm:w-44 h-auto sm:h-full shrink-0 rounded-t-lg sm:rounded-none sm:rounded-l-lg p-4 border-r bg-gray-100 flex flex-col justify-start items-start;
> .section-title {
@apply text-sm mt-2 sm:mt-4 first:mt-3 mb-1 font-mono text-gray-400;
@@ -49,8 +43,7 @@
.hide-scroll-bar();
> .section-container {
.flex(column, flex-start, flex-start);
@apply w-full my-2;
@apply flex flex-col justify-start items-start w-full my-2;
> .title-text {
@apply text-sm mb-3 font-mono text-gray-500;
+6 -6
View File
@@ -63,14 +63,14 @@
@apply flex flex-row justify-start items-center w-full py-3 px-6;
> .normal-text {
@apply w-full flex flex-row justify-start items-center text-sm text-gray-500;
> .name-text {
@apply text-black ml-2;
}
@apply w-full flex flex-row justify-start items-center text-sm leading-6 text-gray-500;
> .icon-text {
@apply text-lg ml-1 mr-2;
@apply text-lg ml-1 mr-2 leading-6;
}
> .name-text {
@apply text-black ml-2 leading-6;
}
}
}
+8 -22
View File
@@ -12,22 +12,10 @@
}
> .btn {
@apply flex flex-col justify-center items-center w-5 h-5 bg-gray-200 rounded ml-2 shadow hover:opacity-80;
@apply flex flex-col justify-center items-center w-5 p-1 h-5 bg-gray-200 rounded ml-2 shadow hover:opacity-80;
> img {
@apply w-4 h-4 opacity-80;
}
}
}
> .create-shortcut-btn-container {
@apply flex flex-row justify-start items-center w-full mt-4 mb-2 ml-4;
> .btn {
@apply flex p-2 px-4 rounded-lg text-sm border border-dashed border-blue-600;
&:hover {
@apply bg-blue-600 text-white;
> .icon-img {
@apply opacity-60;
}
}
}
@@ -66,14 +54,11 @@
@apply flex-row justify-end items-center hidden shrink-0;
> .action-btn {
.flex(row, center, center);
@apply w-6 h-6 shrink-0;
> .icon-img {
@apply w-4 h-auto;
}
@apply flex flex-row justify-center items-center w-6 h-6 shrink-0;
&.toggle-btn {
@apply opacity-60;
&:hover {
& + .action-btns-wrapper {
@apply flex;
@@ -86,7 +71,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;
+2 -6
View File
@@ -1,18 +1,14 @@
@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 {
@apply w-full pr-6 my-2 flex sm:hidden flex-row justify-end items-center;
> .action-btn {
@apply p-1 bg-gray-100 rounded shadow;
> .icon-img {
@apply w-4 h-auto;
}
@apply p-1 bg-gray-100 rounded shadow w-7 h-7 flex flex-row justify-center items-center;
}
}
+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);
}
}
+1 -1
View File
@@ -28,7 +28,7 @@
}
}
.status-text-container {
.amount-text-container {
@apply flex flex-row justify-between items-start w-full px-6 select-none shrink-0 pb-4;
> .status-text {
+17 -12
View File
@@ -1,5 +1,6 @@
import { useEffect } from "react";
import { locationService, userService } from "../services";
import { useAppSelector } from "../store";
import useLoading from "../hooks/useLoading";
import Only from "../components/common/OnlyWhen";
import Sidebar from "../components/Sidebar";
@@ -11,28 +12,32 @@ import toastHelper from "../components/Toast";
import "../less/home.less";
function Home() {
const location = useAppSelector((state) => state.location);
const loadingState = useLoading();
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();
});
}, []);
}, [location]);
return (
<section className="page-wrapper home">
+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")}/`,
},
},
});
+760 -686
View File
File diff suppressed because it is too large Load Diff