Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eea826dd5c | |||
| 3c5b0ea90a | |||
| 15e1037433 | |||
| 5da4c98f05 | |||
| a73ee7aefc | |||
| 6c5bea9caf | |||
| 93ba2f4fab | |||
| 9417797b99 | |||
| 167e5596f2 | |||
| 2a1e34fe03 | |||
| 3de00cf4a8 | |||
| 1d55545e30 | |||
| 9b5a555d1f | |||
| 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,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>
|
||||
|
||||

|
||||
|
||||
## 🎯 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...
|
||||

|
||||
|
||||
## ✨ 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:
|
||||
|
||||
@@ -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.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" {
|
||||
|
||||
|
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,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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 |
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"}`}
|
||||
|
||||
@@ -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,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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 !== ""}>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -17,8 +17,12 @@
|
||||
@apply font-mono mx-1;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply cursor-pointer underline-offset-2 underline text-blue-600;
|
||||
> .addtion-info-container {
|
||||
@apply flex flex-row justify-start items-center;
|
||||
|
||||
> .github-badge-container {
|
||||
@apply mr-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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")}/`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||