Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 77d8e88351 | |||
| ed31f90e32 | |||
| 19dc543eec | |||
| 721aa3c907 | |||
| 77178afad5 | |||
| fd7b8c3293 | |||
| 660908e436 | |||
| 8d694f7732 | |||
| bdfa9f7a56 | |||
| 19ef97a7ec | |||
| 2c17ea703d | |||
| 1591fdf61c | |||
| 811f3340e9 | |||
| 7e8d1128f8 | |||
| 54db6eda04 | |||
| c5b26e3310 | |||
| 7079faf2b9 | |||
| 707d1a96eb | |||
| 76801dfa4f | |||
| 6e4577f721 | |||
| 7b0987610c | |||
| 50fa560d4a | |||
| b8a7df21f2 | |||
| d1a4348048 | |||
| 020b211660 | |||
| 4657e58b52 | |||
| 22971c3a93 | |||
| 5fa9aa3c22 | |||
| fbce43870f | |||
| b1e6956441 | |||
| ad462cec29 | |||
| a0a42285d0 | |||
| b0b2776d03 | |||
| e9ac6affef | |||
| 5eea1339c9 | |||
| f303dc21a0 | |||
| d68891d91d | |||
| 987bb80770 | |||
| b884327a53 | |||
| 4743818fe7 | |||
| 43575e6f54 | |||
| 89f9dc5640 | |||
| f7aca99d52 |
@@ -4,7 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
# Run on pushing branches like `release/1.0.0`
|
||||
- "release/v*.*.*"
|
||||
- "release/*.*.*"
|
||||
|
||||
jobs:
|
||||
build-and-push-release-image:
|
||||
@@ -17,9 +17,9 @@ jobs:
|
||||
|
||||
- name: Extract build args
|
||||
# Extract version from branch name
|
||||
# Example: branch name `release/v1.0.0` sets up env.VERSION=1.0.0
|
||||
# Example: branch name `release/1.0.0` sets up env.VERSION=1.0.0
|
||||
run: |
|
||||
echo "VERSION=${GITHUB_REF_NAME#release/v}" >> $GITHUB_ENV
|
||||
echo "VERSION=${GITHUB_REF_NAME#release/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
|
||||
@@ -26,73 +26,24 @@
|
||||
|
||||
## Deploy with Docker
|
||||
|
||||
#### Docker Run
|
||||
### Docker Run
|
||||
|
||||
```docker
|
||||
docker run -d --name memos -p 5230:5230 -v ~/.memos/:/var/opt/memos neosmemo/memos:latest
|
||||
```
|
||||
|
||||
#### Docker Compose
|
||||
|
||||
```
|
||||
version: "3.0"
|
||||
services:
|
||||
memos:
|
||||
image: neosmemo/memos:latest
|
||||
container_name: memos
|
||||
volumes:
|
||||
- ~/.memos/:/var/opt/memos
|
||||
ports:
|
||||
- 5230:5230
|
||||
```
|
||||
|
||||
Memos should 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.
|
||||
|
||||
## Development
|
||||
### Docker Compose
|
||||
|
||||
Memos is built with a curated tech stack. It is optimized for developer experience and is very easy to start working on the code:
|
||||
See more in the example [`docker-compose.yaml`](./docker-compose.yaml) file.
|
||||
|
||||
1. It has no external dependency.
|
||||
2. It requires zero config.
|
||||
3. 1 command to start backend and 1 command to start frontend, both with live reload support.
|
||||
|
||||
### Tech Stack
|
||||
|
||||
<img alt="tech stack" src="https://raw.githubusercontent.com/usememos/memos/main/resources/tech-stack.png" width="360" />
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Go](https://golang.org/doc/install)
|
||||
- [Air](https://github.com/cosmtrek/air#installation) for backend live reload
|
||||
- [Node.js](https://nodejs.org/)
|
||||
- [yarn](https://yarnpkg.com/getting-started/install)
|
||||
|
||||
### Steps
|
||||
|
||||
1. pull source code
|
||||
|
||||
```bash
|
||||
git clone https://github.com/usememos/memos
|
||||
```
|
||||
|
||||
2. start backend using air(with live reload)
|
||||
|
||||
```bash
|
||||
air -c scripts/.air.toml
|
||||
```
|
||||
|
||||
3. start frontend dev server
|
||||
|
||||
```bash
|
||||
cd web && yarn && yarn dev
|
||||
```
|
||||
|
||||
Memos should now be running at [http://localhost:3000](http://localhost:3000) and change either frontend or backend code would trigger live reload.
|
||||
|
||||
### Contributing
|
||||
## Contributing
|
||||
|
||||
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated. 🥰
|
||||
|
||||
Gets more about [development guide](https://github.com/usememos/memos/tree/main/docs/development.md).
|
||||
|
||||
## Star history
|
||||
|
||||
[](https://star-history.com/#usememos/memos&Date)
|
||||
|
||||
@@ -37,6 +37,9 @@ type Memo struct {
|
||||
Content string `json:"content"`
|
||||
Visibility Visibility `json:"visibility"`
|
||||
Pinned bool `json:"pinned"`
|
||||
|
||||
// Related fields
|
||||
Creator *User `json:"creator"`
|
||||
}
|
||||
|
||||
type MemoCreate struct {
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ func (key UserSettingKey) String() string {
|
||||
}
|
||||
|
||||
var (
|
||||
UserSettingLocaleValue = []string{"en", "zh"}
|
||||
UserSettingLocaleValue = []string{"en", "zh", "vi"}
|
||||
UserSettingMemoVisibilityValue = []Visibility{Privite, Protected, Public}
|
||||
UserSettingEditorFontStyleValue = []string{"normal", "mono"}
|
||||
)
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/usememos/memos/server"
|
||||
"github.com/usememos/memos/server/profile"
|
||||
"github.com/usememos/memos/store"
|
||||
DB "github.com/usememos/memos/store/db"
|
||||
)
|
||||
|
||||
const (
|
||||
greetingBanner = `
|
||||
███╗ ███╗███████╗███╗ ███╗ ██████╗ ███████╗
|
||||
████╗ ████║██╔════╝████╗ ████║██╔═══██╗██╔════╝
|
||||
██╔████╔██║█████╗ ██╔████╔██║██║ ██║███████╗
|
||||
██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║██║ ██║╚════██║
|
||||
██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝███████║
|
||||
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝
|
||||
`
|
||||
)
|
||||
|
||||
func Run(profile *profile.Profile) error {
|
||||
ctx := context.Background()
|
||||
|
||||
db := DB.NewDB(profile)
|
||||
if err := db.Open(ctx); err != nil {
|
||||
return fmt.Errorf("cannot open db: %w", err)
|
||||
}
|
||||
|
||||
s := server.NewServer(profile)
|
||||
|
||||
storeInstance := store.New(db.Db, profile)
|
||||
s.Store = storeInstance
|
||||
|
||||
println(greetingBanner)
|
||||
fmt.Printf("Version %s has started at :%d\n", profile.Version, profile.Port)
|
||||
|
||||
return s.Run()
|
||||
}
|
||||
|
||||
func Execute() error {
|
||||
profile, err := profile.GetProfile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
println("---")
|
||||
println("profile")
|
||||
println("mode:", profile.Mode)
|
||||
println("port:", profile.Port)
|
||||
println("dsn:", profile.DSN)
|
||||
println("version:", profile.Version)
|
||||
println("---")
|
||||
|
||||
if err := Run(profile); err != nil {
|
||||
fmt.Printf("error: %+v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+61
-2
@@ -5,11 +5,70 @@ import (
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"github.com/usememos/memos/bin/server/cmd"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/usememos/memos/server"
|
||||
"github.com/usememos/memos/server/profile"
|
||||
"github.com/usememos/memos/store"
|
||||
|
||||
DB "github.com/usememos/memos/store/db"
|
||||
)
|
||||
|
||||
const (
|
||||
greetingBanner = `
|
||||
███╗ ███╗███████╗███╗ ███╗ ██████╗ ███████╗
|
||||
████╗ ████║██╔════╝████╗ ████║██╔═══██╗██╔════╝
|
||||
██╔████╔██║█████╗ ██╔████╔██║██║ ██║███████╗
|
||||
██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║██║ ██║╚════██║
|
||||
██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝███████║
|
||||
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝
|
||||
`
|
||||
)
|
||||
|
||||
func run(profile *profile.Profile) error {
|
||||
ctx := context.Background()
|
||||
|
||||
db := DB.NewDB(profile)
|
||||
if err := db.Open(ctx); err != nil {
|
||||
return fmt.Errorf("cannot open db: %w", err)
|
||||
}
|
||||
|
||||
s := server.NewServer(profile)
|
||||
|
||||
storeInstance := store.New(db.Db, profile)
|
||||
s.Store = storeInstance
|
||||
|
||||
println(greetingBanner)
|
||||
fmt.Printf("Version %s has started at :%d\n", profile.Version, profile.Port)
|
||||
|
||||
return s.Run()
|
||||
}
|
||||
|
||||
func execute() error {
|
||||
profile, err := profile.GetProfile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
println("---")
|
||||
println("profile")
|
||||
println("mode:", profile.Mode)
|
||||
println("port:", profile.Port)
|
||||
println("dsn:", profile.DSN)
|
||||
println("version:", profile.Version)
|
||||
println("---")
|
||||
|
||||
if err := run(profile); err != nil {
|
||||
fmt.Printf("error: %+v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := cmd.Execute(); err != nil {
|
||||
if err := execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,3 @@ services:
|
||||
- ~/.memos/:/var/opt/memos
|
||||
ports:
|
||||
- 5230:5230
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,40 @@
|
||||
# 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:
|
||||
|
||||
1. It has no external dependency.
|
||||
2. It requires zero config.
|
||||
3. 1 command to start backend and 1 command to start frontend, both with live reload support.
|
||||
|
||||
## Tech Stack
|
||||
|
||||

|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Go](https://golang.org/doc/install)
|
||||
- [Air](https://github.com/cosmtrek/air#installation) for backend live reload
|
||||
- [Node.js](https://nodejs.org/)
|
||||
- [yarn](https://yarnpkg.com/getting-started/install)
|
||||
|
||||
## Steps
|
||||
|
||||
1. pull source code
|
||||
|
||||
```bash
|
||||
git clone https://github.com/usememos/memos
|
||||
```
|
||||
|
||||
2. start backend using air(with live reload)
|
||||
|
||||
```bash
|
||||
air -c scripts/.air.toml
|
||||
```
|
||||
|
||||
3. start frontend dev server
|
||||
|
||||
```bash
|
||||
cd web && yarn && yarn dev
|
||||
```
|
||||
|
||||
Memos should now be running at [http://localhost:3000](http://localhost:3000) and change either frontend or backend code would trigger live reload.
|
||||
@@ -8,20 +8,20 @@ require github.com/google/uuid v1.3.0
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/mattn/go-colorable v0.1.11 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210920023735-84f357641f63
|
||||
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf // indirect
|
||||
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
|
||||
golang.org/x/net v0.0.0-20220728030405-41545e8bf201 // indirect
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/labstack/echo/v4 v4.6.3
|
||||
github.com/labstack/echo/v4 v4.9.0
|
||||
github.com/labstack/gommon v0.3.1 // indirect
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ require (
|
||||
github.com/VictoriaMetrics/fastcache v1.10.0
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/labstack/echo-contrib v0.12.0
|
||||
github.com/labstack/echo-contrib v0.13.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -52,7 +52,7 @@ github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhpl
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/casbin/casbin/v2 v2.40.6/go.mod h1:sEL80qBYTbd+BPeL4iyvwYzFT3qwLaESq5aFKVLbLfA=
|
||||
github.com/casbin/casbin/v2 v2.51.1/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
@@ -63,7 +63,11 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -77,6 +81,7 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
@@ -90,9 +95,11 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
@@ -139,6 +146,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
@@ -178,6 +187,7 @@ github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
@@ -193,21 +203,16 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo-contrib v0.12.0 h1:NPr1ez+XUa5s/4LujEon+32Bxg5DO6EKSW/va06pmLc=
|
||||
github.com/labstack/echo-contrib v0.12.0/go.mod h1:kR62TbwsBgmpV2HVab5iQRsQtLuhPyGqCBee88XRc4M=
|
||||
github.com/labstack/echo/v4 v4.6.1/go.mod h1:RnjgMWNDB9g/HucVWhQYNQP9PvbYf6adqftqryo7s9k=
|
||||
github.com/labstack/echo/v4 v4.6.3 h1:VhPuIZYxsbPmo4m9KAkMU/el2442eB7EBFFhNTTT9ac=
|
||||
github.com/labstack/echo/v4 v4.6.3/go.mod h1:Hk5OiHj0kDqmFq7aHe7eDqI7CUhuCrfpupQtLGGLm7A=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/labstack/echo-contrib v0.13.0 h1:bzSG0SpuZZd7BmJLvsWtPfU23W0Enh3K0tok3aENVKA=
|
||||
github.com/labstack/echo-contrib v0.13.0/go.mod h1:IF9+MJu22ADOZEHD+bAV67XMIO3vNXUy7Naz/ABPHEs=
|
||||
github.com/labstack/echo/v4 v4.7.2/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
|
||||
github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY=
|
||||
github.com/labstack/echo/v4 v4.9.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
|
||||
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
|
||||
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
|
||||
@@ -217,6 +222,7 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
@@ -230,7 +236,7 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
github.com/openzipkin/zipkin-go v0.3.0/go.mod h1:4c3sLeE8xjNqehmF5RpAFLPLJxXscc0R4l6Zg0P1tTQ=
|
||||
github.com/openzipkin/zipkin-go v0.4.0/go.mod h1:4c3sLeE8xjNqehmF5RpAFLPLJxXscc0R4l6Zg0P1tTQ=
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -241,6 +247,8 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
@@ -249,10 +257,13 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
github.com/rabbitmq/amqp091-go v1.1.0/go.mod h1:ogQDLSOACsLPsIq0NpbtiifNZi2YOz0VTJ0kHRghqbM=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
@@ -277,7 +288,6 @@ github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
@@ -302,8 +312,9 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210920023735-84f357641f63 h1:kETrAMYZq6WVGPa8IIixL0CaEcIUNi+1WX7grUoi3y8=
|
||||
golang.org/x/crypto v0.0.0-20210920023735-84f357641f63/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -372,15 +383,20 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf h1:R150MpwJIv1MpS0N/pc+NhTM8ajzvlmxlY5OYsrevXQ=
|
||||
golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220728030405-41545e8bf201 h1:bvOltf3SADAfG05iRml8lAB3qjoEX5RCyN4K6G5v3N0=
|
||||
golang.org/x/net v0.0.0-20220728030405-41545e8bf201/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -391,12 +407,12 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -405,7 +421,6 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -415,7 +430,6 @@ golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -433,17 +447,22 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 h1:D1v9ucDTYBtbz5vNuBbAhIMAGhQhJ6Ym5ah3maMVNX4=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -456,8 +475,9 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ=
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -576,6 +596,7 @@ google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
|
||||
google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -589,6 +610,7 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
version: "3.2"
|
||||
services:
|
||||
portainer:
|
||||
image: neosmemo/memos:latest
|
||||
container_name: memos
|
||||
ports:
|
||||
- "5230:5230"
|
||||
volumes:
|
||||
- ~/.memos/:/var/opt/memos
|
||||
command: --mode=prod --port=5230
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 63 KiB |
+21
-15
@@ -64,20 +64,22 @@ func aclMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
// If there is openId in query string and related user is found, then skip auth.
|
||||
openID := c.QueryParam("openId")
|
||||
if openID != "" {
|
||||
userFind := &api.UserFind{
|
||||
OpenID: &openID,
|
||||
}
|
||||
user, err := s.Store.FindUser(ctx, userFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by open_id").SetInternal(err)
|
||||
}
|
||||
if user != nil {
|
||||
// Stores userID into context.
|
||||
c.Set(getUserIDContextKey(), user.ID)
|
||||
return next(c)
|
||||
{
|
||||
// If there is openId in query string and related user is found, then skip auth.
|
||||
openID := c.QueryParam("openId")
|
||||
if openID != "" {
|
||||
userFind := &api.UserFind{
|
||||
OpenID: &openID,
|
||||
}
|
||||
user, err := s.Store.FindUser(ctx, userFind)
|
||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by open_id").SetInternal(err)
|
||||
}
|
||||
if user != nil {
|
||||
// Stores userID into context.
|
||||
c.Set(getUserIDContextKey(), user.ID)
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +92,7 @@ func aclMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc {
|
||||
ID: &userID,
|
||||
}
|
||||
user, err := s.Store.FindUser(ctx, userFind)
|
||||
if err != nil {
|
||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by ID: %d", userID)).SetInternal(err)
|
||||
}
|
||||
if user != nil {
|
||||
@@ -102,6 +104,10 @@ func aclMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
if common.HasPrefixes(path, "/api/memo/all") && c.Request().Method == http.MethodGet {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
if common.HasPrefixes(path, "/api/memo", "/api/tag", "/api/shortcut") && c.Request().Method == http.MethodGet {
|
||||
if _, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
|
||||
return next(c)
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
|
||||
Email: &signin.Email,
|
||||
}
|
||||
user, err := s.Store.FindUser(ctx, userFind)
|
||||
if err != nil {
|
||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by email %s", signin.Email)).SetInternal(err)
|
||||
}
|
||||
if user == nil {
|
||||
@@ -68,7 +68,7 @@ func (s *Server) registerAuthRoutes(g *echo.Group) {
|
||||
Role: &hostUserType,
|
||||
}
|
||||
hostUser, err := s.Store.FindUser(ctx, &hostUserFind)
|
||||
if err != nil {
|
||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find host user").SetInternal(err)
|
||||
}
|
||||
if hostUser != nil {
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
//go:embed dist
|
||||
var embeddedFiles embed.FS
|
||||
|
||||
func getFileSystem() http.FileSystem {
|
||||
fs, err := fs.Sub(embeddedFiles, "dist")
|
||||
func getFileSystem(path string) http.FileSystem {
|
||||
fs, err := fs.Sub(embeddedFiles, path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -22,8 +22,22 @@ func getFileSystem() http.FileSystem {
|
||||
}
|
||||
|
||||
func embedFrontend(e *echo.Echo) {
|
||||
// Use echo static middleware to serve the built dist folder
|
||||
// refer: https://github.com/labstack/echo/blob/master/middleware/static.go
|
||||
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
|
||||
HTML5: true,
|
||||
Filesystem: getFileSystem(),
|
||||
Filesystem: getFileSystem("dist"),
|
||||
}))
|
||||
|
||||
g := e.Group("assets")
|
||||
g.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
c.Response().Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
|
||||
return next(c)
|
||||
}
|
||||
})
|
||||
g.Use(middleware.StaticWithConfig(middleware.StaticConfig{
|
||||
HTML5: true,
|
||||
Filesystem: getFileSystem("dist/assets"),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -151,6 +151,58 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
return nil
|
||||
})
|
||||
|
||||
g.GET("/memo/all", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
memoFind := &api.MemoFind{}
|
||||
|
||||
_, ok := c.Get(getUserIDContextKey()).(int)
|
||||
if !ok {
|
||||
memoFind.VisibilityList = []api.Visibility{api.Public}
|
||||
} else {
|
||||
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
|
||||
}
|
||||
|
||||
pinnedStr := c.QueryParam("pinned")
|
||||
if pinnedStr != "" {
|
||||
pinned := pinnedStr == "true"
|
||||
memoFind.Pinned = &pinned
|
||||
}
|
||||
tag := c.QueryParam("tag")
|
||||
if tag != "" {
|
||||
contentSearch := "#" + tag + " "
|
||||
memoFind.ContentSearch = &contentSearch
|
||||
}
|
||||
visibilitListStr := c.QueryParam("visibility")
|
||||
if visibilitListStr != "" {
|
||||
visibilityList := []api.Visibility{}
|
||||
for _, visibility := range strings.Split(visibilitListStr, ",") {
|
||||
visibilityList = append(visibilityList, api.Visibility(visibility))
|
||||
}
|
||||
memoFind.VisibilityList = visibilityList
|
||||
}
|
||||
if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
|
||||
memoFind.Limit = limit
|
||||
}
|
||||
if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
|
||||
memoFind.Offset = offset
|
||||
}
|
||||
|
||||
// Only fetch normal status memos.
|
||||
normalStatus := api.Normal
|
||||
memoFind.RowStatus = &normalStatus
|
||||
|
||||
list, err := s.Store.FindMemoList(ctx, memoFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch all memo list").SetInternal(err)
|
||||
}
|
||||
|
||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(list)); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode all memo list response").SetInternal(err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.POST("/memo/:memoId/organizer", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
memoID, err := strconv.Atoi(c.Param("memoId"))
|
||||
@@ -230,6 +282,9 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
||||
ID: memoID,
|
||||
}
|
||||
if err := s.Store.DeleteMemo(ctx, memoDelete); err != nil {
|
||||
if common.ErrorCode(err) == common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Memo ID not found: %d", memoID))
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete memo ID: %v", memoID)).SetInternal(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/usememos/memos/common"
|
||||
"github.com/usememos/memos/server/version"
|
||||
)
|
||||
|
||||
// Profile is the configuration to start main server.
|
||||
@@ -68,7 +68,7 @@ func GetProfile() (*Profile, error) {
|
||||
|
||||
profile.Data = dataDir
|
||||
profile.DSN = fmt.Sprintf("%s/memos_%s.db", dataDir, profile.Mode)
|
||||
profile.Version = common.GetCurrentVersion(profile.Mode)
|
||||
profile.Version = version.GetCurrentVersion(profile.Mode)
|
||||
|
||||
return &profile, nil
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ package server
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/common"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
@@ -158,9 +160,41 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||
CreatorID: userID,
|
||||
}
|
||||
if err := s.Store.DeleteResource(ctx, resourceDelete); err != nil {
|
||||
if common.ErrorCode(err) == common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource ID not found: %d", resourceID))
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete resource").SetInternal(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) registerResourcePublicRoutes(g *echo.Group) {
|
||||
g.GET("/r/:resourceId/:filename", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
resourceID, err := strconv.Atoi(c.Param("resourceId"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
|
||||
}
|
||||
|
||||
filename := html.UnescapeString(c.Param("filename"))
|
||||
resourceFind := &api.ResourceFind{
|
||||
ID: &resourceID,
|
||||
Filename: &filename,
|
||||
}
|
||||
resource, err := s.Store.FindResource(ctx, resourceFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to fetch resource ID: %v", resourceID)).SetInternal(err)
|
||||
}
|
||||
|
||||
c.Response().Writer.WriteHeader(http.StatusOK)
|
||||
c.Response().Writer.Header().Set("Content-Type", resource.Type)
|
||||
c.Response().Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
|
||||
if _, err := c.Response().Writer.Write(resource.Blob); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write response").SetInternal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
+4
-2
@@ -56,9 +56,11 @@ func NewServer(profile *profile.Profile) *Server {
|
||||
Profile: profile,
|
||||
}
|
||||
|
||||
// Webhooks api skips auth checker.
|
||||
webhookGroup := e.Group("/h")
|
||||
s.registerWebhookRoutes(webhookGroup)
|
||||
s.registerResourcePublicRoutes(webhookGroup)
|
||||
|
||||
publicGroup := e.Group("/o")
|
||||
s.registerResourcePublicRoutes(publicGroup)
|
||||
|
||||
apiGroup := e.Group("/api")
|
||||
apiGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/common"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
@@ -123,6 +124,9 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
|
||||
ID: shortcutID,
|
||||
}
|
||||
if err := s.Store.DeleteShortcut(ctx, shortcutDelete); err != nil {
|
||||
if common.ErrorCode(err) == common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Shortcut ID not found: %d", shortcutID))
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete shortcut").SetInternal(err)
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
"github.com/usememos/memos/common"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
@@ -27,7 +28,7 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
||||
Role: &hostUserType,
|
||||
}
|
||||
hostUser, err := s.Store.FindUser(ctx, &hostUserFind)
|
||||
if err != nil {
|
||||
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find host user").SetInternal(err)
|
||||
}
|
||||
|
||||
|
||||
+4
-6
@@ -12,6 +12,8 @@ import (
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
var tagRegexp = regexp.MustCompile(`#([^\s#]+?) `)
|
||||
|
||||
func (s *Server) registerTagRoutes(g *echo.Group) {
|
||||
g.GET("/tag", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
@@ -47,13 +49,9 @@ func (s *Server) registerTagRoutes(g *echo.Group) {
|
||||
|
||||
tagMapSet := make(map[string]bool)
|
||||
|
||||
r := regexp.MustCompile(`#([^\s#]+?) `)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compile regexp").SetInternal(err)
|
||||
}
|
||||
for _, memo := range memoList {
|
||||
for _, rawTag := range r.FindAllString(memo.Content, -1) {
|
||||
tag := r.ReplaceAllString(rawTag, "$1")
|
||||
for _, rawTag := range tagRegexp.FindAllString(memo.Content, -1) {
|
||||
tag := tagRegexp.ReplaceAllString(rawTag, "$1")
|
||||
tagMapSet[tag] = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,6 +250,9 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
|
||||
ID: userID,
|
||||
}
|
||||
if err := s.Store.DeleteUser(ctx, userDelete); err != nil {
|
||||
if common.ErrorCode(err) == common.NotFound {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("User ID not found: %d", userID))
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete user").SetInternal(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package common
|
||||
package version
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
|
||||
// Version is the service current released version.
|
||||
// Semantic versioning: https://semver.org/
|
||||
var Version = "0.4.2"
|
||||
var Version = "0.4.4"
|
||||
|
||||
// DevVersion is the service current development version.
|
||||
var DevVersion = "0.4.2"
|
||||
var DevVersion = "0.4.4"
|
||||
|
||||
func GetCurrentVersion(mode string) string {
|
||||
if mode == "dev" {
|
||||
@@ -27,6 +27,12 @@ func GetMinorVersion(version string) string {
|
||||
return versionList[0] + "." + versionList[1]
|
||||
}
|
||||
|
||||
func GetSchemaVersion(version string) string {
|
||||
minorVersion := GetMinorVersion(version)
|
||||
|
||||
return minorVersion + ".0"
|
||||
}
|
||||
|
||||
// convSemanticVersionToInt converts version string to int.
|
||||
func convSemanticVersionToInt(version string) int {
|
||||
versionList := strings.Split(version, ".")
|
||||
@@ -1,44 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/usememos/memos/api"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func (s *Server) registerWebhookRoutes(g *echo.Group) {
|
||||
g.GET("/test", func(c echo.Context) error {
|
||||
return c.HTML(http.StatusOK, "<strong>Hello, World!</strong>")
|
||||
})
|
||||
|
||||
g.GET("/r/:resourceId/:filename", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
resourceID, err := strconv.Atoi(c.Param("resourceId"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
|
||||
}
|
||||
|
||||
filename := html.UnescapeString(c.Param("filename"))
|
||||
resourceFind := &api.ResourceFind{
|
||||
ID: &resourceID,
|
||||
Filename: &filename,
|
||||
}
|
||||
resource, err := s.Store.FindResource(ctx, resourceFind)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to fetch resource ID: %v", resourceID)).SetInternal(err)
|
||||
}
|
||||
|
||||
c.Response().Writer.WriteHeader(http.StatusOK)
|
||||
c.Response().Writer.Header().Set("Content-Type", resource.Type)
|
||||
if _, err := c.Response().Writer.Write(resource.Blob); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write response").SetInternal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
+4
-4
@@ -12,8 +12,8 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/usememos/memos/common"
|
||||
"github.com/usememos/memos/server/profile"
|
||||
"github.com/usememos/memos/server/version"
|
||||
)
|
||||
|
||||
//go:embed migration
|
||||
@@ -70,7 +70,7 @@ func (db *DB) Open(ctx context.Context) (err error) {
|
||||
return fmt.Errorf("failed to create migration_history table: %w", err)
|
||||
}
|
||||
|
||||
currentVersion := common.GetCurrentVersion(db.profile.Mode)
|
||||
currentVersion := version.GetCurrentVersion(db.profile.Mode)
|
||||
migrationHistory, err := db.FindMigrationHistory(ctx, &MigrationHistoryFind{})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -84,7 +84,7 @@ func (db *DB) Open(ctx context.Context) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
if common.IsVersionGreaterThan(currentVersion, migrationHistory.Version) {
|
||||
if version.IsVersionGreaterThan(version.GetSchemaVersion(currentVersion), migrationHistory.Version) {
|
||||
minorVersionList := getMinorVersionList()
|
||||
|
||||
// backup the raw database file before migration
|
||||
@@ -101,7 +101,7 @@ func (db *DB) Open(ctx context.Context) (err error) {
|
||||
println("start migrate")
|
||||
for _, minorVersion := range minorVersionList {
|
||||
normalizedVersion := minorVersion + ".0"
|
||||
if common.IsVersionGreaterThan(normalizedVersion, migrationHistory.Version) && common.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
|
||||
if version.IsVersionGreaterThan(normalizedVersion, migrationHistory.Version) && version.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
|
||||
println("applying migration for", normalizedVersion)
|
||||
if err := db.applyMigrationForMinorVersion(ctx, minorVersion); err != nil {
|
||||
return fmt.Errorf("failed to apply minor version migration: %w", err)
|
||||
|
||||
+11
-1
@@ -57,6 +57,11 @@ func (s *Store) composeMemo(ctx context.Context, raw *memoRaw) (*api.Memo, error
|
||||
memo.Pinned = memoOrganizer.Pinned
|
||||
}
|
||||
|
||||
err = s.ComposeMemoCreator(ctx, memo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return memo, nil
|
||||
}
|
||||
|
||||
@@ -348,7 +353,7 @@ func findMemoRawList(ctx context.Context, tx *sql.Tx, find *api.MemoFind) ([]*me
|
||||
}
|
||||
|
||||
func deleteMemo(ctx context.Context, tx *sql.Tx, delete *api.MemoDelete) error {
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
PRAGMA foreign_keys = ON;
|
||||
DELETE FROM memo WHERE id = ?
|
||||
`, delete.ID)
|
||||
@@ -356,5 +361,10 @@ func deleteMemo(ctx context.Context, tx *sql.Tx, delete *api.MemoDelete) error {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("memo ID not found: %d", delete.ID)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+11
-1
@@ -141,6 +141,11 @@ func (s *Store) DeleteResource(ctx context.Context, delete *api.ResourceDelete)
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
// Vacuum sqlite database file size after deleting resource.
|
||||
if _, err := s.db.Exec("VACUUM;"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.cache.DeleteCache(api.ResourceCache, delete.ID)
|
||||
|
||||
return nil
|
||||
@@ -235,7 +240,7 @@ func findResourceList(ctx context.Context, tx *sql.Tx, find *api.ResourceFind) (
|
||||
}
|
||||
|
||||
func deleteResource(ctx context.Context, tx *sql.Tx, delete *api.ResourceDelete) error {
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
PRAGMA foreign_keys = ON;
|
||||
DELETE FROM resource WHERE id = ? AND creator_id = ?
|
||||
`, delete.ID, delete.CreatorID)
|
||||
@@ -243,5 +248,10 @@ func deleteResource(ctx context.Context, tx *sql.Tx, delete *api.ResourceDelete)
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("resource ID not found: %d", delete.ID)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+6
-1
@@ -287,7 +287,7 @@ func findShortcutList(ctx context.Context, tx *sql.Tx, find *api.ShortcutFind) (
|
||||
}
|
||||
|
||||
func deleteShortcut(ctx context.Context, tx *sql.Tx, delete *api.ShortcutDelete) error {
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
PRAGMA foreign_keys = ON;
|
||||
DELETE FROM shortcut WHERE id = ?
|
||||
`, delete.ID)
|
||||
@@ -295,5 +295,10 @@ func deleteShortcut(ctx context.Context, tx *sql.Tx, delete *api.ShortcutDelete)
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("shortcut ID not found: %d", delete.ID)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+24
-4
@@ -44,6 +44,20 @@ func (raw *userRaw) toUser() *api.User {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) ComposeMemoCreator(ctx context.Context, memo *api.Memo) error {
|
||||
user, err := s.FindUser(ctx, &api.UserFind{
|
||||
ID: &memo.CreatorID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.OpenID = ""
|
||||
user.UserSettingList = nil
|
||||
memo.Creator = user
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateUser(ctx context.Context, create *api.UserCreate) (*api.User, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
@@ -138,9 +152,9 @@ func (s *Store) FindUser(ctx context.Context, find *api.UserFind) (*api.User, er
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
return nil, nil
|
||||
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found user with filter %+v", find)}
|
||||
} else if len(list) > 1 {
|
||||
return nil, &common.Error{Code: common.Conflict, Err: fmt.Errorf("found %d users with filter %+v, expect 1. ", len(list), find)}
|
||||
return nil, &common.Error{Code: common.Conflict, Err: fmt.Errorf("found %d users with filter %+v, expect 1", len(list), find)}
|
||||
}
|
||||
|
||||
user := list[0].toUser()
|
||||
@@ -336,12 +350,18 @@ func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userR
|
||||
}
|
||||
|
||||
func deleteUser(ctx context.Context, tx *sql.Tx, delete *api.UserDelete) error {
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
PRAGMA foreign_keys = ON;
|
||||
DELETE FROM user WHERE id = ?
|
||||
`, delete.ID); err != nil {
|
||||
`, delete.ID)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("user ID not found: %d", delete.ID)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-empty-interface": ["off"],
|
||||
"@typescript-eslint/no-explicit-any": ["off"],
|
||||
"react/react-in-jsx-scope": "off"
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<script>
|
||||
var global = global || window;
|
||||
window.addEventListener("load", () => {
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.register("/sw.js");
|
||||
|
||||
+5
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "memos",
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.4",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
@@ -10,13 +10,16 @@
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.8.1",
|
||||
"axios": "^0.27.2",
|
||||
"copy-to-clipboard": "^3.3.2",
|
||||
"dayjs": "^1.11.3",
|
||||
"emoji-picker-react": "^3.6.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"qs": "^6.11.0",
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"react-feather": "^2.0.10",
|
||||
"react-redux": "^8.0.1"
|
||||
"react-redux": "^8.0.1",
|
||||
"react-router-dom": "^6.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash-es": "^4.17.5",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 63 KiB |
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"short_name": "Memos",
|
||||
"name": "Memos",
|
||||
"description": "usememos/memos",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/logo.webp",
|
||||
"type": "image/*",
|
||||
"sizes": "200x200"
|
||||
"type": "image/webp",
|
||||
"sizes": "520x520"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"background_color": "#f6f5f4",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"theme_color": "#f6f5f4",
|
||||
"description": "usememos/memos"
|
||||
"background_color": "#f6f5f4"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { generateDialog } from "./Dialog";
|
||||
import GitHubBadge from "./GitHubBadge";
|
||||
import "../less/about-site-dialog.less";
|
||||
|
||||
interface Props extends DialogProps {}
|
||||
type Props = DialogProps;
|
||||
|
||||
const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { IMAGE_URL_REG } from "../helpers/marked";
|
||||
import * as utils from "../helpers/utils";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import useToggle from "../hooks/useToggle";
|
||||
import { memoService } from "../services";
|
||||
import { formatMemoContent } from "../helpers/marked";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import Image from "./Image";
|
||||
import toastHelper from "./Toast";
|
||||
import MemoContent from "./MemoContent";
|
||||
import MemoResources from "./MemoResources";
|
||||
import "../less/memo.less";
|
||||
|
||||
interface Props {
|
||||
@@ -21,14 +19,14 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
|
||||
archivedAtStr: utils.getDateTimeString(propsMemo.updatedTs ?? Date.now()),
|
||||
};
|
||||
const { t } = useI18n();
|
||||
|
||||
const [showConfirmDeleteBtn, toggleConfirmDeleteBtn] = useToggle(false);
|
||||
const imageUrls = Array.from(memo.content.match(IMAGE_URL_REG) ?? []).map((s) => s.replace(IMAGE_URL_REG, "$1"));
|
||||
|
||||
const handleDeleteMemoClick = async () => {
|
||||
if (showConfirmDeleteBtn) {
|
||||
try {
|
||||
await memoService.deleteMemoById(memo.id);
|
||||
await memoService.fetchAllMemos();
|
||||
await memoService.fetchMemos();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toastHelper.error(error.response.data.message);
|
||||
@@ -44,8 +42,8 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
|
||||
id: memo.id,
|
||||
rowStatus: "NORMAL",
|
||||
});
|
||||
await memoService.fetchAllMemos();
|
||||
toastHelper.info("Restored successfully");
|
||||
await memoService.fetchMemos();
|
||||
toastHelper.info(t("message.restored-successfully"));
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toastHelper.error(error.response.data.message);
|
||||
@@ -61,7 +59,9 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div className={`memo-wrapper archived-memo ${"memos-" + memo.id}`} onMouseLeave={handleMouseLeaveMemoWrapper}>
|
||||
<div className="memo-top-wrapper">
|
||||
<span className="time-text">Archived at {memo.archivedAtStr}</span>
|
||||
<span className="time-text">
|
||||
{t("common.archived-at")} {memo.archivedAtStr}
|
||||
</span>
|
||||
<div className="btns-container">
|
||||
<span className="btn restore-btn" onClick={handleRestoreMemoClick}>
|
||||
{t("common.restore")}
|
||||
@@ -72,14 +72,8 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="memo-content-text" dangerouslySetInnerHTML={{ __html: formatMemoContent(memo.content) }}></div>
|
||||
<Only when={imageUrls.length > 0}>
|
||||
<div className="images-wrapper">
|
||||
{imageUrls.map((imgUrl, idx) => (
|
||||
<Image className="memo-img" key={idx} imgUrl={imgUrl} />
|
||||
))}
|
||||
</div>
|
||||
</Only>
|
||||
<MemoContent className="memo-content-wrapper" content={memo.content} />
|
||||
<MemoResources memo={memo} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import { memoService } from "../services";
|
||||
import { useAppSelector } from "../store";
|
||||
import Icon from "./Icon";
|
||||
@@ -8,9 +9,10 @@ import toastHelper from "./Toast";
|
||||
import ArchivedMemo from "./ArchivedMemo";
|
||||
import "../less/archived-memo-dialog.less";
|
||||
|
||||
interface Props extends DialogProps {}
|
||||
type Props = DialogProps;
|
||||
|
||||
const ArchivedMemoDialog: React.FC<Props> = (props: Props) => {
|
||||
const { t } = useI18n();
|
||||
const { destroy } = props;
|
||||
const memos = useAppSelector((state) => state.memo.memos);
|
||||
const loadingState = useLoading();
|
||||
@@ -36,7 +38,7 @@ const ArchivedMemoDialog: React.FC<Props> = (props: Props) => {
|
||||
<div className="dialog-header-container">
|
||||
<p className="title-text">
|
||||
<span className="icon-text">🗂</span>
|
||||
Archived Memos
|
||||
{t("archived.archived-memos")}
|
||||
</p>
|
||||
<button className="btn close-btn" onClick={destroy}>
|
||||
<Icon.X className="icon-img" />
|
||||
@@ -45,11 +47,11 @@ const ArchivedMemoDialog: React.FC<Props> = (props: Props) => {
|
||||
<div className="dialog-content-container">
|
||||
{loadingState.isLoading ? (
|
||||
<div className="tip-text-container">
|
||||
<p className="tip-text">fetching data...</p>
|
||||
<p className="tip-text">{t("archived.fetching-data")}</p>
|
||||
</div>
|
||||
) : archivedMemos.length === 0 ? (
|
||||
<div className="tip-text-container">
|
||||
<p className="tip-text">No archived memos.</p>
|
||||
<p className="tip-text">{t("archived.no-archived-memos")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="archived-memos-container">
|
||||
|
||||
@@ -23,7 +23,7 @@ const ChangeMemoCreatedTsDialog: React.FC<Props> = (props: Props) => {
|
||||
const datetime = dayjs(memo.createdTs).format("YYYY-MM-DDTHH:mm");
|
||||
setCreatedAt(datetime);
|
||||
} else {
|
||||
toastHelper.error("Memo not found.");
|
||||
toastHelper.error(t("message.memo-not-found"));
|
||||
destroy();
|
||||
}
|
||||
}, []);
|
||||
@@ -42,7 +42,7 @@ const ChangeMemoCreatedTsDialog: React.FC<Props> = (props: Props) => {
|
||||
const createdTs = dayjs(createdAt).unix();
|
||||
|
||||
if (createdTs > nowTs) {
|
||||
toastHelper.error("Invalid created datetime.");
|
||||
toastHelper.error(t("message.invalid-created-datetime"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ const ChangeMemoCreatedTsDialog: React.FC<Props> = (props: Props) => {
|
||||
id: memoId,
|
||||
createdTs,
|
||||
});
|
||||
toastHelper.info("Memo created datetime changed.");
|
||||
toastHelper.info(t("message.memo-updated-datetime"));
|
||||
handleCloseBtnClick();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
@@ -62,7 +62,7 @@ const ChangeMemoCreatedTsDialog: React.FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<>
|
||||
<div className="dialog-header-container">
|
||||
<p className="title-text">Change memo created time</p>
|
||||
<p className="title-text">{t("message.change-memo-created-time")}</p>
|
||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||
<Icon.X />
|
||||
</button>
|
||||
|
||||
@@ -14,10 +14,10 @@ const validateConfig: ValidatorConfig = {
|
||||
noChinese: true,
|
||||
};
|
||||
|
||||
interface Props extends DialogProps {}
|
||||
type Props = DialogProps;
|
||||
|
||||
const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
const { t } = useI18n();
|
||||
const { t, locale } = useI18n();
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [newPasswordAgain, setNewPasswordAgain] = useState("");
|
||||
|
||||
@@ -41,19 +41,19 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
|
||||
const handleSaveBtnClick = async () => {
|
||||
if (newPassword === "" || newPasswordAgain === "") {
|
||||
toastHelper.error("Please fill in all fields.");
|
||||
toastHelper.error(t("message.fill-all"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== newPasswordAgain) {
|
||||
toastHelper.error("New passwords do not match.");
|
||||
toastHelper.error(t("message.new-password-not-match"));
|
||||
setNewPasswordAgain("");
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordValidResult = validate(newPassword, validateConfig);
|
||||
if (!passwordValidResult.result) {
|
||||
toastHelper.error("Password " + passwordValidResult.reason);
|
||||
toastHelper.error(t("common.password") + locale === "zh" ? "" : " " + passwordValidResult.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
id: user.id,
|
||||
password: newPassword,
|
||||
});
|
||||
toastHelper.info("Password changed.");
|
||||
toastHelper.info(t("common.password") + t("common.changed"));
|
||||
handleCloseBtnClick();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
@@ -74,17 +74,22 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<div className="dialog-header-container">
|
||||
<p className="title-text">Change Password</p>
|
||||
<p className="title-text">{t("common.password" + " " + t("common.change"))}</p>
|
||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||
<Icon.X />
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
<label className="form-label input-form-label">
|
||||
<input type="password" placeholder="New passworld" value={newPassword} onChange={handleNewPasswordChanged} />
|
||||
<input type="password" placeholder={t("common.new-password")} value={newPassword} onChange={handleNewPasswordChanged} />
|
||||
</label>
|
||||
<label className="form-label input-form-label">
|
||||
<input type="password" placeholder="Repeat the new password" value={newPasswordAgain} onChange={handleNewPasswordAgainChanged} />
|
||||
<input
|
||||
type="password"
|
||||
placeholder={t("common.repeat-new-password")}
|
||||
value={newPasswordAgain}
|
||||
onChange={handleNewPasswordAgainChanged}
|
||||
/>
|
||||
</label>
|
||||
<div className="btns-container">
|
||||
<span className="btn cancel-btn" onClick={handleCloseBtnClick}>
|
||||
|
||||
@@ -6,6 +6,7 @@ import Icon from "./Icon";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import toastHelper from "./Toast";
|
||||
import Selector from "./common/Selector";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import "../less/create-shortcut-dialog.less";
|
||||
|
||||
interface Props extends DialogProps {
|
||||
@@ -18,6 +19,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
const [title, setTitle] = useState<string>("");
|
||||
const [filters, setFilters] = useState<Filter[]>([]);
|
||||
const requestState = useLoading(false);
|
||||
const { t } = useI18n();
|
||||
|
||||
const shownMemoLength = memoService.getState().memos.filter((memo) => {
|
||||
return checkShouldShowMemoWithFilters(memo, filters);
|
||||
@@ -43,7 +45,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
|
||||
const handleSaveBtnClick = async () => {
|
||||
if (!title) {
|
||||
toastHelper.error("Title is required");
|
||||
toastHelper.error(t("shortcut-list.title-required"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -71,7 +73,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
if (filters.length > 0) {
|
||||
const lastFilter = filters[filters.length - 1];
|
||||
if (lastFilter.value.value === "") {
|
||||
toastHelper.info("Please fill in previous filter value");
|
||||
toastHelper.info(t("shortcut-list.fill-previous"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -99,7 +101,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
<div className="dialog-header-container">
|
||||
<p className="title-text">
|
||||
<span className="icon-text">🚀</span>
|
||||
{shortcutId ? "Edit Shortcut" : "Create Shortcut"}
|
||||
{shortcutId ? t("shortcut-list.edit-shortcut") : t("shortcut-list.create-shortcut")}
|
||||
</p>
|
||||
<button className="btn close-btn" onClick={destroy}>
|
||||
<Icon.X />
|
||||
@@ -107,11 +109,17 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
<div className="form-item-container input-form-container">
|
||||
<span className="normal-text">Title</span>
|
||||
<input className="title-input" type="text" placeholder="shortcut title" value={title} onChange={handleTitleInputChange} />
|
||||
<span className="normal-text">{t("common.title")}</span>
|
||||
<input
|
||||
className="title-input"
|
||||
type="text"
|
||||
placeholder={t("shortcut-list.shortcut-title")}
|
||||
value={title}
|
||||
onChange={handleTitleInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item-container filter-form-container">
|
||||
<span className="normal-text">Filters</span>
|
||||
<span className="normal-text">{t("common.filter")}</span>
|
||||
<div className="filters-wrapper">
|
||||
{filters.map((f, index) => {
|
||||
return (
|
||||
@@ -125,7 +133,7 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
);
|
||||
})}
|
||||
<div className="create-filter-btn" onClick={handleAddFilterBenClick}>
|
||||
New Filter
|
||||
{t("shortcut-list.new-filter")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -134,10 +142,10 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
|
||||
<div></div>
|
||||
<div className="btns-container">
|
||||
<span className={`tip-text ${filters.length === 0 && "hidden"}`}>
|
||||
<strong>{shownMemoLength}</strong> eligible memo
|
||||
<strong>{shownMemoLength}</strong> {t("shortcut-list.eligible-memo")}
|
||||
</span>
|
||||
<button className={`btn save-btn ${requestState.isLoading ? "requesting" : ""}`} onClick={handleSaveBtnClick}>
|
||||
Save
|
||||
{t("common.save")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as utils from "../helpers/utils";
|
||||
import { formatMemoContent } from "../helpers/marked";
|
||||
import MemoContent, { DisplayConfig } from "./MemoContent";
|
||||
import "../less/daily-memo.less";
|
||||
|
||||
interface DailyMemo extends Memo {
|
||||
@@ -18,22 +18,17 @@ const DailyMemo: React.FC<Props> = (props: Props) => {
|
||||
createdAtStr: utils.getDateTimeString(propsMemo.createdTs),
|
||||
timeStr: utils.getTimeString(propsMemo.createdTs),
|
||||
};
|
||||
const displayConfig: DisplayConfig = {
|
||||
enableExpand: false,
|
||||
showInlineImage: true,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="daily-memo-wrapper">
|
||||
<div className="time-wrapper">
|
||||
<span className="normal-text">{memo.timeStr}</span>
|
||||
</div>
|
||||
<div className="memo-content-container">
|
||||
<div
|
||||
className="memo-content-text"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: formatMemoContent(memo.content, {
|
||||
inlineImage: true,
|
||||
}),
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<MemoContent className="memo-content-container" content={memo.content} displayConfig={displayConfig} />
|
||||
<div className="split-line"></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -97,7 +97,7 @@ const DailyReviewDialog: React.FC<Props> = (props: Props) => {
|
||||
</div>
|
||||
{dailyMemos.length === 0 ? (
|
||||
<div className="tip-container">
|
||||
<p className="tip-text">Oops, there is nothing.</p>
|
||||
<p className="tip-text">{t("daily-review.oops-nothing")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="dailymemos-wrapper">
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { forwardRef, useEffect } from "react";
|
||||
import Picker, { IEmojiPickerProps } from "emoji-picker-react";
|
||||
|
||||
export type EmojiPickerElement = HTMLDivElement;
|
||||
|
||||
interface Props {
|
||||
isShowEmojiPicker: boolean;
|
||||
onEmojiClick: IEmojiPickerProps["onEmojiClick"];
|
||||
handleChangeIsShowEmojiPicker: (status: boolean) => void;
|
||||
}
|
||||
|
||||
export const EmojiPicker = forwardRef<EmojiPickerElement, Props>((props: Props, ref) => {
|
||||
const { isShowEmojiPicker, onEmojiClick, handleChangeIsShowEmojiPicker } = props;
|
||||
|
||||
useEffect(() => {
|
||||
if (isShowEmojiPicker) {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const emojiWrapper = document.querySelector(".emoji-picker-react");
|
||||
const isContains = emojiWrapper?.contains(event.target as Node);
|
||||
if (!isContains) {
|
||||
handleChangeIsShowEmojiPicker(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
// Unbind the event listener on clean up
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}
|
||||
}, [isShowEmojiPicker]);
|
||||
|
||||
return (
|
||||
<div className="emoji-picker" ref={ref}>
|
||||
<Picker onEmojiClick={onEmojiClick} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
EmojiPicker.displayName = "EmojiPicker";
|
||||
|
||||
export default EmojiPicker;
|
||||
@@ -3,9 +3,7 @@ import * as api from "../helpers/api";
|
||||
import Icon from "./Icon";
|
||||
import "../less/github-badge.less";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const GitHubBadge: React.FC<Props> = () => {
|
||||
const GitHubBadge = () => {
|
||||
const [starCount, setStarCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
+23
-56
@@ -1,34 +1,27 @@
|
||||
import { memo, useEffect, useRef, useState } from "react";
|
||||
import { indexOf } from "lodash-es";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { indexOf } from "lodash-es";
|
||||
import { memo, useEffect, useRef, useState } from "react";
|
||||
import "dayjs/locale/zh";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import { UNKNOWN_ID } from "../helpers/consts";
|
||||
import { DONE_BLOCK_REG, formatMemoContent, IMAGE_URL_REG, TODO_BLOCK_REG } from "../helpers/marked";
|
||||
import { DONE_BLOCK_REG, TODO_BLOCK_REG } from "../helpers/marked";
|
||||
import { editorStateService, locationService, memoService, userService } from "../services";
|
||||
import Icon from "./Icon";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import toastHelper from "./Toast";
|
||||
import Image from "./Image";
|
||||
import MemoContent from "./MemoContent";
|
||||
import MemoResources from "./MemoResources";
|
||||
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;
|
||||
}
|
||||
|
||||
interface State {
|
||||
expandButtonStatus: ExpandButtonStatus;
|
||||
}
|
||||
|
||||
export const getFormatedMemoCreatedAtStr = (createdTs: number, locale = "en"): string => {
|
||||
if (Date.now() - createdTs < 1000 * 60 * 60 * 24) {
|
||||
return dayjs(createdTs).locale(locale).fromNow();
|
||||
@@ -40,27 +33,12 @@ export const getFormatedMemoCreatedAtStr = (createdTs: number, locale = "en"): s
|
||||
const Memo: React.FC<Props> = (props: Props) => {
|
||||
const memo = props.memo;
|
||||
const { t, locale } = useI18n();
|
||||
const [state, setState] = useState<State>({
|
||||
expandButtonStatus: -1,
|
||||
});
|
||||
const [createdAtStr, setCreatedAtStr] = useState<string>(getFormatedMemoCreatedAtStr(memo.createdTs, locale));
|
||||
const memoContainerRef = useRef<HTMLDivElement>(null);
|
||||
const memoContentContainerRef = 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 (!memoContentContainerRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Number(memoContentContainerRef.current?.clientHeight) > MAX_MEMO_CONTAINER_HEIGHT) {
|
||||
setState({
|
||||
...state,
|
||||
expandButtonStatus: 0,
|
||||
});
|
||||
}
|
||||
|
||||
let intervalFlag = -1;
|
||||
if (Date.now() - memo.createdTs < 1000 * 60 * 60 * 24) {
|
||||
intervalFlag = setInterval(() => {
|
||||
@@ -127,7 +105,7 @@ const Memo: React.FC<Props> = (props: Props) => {
|
||||
if (memoTemp) {
|
||||
showMemoCardDialog(memoTemp);
|
||||
} else {
|
||||
toastHelper.error("Memo Not Found");
|
||||
toastHelper.error(t("message.memo-not-found"));
|
||||
targetEl.classList.remove("memo-link-text");
|
||||
}
|
||||
} else if (targetEl.className === "tag-span") {
|
||||
@@ -172,15 +150,18 @@ const Memo: React.FC<Props> = (props: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleExpandBtnClick = () => {
|
||||
const expandButtonStatus = Boolean(!state.expandButtonStatus);
|
||||
if (!expandButtonStatus) {
|
||||
memoContainerRef.current?.scrollIntoView();
|
||||
const handleMemoContentDoubleClick = (e: React.MouseEvent) => {
|
||||
const targetEl = e.target as HTMLElement;
|
||||
|
||||
if (targetEl.className === "memo-link-text") {
|
||||
return;
|
||||
} else if (targetEl.className === "tag-span") {
|
||||
return;
|
||||
} else if (targetEl.classList.contains("todo-block")) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState({
|
||||
expandButtonStatus: Number(expandButtonStatus) as ExpandButtonStatus,
|
||||
});
|
||||
editorStateService.setEditMemoWithId(memo.id);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -225,27 +206,13 @@ const Memo: React.FC<Props> = (props: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={memoContentContainerRef}
|
||||
className={`memo-content-text ${state.expandButtonStatus === 0 ? "expanded" : ""}`}
|
||||
onClick={handleMemoContentClick}
|
||||
dangerouslySetInnerHTML={{ __html: formatMemoContent(memo.content) }}
|
||||
></div>
|
||||
{state.expandButtonStatus !== -1 && (
|
||||
<div className="expand-btn-container">
|
||||
<span className={`btn ${state.expandButtonStatus === 0 ? "expand-btn" : "fold-btn"}`} onClick={handleExpandBtnClick}>
|
||||
{state.expandButtonStatus === 0 ? "Expand" : "Fold"}
|
||||
<Icon.ChevronRight className="icon-img" />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Only when={imageUrls.length > 0}>
|
||||
<div className="images-wrapper">
|
||||
{imageUrls.map((imgUrl, idx) => (
|
||||
<Image className="memo-img" key={idx} imgUrl={imgUrl} />
|
||||
))}
|
||||
</div>
|
||||
</Only>
|
||||
<MemoContent
|
||||
className=""
|
||||
content={memo.content}
|
||||
onMemoContentClick={handleMemoContentClick}
|
||||
onMemoContentDoubleClick={handleMemoContentDoubleClick}
|
||||
/>
|
||||
<MemoResources memo={memo} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import copy from "copy-to-clipboard";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { editorStateService, memoService, userService } from "../services";
|
||||
import { useAppSelector } from "../store";
|
||||
import { UNKNOWN_ID, VISIBILITY_SELECTOR_ITEMS } from "../helpers/consts";
|
||||
import * as utils from "../helpers/utils";
|
||||
import { formatMemoContent, IMAGE_URL_REG, MEMO_LINK_REG, parseHtmlToRawText } from "../helpers/marked";
|
||||
import { formatMemoContent, MEMO_LINK_REG, parseHtmlToRawText } from "../helpers/marked";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import toastHelper from "./Toast";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import Image from "./Image";
|
||||
import Icon from "./Icon";
|
||||
import Selector from "./common/Selector";
|
||||
import MemoContent from "./MemoContent";
|
||||
import MemoResources from "./MemoResources";
|
||||
import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import "../less/memo-card-dialog.less";
|
||||
|
||||
interface LinkedMemo extends Memo {
|
||||
@@ -29,7 +32,7 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
});
|
||||
const [linkMemos, setLinkMemos] = useState<LinkedMemo[]>([]);
|
||||
const [linkedMemos, setLinkedMemos] = useState<LinkedMemo[]>([]);
|
||||
const imageUrls = Array.from(memo.content.match(IMAGE_URL_REG) ?? []).map((s) => s.replace(IMAGE_URL_REG, "$1"));
|
||||
const { t } = useI18n();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLinkedMemos = async () => {
|
||||
@@ -95,7 +98,7 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
setLinkedMemos([]);
|
||||
setMemo(nextMemo);
|
||||
} else {
|
||||
toastHelper.error("Memo Not Found");
|
||||
toastHelper.error(t("message.memo-not-found"));
|
||||
targetEl.classList.remove("memo-link-text");
|
||||
}
|
||||
}
|
||||
@@ -107,6 +110,16 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
setMemo(memo);
|
||||
}, []);
|
||||
|
||||
const handleCopyMemoLinkBtnClick = () => {
|
||||
if (memo.visibility === "PRIVATE") {
|
||||
toastHelper.error("This memo is private only.");
|
||||
return;
|
||||
}
|
||||
|
||||
copy(`${window.location.origin}/explore?memoId=${memo.id}`);
|
||||
toastHelper.success("Copied");
|
||||
};
|
||||
|
||||
const handleEditMemoBtnClick = () => {
|
||||
props.destroy();
|
||||
editorStateService.setEditMemoWithId(memo.id);
|
||||
@@ -150,6 +163,9 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
<div className="btns-container">
|
||||
<Only when={!userService.isVisitorMode()}>
|
||||
<>
|
||||
<button className="btn edit-btn" onClick={handleCopyMemoLinkBtnClick}>
|
||||
<Icon.Link className="icon-img" />
|
||||
</button>
|
||||
<button className="btn edit-btn" onClick={handleEditMemoBtnClick}>
|
||||
<Icon.Edit3 className="icon-img" />
|
||||
</button>
|
||||
@@ -162,18 +178,13 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="memo-container">
|
||||
<div
|
||||
className="memo-content-text"
|
||||
onClick={handleMemoContentClick}
|
||||
dangerouslySetInnerHTML={{ __html: formatMemoContent(memo.content) }}
|
||||
></div>
|
||||
<Only when={imageUrls.length > 0}>
|
||||
<div className="images-wrapper">
|
||||
{imageUrls.map((imgUrl, idx) => (
|
||||
<Image className="memo-img" key={idx} imgUrl={imgUrl} />
|
||||
))}
|
||||
</div>
|
||||
</Only>
|
||||
<MemoContent
|
||||
className=""
|
||||
displayConfig={{ enableExpand: false }}
|
||||
content={memo.content}
|
||||
onMemoContentClick={handleMemoContentClick}
|
||||
/>
|
||||
<MemoResources memo={memo} />
|
||||
</div>
|
||||
<div className="layer-container"></div>
|
||||
{linkMemos.map((_, idx) => {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { formatMemoContent } from "../helpers/marked";
|
||||
import Icon from "./Icon";
|
||||
import "../less/memo-content.less";
|
||||
|
||||
const defaultDisplayConfig: DisplayConfig = {
|
||||
enableExpand: true,
|
||||
showInlineImage: false,
|
||||
};
|
||||
|
||||
export interface DisplayConfig {
|
||||
enableExpand: boolean;
|
||||
showInlineImage: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
className: string;
|
||||
content: string;
|
||||
displayConfig?: Partial<DisplayConfig>;
|
||||
onMemoContentClick?: (e: React.MouseEvent) => void;
|
||||
onMemoContentDoubleClick?: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
type ExpandButtonStatus = -1 | 0 | 1;
|
||||
|
||||
interface State {
|
||||
expandButtonStatus: ExpandButtonStatus;
|
||||
}
|
||||
|
||||
const MAX_MEMO_CONTAINER_HEIGHT = 384;
|
||||
|
||||
const MemoContent: React.FC<Props> = (props: Props) => {
|
||||
const { className, content, onMemoContentClick, onMemoContentDoubleClick } = props;
|
||||
const [state, setState] = useState<State>({
|
||||
expandButtonStatus: -1,
|
||||
});
|
||||
const memoContentContainerRef = useRef<HTMLDivElement>(null);
|
||||
const displayConfig = {
|
||||
...defaultDisplayConfig,
|
||||
...props.displayConfig,
|
||||
};
|
||||
const formatConfig = {
|
||||
inlineImage: displayConfig.showInlineImage,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!memoContentContainerRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (displayConfig.enableExpand) {
|
||||
if (Number(memoContentContainerRef.current?.clientHeight) > MAX_MEMO_CONTAINER_HEIGHT) {
|
||||
setState({
|
||||
...state,
|
||||
expandButtonStatus: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleMemoContentClick = async (e: React.MouseEvent) => {
|
||||
if (onMemoContentClick) {
|
||||
onMemoContentClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMemoContentDoubleClick = async (e: React.MouseEvent) => {
|
||||
if (onMemoContentDoubleClick) {
|
||||
onMemoContentDoubleClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExpandBtnClick = () => {
|
||||
const expandButtonStatus = Boolean(!state.expandButtonStatus);
|
||||
setState({
|
||||
expandButtonStatus: Number(expandButtonStatus) as ExpandButtonStatus,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`memo-content-wrapper ${className}`}>
|
||||
<div
|
||||
ref={memoContentContainerRef}
|
||||
className={`memo-content-text ${state.expandButtonStatus === 0 ? "expanded" : ""}`}
|
||||
onClick={handleMemoContentClick}
|
||||
onDoubleClick={handleMemoContentDoubleClick}
|
||||
dangerouslySetInnerHTML={{ __html: formatMemoContent(content, formatConfig) }}
|
||||
></div>
|
||||
{state.expandButtonStatus !== -1 && (
|
||||
<div className="expand-btn-container">
|
||||
<span className={`btn ${state.expandButtonStatus === 0 ? "expand-btn" : "fold-btn"}`} onClick={handleExpandBtnClick}>
|
||||
{state.expandButtonStatus === 0 ? "Expand" : "Fold"}
|
||||
<Icon.ChevronRight className="icon-img" />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemoContent;
|
||||
@@ -7,16 +7,16 @@ import * as storage from "../helpers/storage";
|
||||
import Icon from "./Icon";
|
||||
import toastHelper from "./Toast";
|
||||
import Editor, { EditorRefActions } from "./Editor/Editor";
|
||||
import EmojiPicker from "./Editor/EmojiPicker";
|
||||
import "../less/memo-editor.less";
|
||||
|
||||
interface Props {}
|
||||
|
||||
interface State {
|
||||
isUploadingResource: boolean;
|
||||
fullscreen: boolean;
|
||||
isShowEmojiPicker: boolean;
|
||||
}
|
||||
|
||||
const MemoEditor: React.FC<Props> = () => {
|
||||
const MemoEditor = () => {
|
||||
const { t, locale } = useI18n();
|
||||
const user = useAppSelector((state) => state.user.user);
|
||||
const editorState = useAppSelector((state) => state.editor);
|
||||
@@ -24,6 +24,7 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
const [state, setState] = useState<State>({
|
||||
isUploadingResource: false,
|
||||
fullscreen: false,
|
||||
isShowEmojiPicker: false,
|
||||
});
|
||||
const editorRef = useRef<EditorRefActions>(null);
|
||||
const prevGlobalStateRef = useRef(editorState);
|
||||
@@ -112,13 +113,13 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
const { type } = file;
|
||||
|
||||
if (!type.startsWith("image")) {
|
||||
toastHelper.error("Only image file supported.");
|
||||
toastHelper.error(t("editor.only-image-supported"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const image = await resourceService.upload(file);
|
||||
const url = `/h/r/${image.id}/${image.filename}`;
|
||||
const url = `/o/r/${image.id}/${image.filename}`;
|
||||
return url;
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
@@ -135,7 +136,7 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
|
||||
const handleSaveBtnClick = async (content: string) => {
|
||||
if (content === "") {
|
||||
toastHelper.error("Content can't be empty");
|
||||
toastHelper.error(t("editor.cant-empty"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -227,9 +228,9 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
editorRef.current?.insertText(``);
|
||||
}
|
||||
}
|
||||
document.body.removeChild(inputEl);
|
||||
};
|
||||
inputEl.click();
|
||||
document.body.removeChild(inputEl);
|
||||
}, []);
|
||||
|
||||
const handleFullscreenBtnClick = () => {
|
||||
@@ -246,6 +247,21 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleChangeIsShowEmojiPicker = (status: boolean) => {
|
||||
setState({
|
||||
...state,
|
||||
isShowEmojiPicker: status,
|
||||
});
|
||||
};
|
||||
|
||||
const handleEmojiClick = (event: any, emojiObject: any) => {
|
||||
if (!editorRef.current) {
|
||||
return;
|
||||
}
|
||||
editorRef.current?.insertText(`${emojiObject.emoji}`);
|
||||
handleChangeIsShowEmojiPicker(false);
|
||||
};
|
||||
|
||||
const isEditing = Boolean(editorState.editMemoId && editorState.editMemoId !== UNKNOWN_ID);
|
||||
|
||||
const editorConfig = useMemo(
|
||||
@@ -298,6 +314,9 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
<button className="action-btn">
|
||||
<Icon.Code className="icon-img" onClick={handleCodeBlockBtnClick} />
|
||||
</button>
|
||||
<button className="action-btn">
|
||||
<Icon.Smile className="icon-img" onClick={() => handleChangeIsShowEmojiPicker(!state.isShowEmojiPicker)} />
|
||||
</button>
|
||||
<button className="action-btn">
|
||||
<Icon.Image className="icon-img" onClick={handleUploadFileBtnClick} />
|
||||
<span className={`tip-text ${state.isUploadingResource ? "!block" : ""}`}>Uploading</span>
|
||||
@@ -308,6 +327,13 @@ const MemoEditor: React.FC<Props> = () => {
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{state.isShowEmojiPicker && (
|
||||
<EmojiPicker
|
||||
onEmojiClick={handleEmojiClick}
|
||||
isShowEmojiPicker={state.isShowEmojiPicker}
|
||||
handleChangeIsShowEmojiPicker={handleChangeIsShowEmojiPicker}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,20 +2,20 @@ import { useAppSelector } from "../store";
|
||||
import { locationService, shortcutService } from "../services";
|
||||
import * as utils from "../helpers/utils";
|
||||
import { getTextWithMemoType } from "../helpers/filter";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import "../less/memo-filter.less";
|
||||
|
||||
interface FilterProps {}
|
||||
|
||||
const MemoFilter: React.FC<FilterProps> = () => {
|
||||
const MemoFilter = () => {
|
||||
const query = useAppSelector((state) => state.location.query);
|
||||
useAppSelector((state) => state.shortcut.shortcuts);
|
||||
const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId } = query;
|
||||
const shortcut = shortcutId ? shortcutService.getShortcutById(shortcutId) : null;
|
||||
const showFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || shortcut);
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<div className={`filter-query-container ${showFilter ? "" : "!hidden"}`}>
|
||||
<span className="tip-text">Filter:</span>
|
||||
<span className="tip-text">{t("common.filter")}:</span>
|
||||
<div
|
||||
className={"filter-item-container " + (shortcut ? "" : "hidden")}
|
||||
onClick={() => {
|
||||
|
||||
@@ -10,9 +10,7 @@ import Only from "./common/OnlyWhen";
|
||||
import Memo from "./Memo";
|
||||
import "../less/memo-list.less";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const MemoList: React.FC<Props> = () => {
|
||||
const MemoList = () => {
|
||||
const { t } = useI18n();
|
||||
const query = useAppSelector((state) => state.location.query);
|
||||
const { memos, isFetching } = useAppSelector((state) => state.memo);
|
||||
@@ -82,7 +80,7 @@ const MemoList: React.FC<Props> = () => {
|
||||
|
||||
useEffect(() => {
|
||||
memoService
|
||||
.fetchAllMemos()
|
||||
.fetchMemos()
|
||||
.then(() => {
|
||||
// do nth
|
||||
})
|
||||
@@ -110,7 +108,7 @@ const MemoList: React.FC<Props> = () => {
|
||||
))}
|
||||
<Only when={!isFetching}>
|
||||
<div className="status-text-container">
|
||||
<p className="status-text">{sortedMemos.length === 0 ? "no memos 🌃" : showMemoFilter ? "" : "all memos are ready 🎉"}</p>
|
||||
<p className="status-text">{sortedMemos.length === 0 ? t("message.no-memos") : showMemoFilter ? "" : t("message.memos-ready")}</p>
|
||||
</div>
|
||||
</Only>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { IMAGE_URL_REG } from "../helpers/marked";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import Image from "./Image";
|
||||
import "../less/memo-resources.less";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
memo: Memo;
|
||||
}
|
||||
|
||||
const MemoResources: React.FC<Props> = (props: Props) => {
|
||||
const { className, memo } = props;
|
||||
const imageUrls = Array.from(memo.content.match(IMAGE_URL_REG) ?? []).map((s) => s.replace(IMAGE_URL_REG, "$1"));
|
||||
|
||||
return (
|
||||
<div className="resource-wrapper">
|
||||
<Only when={imageUrls.length > 0}>
|
||||
<div className={`images-wrapper ${className ?? ""}`}>
|
||||
{imageUrls.map((imgUrl, idx) => (
|
||||
<Image className="memo-img" key={idx} imgUrl={imgUrl} />
|
||||
))}
|
||||
</div>
|
||||
</Only>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemoResources;
|
||||
@@ -8,9 +8,7 @@ import "../less/memos-header.less";
|
||||
|
||||
let prevRequestTimestamp = Date.now();
|
||||
|
||||
interface Props {}
|
||||
|
||||
const MemosHeader: React.FC<Props> = () => {
|
||||
const MemosHeader = () => {
|
||||
const query = useAppSelector((state) => state.location.query);
|
||||
const shortcuts = useAppSelector((state) => state.shortcut.shortcuts);
|
||||
const [titleText, setTitleText] = useState("MEMOS");
|
||||
@@ -31,7 +29,7 @@ const MemosHeader: React.FC<Props> = () => {
|
||||
const now = Date.now();
|
||||
if (now - prevRequestTimestamp > 1 * 1000) {
|
||||
prevRequestTimestamp = now;
|
||||
memoService.fetchAllMemos().catch(() => {
|
||||
memoService.fetchMemos().catch(() => {
|
||||
// do nth
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import * as api from "../helpers/api";
|
||||
import { locationService, userService } from "../services";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import toastHelper from "./Toast";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import showAboutSiteDialog from "./AboutSiteDialog";
|
||||
import showArchivedMemoDialog from "./ArchivedMemoDialog";
|
||||
import showResourcesDialog from "./ResourcesDialog";
|
||||
import "../less/menu-btns-popup.less";
|
||||
|
||||
interface Props {
|
||||
@@ -32,19 +32,12 @@ const MenuBtnsPopup: React.FC<Props> = (props: Props) => {
|
||||
}
|
||||
}, [shownStatus]);
|
||||
|
||||
const handlePingBtnClick = () => {
|
||||
api
|
||||
.getSystemStatus()
|
||||
.then(({ data }) => {
|
||||
const {
|
||||
data: { profile },
|
||||
} = data;
|
||||
toastHelper.info(JSON.stringify(profile, null, 4));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toastHelper.error(error.response.data.message);
|
||||
});
|
||||
const handleResourcesBtnClick = () => {
|
||||
showResourcesDialog();
|
||||
};
|
||||
|
||||
const handleArchivedBtnClick = () => {
|
||||
showArchivedMemoDialog();
|
||||
};
|
||||
|
||||
const handleAboutBtnClick = () => {
|
||||
@@ -65,12 +58,17 @@ const MenuBtnsPopup: React.FC<Props> = (props: Props) => {
|
||||
|
||||
return (
|
||||
<div className={`menu-btns-popup ${shownStatus ? "" : "hidden"}`} ref={popupElRef}>
|
||||
<Only when={!userService.isVisitorMode()}>
|
||||
<button className="btn action-btn" onClick={handleResourcesBtnClick}>
|
||||
<span className="icon">🌄</span> {t("sidebar.resources")}
|
||||
</button>
|
||||
<button className="btn action-btn" onClick={handleArchivedBtnClick}>
|
||||
<span className="icon">🗂</span> {t("sidebar.archived")}
|
||||
</button>
|
||||
</Only>
|
||||
<button className="btn action-btn" onClick={handleAboutBtnClick}>
|
||||
<span className="icon">🤠</span> {t("common.about")}
|
||||
</button>
|
||||
<button className="btn action-btn" onClick={handlePingBtnClick}>
|
||||
<span className="icon">🎯</span> Ping
|
||||
</button>
|
||||
<Only when={!userService.isVisitorMode()}>
|
||||
<button className="btn action-btn" onClick={handleSignOutBtnClick}>
|
||||
<span className="icon">👋</span> {t("common.sign-out")}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import * as utils from "../helpers/utils";
|
||||
import copy from "copy-to-clipboard";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import { resourceService } from "../services";
|
||||
@@ -11,7 +11,7 @@ import Icon from "./Icon";
|
||||
import toastHelper from "./Toast";
|
||||
import "../less/resources-dialog.less";
|
||||
|
||||
interface Props extends DialogProps {}
|
||||
type Props = DialogProps;
|
||||
|
||||
interface State {
|
||||
resources: Resource[];
|
||||
@@ -58,7 +58,7 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
|
||||
document.body.appendChild(inputEl);
|
||||
inputEl.type = "file";
|
||||
inputEl.multiple = true;
|
||||
inputEl.accept = "image/*";
|
||||
inputEl.accept = "*";
|
||||
inputEl.onchange = async () => {
|
||||
if (!inputEl.files || inputEl.files.length === 0) {
|
||||
return;
|
||||
@@ -70,7 +70,6 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
|
||||
});
|
||||
|
||||
for (const file of inputEl.files) {
|
||||
await resourceService.upload(file);
|
||||
try {
|
||||
await resourceService.upload(file);
|
||||
} catch (error: any) {
|
||||
@@ -84,18 +83,23 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
|
||||
}
|
||||
}
|
||||
|
||||
document.body.removeChild(inputEl);
|
||||
await fetchResources();
|
||||
};
|
||||
inputEl.click();
|
||||
document.body.removeChild(inputEl);
|
||||
};
|
||||
|
||||
const handlPreviewBtnClick = (resource: Resource) => {
|
||||
showPreviewImageDialog(`${window.location.origin}/h/r/${resource.id}/${resource.filename}`);
|
||||
const resourceUrl = `${window.location.origin}/o/r/${resource.id}/${resource.filename}`;
|
||||
if (resource.type.startsWith("image")) {
|
||||
showPreviewImageDialog(resourceUrl);
|
||||
} else {
|
||||
window.open(resourceUrl);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyResourceLinkBtnClick = (resource: Resource) => {
|
||||
utils.copyTextToClipboard(`${window.location.origin}/h/r/${resource.id}/${resource.filename}`);
|
||||
copy(`${window.location.origin}/o/r/${resource.id}/${resource.filename}`);
|
||||
toastHelper.success("Succeed to copy resource link to clipboard");
|
||||
};
|
||||
|
||||
@@ -123,16 +127,15 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
<div className="tip-text-container">(👨💻WIP) View your static resources in memos. e.g. images</div>
|
||||
<div className="upload-resource-container" onClick={() => handleUploadFileBtnClick()}>
|
||||
<div className="upload-resource-btn">
|
||||
<Icon.File className="icon-img" />
|
||||
<span>Upload</span>
|
||||
<span>{t("resources.upload")}</span>
|
||||
</div>
|
||||
</div>
|
||||
{loadingState.isLoading ? (
|
||||
<div className="loading-text-container">
|
||||
<p className="tip-text">fetching data...</p>
|
||||
<p className="tip-text">{t("resources.fetching-data")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="resource-table-container">
|
||||
@@ -143,7 +146,7 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
|
||||
<span></span>
|
||||
</div>
|
||||
{state.resources.length === 0 ? (
|
||||
<p className="tip-text">No resource.</p>
|
||||
<p className="tip-text">{t("resources.no-resources")}</p>
|
||||
) : (
|
||||
state.resources.map((resource) => (
|
||||
<div key={resource.id} className="resource-container">
|
||||
@@ -152,10 +155,10 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
|
||||
<span className="field-text">{resource.type}</span>
|
||||
<div className="buttons-container">
|
||||
<Dropdown className="actions-dropdown">
|
||||
<button onClick={() => handlPreviewBtnClick(resource)}>Preview</button>
|
||||
<button onClick={() => handleCopyResourceLinkBtnClick(resource)}>Copy Link</button>
|
||||
<button onClick={() => handlPreviewBtnClick(resource)}>{t("resources.preview")}</button>
|
||||
<button onClick={() => handleCopyResourceLinkBtnClick(resource)}>{t("resources.copy-link")}</button>
|
||||
<button className="delete-btn" onClick={() => handleDeleteResourceBtnClick(resource)}>
|
||||
Delete
|
||||
{t("common.delete")}
|
||||
</button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,12 @@ import { locationService } from "../services";
|
||||
import { useAppSelector } from "../store";
|
||||
import { memoSpecialTypes } from "../helpers/filter";
|
||||
import Icon from "./Icon";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import "../less/search-bar.less";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const SearchBar: React.FC<Props> = () => {
|
||||
const SearchBar = () => {
|
||||
const memoType = useAppSelector((state) => state.location.query?.type);
|
||||
const { t } = useI18n();
|
||||
|
||||
const handleMemoTypeItemClick = (type: MemoSpecType | undefined) => {
|
||||
const { type: prevType } = locationService.getState().query ?? {};
|
||||
@@ -30,9 +30,9 @@ const SearchBar: React.FC<Props> = () => {
|
||||
</div>
|
||||
<div className="quickly-action-wrapper">
|
||||
<div className="quickly-action-container">
|
||||
<p className="title-text">QUICKLY FILTER</p>
|
||||
<p className="title-text">{t("search.quickly-filter").toUpperCase()}</p>
|
||||
<div className="section-container types-container">
|
||||
<span className="section-text">Type:</span>
|
||||
<span className="section-text">{t("common.type").toUpperCase()}:</span>
|
||||
<div className="values-container">
|
||||
{memoSpecialTypes.map((t, idx) => {
|
||||
return (
|
||||
|
||||
@@ -8,7 +8,7 @@ import PreferencesSection from "./Settings/PreferencesSection";
|
||||
import MemberSection from "./Settings/MemberSection";
|
||||
import "../less/setting-dialog.less";
|
||||
|
||||
interface Props extends DialogProps {}
|
||||
type Props = DialogProps;
|
||||
|
||||
type SettingSection = "my-account" | "preferences" | "member";
|
||||
|
||||
|
||||
@@ -9,14 +9,12 @@ import Dropdown from "../common/Dropdown";
|
||||
import { showCommonDialog } from "../Dialog/CommonDialog";
|
||||
import "../../less/settings/member-section.less";
|
||||
|
||||
interface Props {}
|
||||
|
||||
interface State {
|
||||
createUserEmail: string;
|
||||
createUserPassword: string;
|
||||
}
|
||||
|
||||
const PreferencesSection: React.FC<Props> = () => {
|
||||
const PreferencesSection = () => {
|
||||
const { t } = useI18n();
|
||||
const currentUser = useAppSelector((state) => state.user.user);
|
||||
const [state, setState] = useState<State>({
|
||||
@@ -50,7 +48,7 @@ const PreferencesSection: React.FC<Props> = () => {
|
||||
|
||||
const handleCreateUserBtnClick = async () => {
|
||||
if (isEmpty(state.createUserEmail) || isEmpty(state.createUserPassword)) {
|
||||
toastHelper.error("Please fill out this form");
|
||||
toastHelper.error(t("message.fill-form"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -127,10 +125,10 @@ const PreferencesSection: React.FC<Props> = () => {
|
||||
<button onClick={handleCreateUserBtnClick}>{t("common.create")}</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="title-text">Member list</p>
|
||||
<p className="title-text">{t("setting.member-list")}</p>
|
||||
<div className="member-container field-container">
|
||||
<span className="field-text">ID</span>
|
||||
<span className="field-text">EMAIL</span>
|
||||
<span className="field-text">{t("common.email")}</span>
|
||||
<span></span>
|
||||
</div>
|
||||
{userList.map((user) => (
|
||||
@@ -139,7 +137,7 @@ const PreferencesSection: React.FC<Props> = () => {
|
||||
<span className="field-text email-text">{user.email}</span>
|
||||
<div className="buttons-container">
|
||||
{currentUser?.id === user.id ? (
|
||||
<span className="tip-text">Yourself</span>
|
||||
<span className="tip-text">{t("common.yourself")}</span>
|
||||
) : (
|
||||
<Dropdown className="actions-dropdown">
|
||||
{user.rowStatus === "NORMAL" ? (
|
||||
|
||||
@@ -9,16 +9,14 @@ import showChangePasswordDialog from "../ChangePasswordDialog";
|
||||
import "../../less/settings/my-account-section.less";
|
||||
|
||||
const validateConfig: ValidatorConfig = {
|
||||
minLength: 4,
|
||||
minLength: 1,
|
||||
maxLength: 24,
|
||||
noSpace: true,
|
||||
noChinese: true,
|
||||
noChinese: false,
|
||||
};
|
||||
|
||||
interface Props {}
|
||||
|
||||
const MyAccountSection: React.FC<Props> = () => {
|
||||
const { t } = useI18n();
|
||||
const MyAccountSection = () => {
|
||||
const { t, locale } = useI18n();
|
||||
const user = useAppSelector((state) => state.user.user as User);
|
||||
const [username, setUsername] = useState<string>(user.name);
|
||||
const openAPIRoute = `${window.location.origin}/api/memo?openId=${user.openId}`;
|
||||
@@ -35,7 +33,7 @@ const MyAccountSection: React.FC<Props> = () => {
|
||||
|
||||
const usernameValidResult = validate(username, validateConfig);
|
||||
if (!usernameValidResult.result) {
|
||||
toastHelper.error("Username " + usernameValidResult.reason);
|
||||
toastHelper.error(t("common.username") + locale === "zh" ? "" : " " + usernameValidResult.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,7 +42,7 @@ const MyAccountSection: React.FC<Props> = () => {
|
||||
id: user.id,
|
||||
name: username,
|
||||
});
|
||||
toastHelper.info("Username changed");
|
||||
toastHelper.info(t("common.username") + locale === "zh" ? "" : " " + t("common.changed"));
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
toastHelper.error(error.response.data.message);
|
||||
|
||||
@@ -3,11 +3,8 @@ import { useAppSelector } from "../../store";
|
||||
import { VISIBILITY_SELECTOR_ITEMS } from "../../helpers/consts";
|
||||
import useI18n from "../../hooks/useI18n";
|
||||
import Selector from "../common/Selector";
|
||||
import BetaBadge from "../BetaBadge";
|
||||
import "../../less/settings/preferences-section.less";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const localeSelectorItems = [
|
||||
{
|
||||
text: "English",
|
||||
@@ -17,6 +14,10 @@ const localeSelectorItems = [
|
||||
text: "中文",
|
||||
value: "zh",
|
||||
},
|
||||
{
|
||||
text: "Tiếng Việt",
|
||||
value: "vi",
|
||||
},
|
||||
];
|
||||
|
||||
const editorFontStyleSelectorItems = [
|
||||
@@ -30,7 +31,7 @@ const editorFontStyleSelectorItems = [
|
||||
},
|
||||
];
|
||||
|
||||
const PreferencesSection: React.FC<Props> = () => {
|
||||
const PreferencesSection = () => {
|
||||
const { t } = useI18n();
|
||||
const { setting } = useAppSelector((state) => state.user.user as User);
|
||||
|
||||
@@ -51,10 +52,7 @@ const PreferencesSection: React.FC<Props> = () => {
|
||||
<div className="section-container preferences-section-container">
|
||||
<p className="title-text">{t("common.basic")}</p>
|
||||
<label className="form-label selector">
|
||||
<span className="normal-text">
|
||||
{t("common.language")}
|
||||
<BetaBadge className="ml-2" />
|
||||
</span>
|
||||
<span className="normal-text">{t("common.language")}</span>
|
||||
<Selector className="ml-2 w-32" value={setting.locale} dataSource={localeSelectorItems} handleValueChanged={handleLocaleChanged} />
|
||||
</label>
|
||||
<p className="title-text">{t("setting.preference")}</p>
|
||||
|
||||
@@ -4,11 +4,12 @@ import toImage from "../labs/html2image";
|
||||
import { ANIMATION_DURATION } from "../helpers/consts";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import * as utils from "../helpers/utils";
|
||||
import { formatMemoContent, IMAGE_URL_REG } from "../helpers/marked";
|
||||
import { IMAGE_URL_REG } from "../helpers/marked";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import Icon from "./Icon";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import toastHelper from "./Toast";
|
||||
import MemoContent from "./MemoContent";
|
||||
import "../less/share-memo-image-dialog.less";
|
||||
|
||||
interface Props extends DialogProps {
|
||||
@@ -58,7 +59,7 @@ const ShareMemoImageDialog: React.FC<Props> = (props: Props) => {
|
||||
|
||||
const handleImageOnLoad = (event: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
if (event.type === "error") {
|
||||
toastHelper.error("Image load failed");
|
||||
toastHelper.error(t("message.image-load-failed"));
|
||||
(event.target as HTMLImageElement).remove();
|
||||
}
|
||||
setImgAmount(imgAmount - 1);
|
||||
@@ -91,7 +92,7 @@ const ShareMemoImageDialog: React.FC<Props> = (props: Props) => {
|
||||
<img className="memo-shortcut-img" onClick={handleDownloadBtnClick} src={shortcutImgUrl} />
|
||||
</Only>
|
||||
<span className="time-text">{memo.createdAtStr}</span>
|
||||
<div className="memo-content-text" dangerouslySetInnerHTML={{ __html: formatMemoContent(memo.content) }}></div>
|
||||
<MemoContent className="memo-content-wrapper" content={memo.content} displayConfig={{ enableExpand: false }} />
|
||||
<Only when={imageUrls.length > 0}>
|
||||
<div className="images-container">
|
||||
{imageUrls.map((imgUrl, idx) => (
|
||||
|
||||
@@ -10,12 +10,11 @@ import toastHelper from "./Toast";
|
||||
import showCreateShortcutDialog from "./CreateShortcutDialog";
|
||||
import "../less/shortcut-list.less";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const ShortcutList: React.FC<Props> = () => {
|
||||
const ShortcutList = () => {
|
||||
const query = useAppSelector((state) => state.location.query);
|
||||
const shortcuts = useAppSelector((state) => state.shortcut.shortcuts);
|
||||
const loadingState = useLoading();
|
||||
const { t } = useI18n();
|
||||
|
||||
const pinnedShortcuts = shortcuts
|
||||
.filter((s) => s.rowStatus === "ARCHIVED")
|
||||
@@ -39,7 +38,7 @@ const ShortcutList: React.FC<Props> = () => {
|
||||
return (
|
||||
<div className="shortcuts-wrapper">
|
||||
<p className="title-text">
|
||||
<span className="normal-text">Shortcuts</span>
|
||||
<span className="normal-text">{t("common.shortcuts")}</span>
|
||||
<button className="btn" onClick={() => showCreateShortcutDialog()}>
|
||||
<Icon.Plus className="icon-img" />
|
||||
</button>
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
import { userService } from "../services";
|
||||
import { locationService, userService } from "../services";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import Icon from "./Icon";
|
||||
import Only from "./common/OnlyWhen";
|
||||
import showDailyReviewDialog from "./DailyReviewDialog";
|
||||
import showSettingDialog from "./SettingDialog";
|
||||
import showArchivedMemoDialog from "./ArchivedMemoDialog";
|
||||
import showResourcesDialog from "./ResourcesDialog";
|
||||
import UserBanner from "./UserBanner";
|
||||
import UsageHeatMap from "./UsageHeatMap";
|
||||
import ShortcutList from "./ShortcutList";
|
||||
import TagList from "./TagList";
|
||||
import "../less/siderbar.less";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const Sidebar: React.FC<Props> = () => {
|
||||
const Sidebar = () => {
|
||||
const { t } = useI18n();
|
||||
|
||||
const handleMyAccountBtnClick = () => {
|
||||
const handleSettingBtnClick = () => {
|
||||
showSettingDialog();
|
||||
};
|
||||
const handleResourcesBtnClick = () => {
|
||||
showResourcesDialog();
|
||||
};
|
||||
const handleArchivedBtnClick = () => {
|
||||
showArchivedMemoDialog();
|
||||
|
||||
const handleExploreBtnClick = () => {
|
||||
locationService.pushHistory("/explore");
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -41,15 +35,12 @@ const Sidebar: React.FC<Props> = () => {
|
||||
<span className="icon">📅</span> {t("sidebar.daily-review")}
|
||||
</button>
|
||||
<Only when={!userService.isVisitorMode()}>
|
||||
<button className="btn action-btn" onClick={handleResourcesBtnClick}>
|
||||
<span className="icon">🌄</span> {t("sidebar.resources")}
|
||||
<button className="btn action-btn" onClick={() => handleExploreBtnClick()}>
|
||||
<span className="icon">🏂</span> {t("common.explore")}
|
||||
</button>
|
||||
<button className="btn action-btn" onClick={handleMyAccountBtnClick}>
|
||||
<button className="btn action-btn" onClick={handleSettingBtnClick}>
|
||||
<span className="icon">⚙️</span> {t("sidebar.setting")}
|
||||
</button>
|
||||
<button className="btn action-btn" onClick={handleArchivedBtnClick}>
|
||||
<span className="icon">🗂</span> {t("sidebar.archived")}
|
||||
</button>
|
||||
</Only>
|
||||
</div>
|
||||
<Only when={!userService.isVisitorMode()}>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import * as utils from "../helpers/utils";
|
||||
import { useAppSelector } from "../store";
|
||||
import { locationService, memoService, userService } from "../services";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
@@ -14,9 +13,7 @@ interface Tag {
|
||||
subTags: Tag[];
|
||||
}
|
||||
|
||||
interface Props {}
|
||||
|
||||
const TagList: React.FC<Props> = () => {
|
||||
const TagList = () => {
|
||||
const { t } = useI18n();
|
||||
const { memos, tags: tagsText } = useAppSelector((state) => state.memo);
|
||||
const query = useAppSelector((state) => state.location.query);
|
||||
@@ -71,7 +68,7 @@ const TagList: React.FC<Props> = () => {
|
||||
|
||||
return (
|
||||
<div className="tags-wrapper">
|
||||
<p className="title-text">Tags</p>
|
||||
<p className="title-text">{t("common.tags")}</p>
|
||||
<div className="tags-container">
|
||||
{tags.map((t, idx) => (
|
||||
<TagItemContainer key={t.text + "-" + idx} tag={t} tagQuery={query?.tag} />
|
||||
@@ -99,7 +96,6 @@ const TagItemContainer: React.FC<TagItemContainerProps> = (props: TagItemContain
|
||||
if (isActive) {
|
||||
locationService.setTagQuery(undefined);
|
||||
} else {
|
||||
utils.copyTextToClipboard(`#${tag.text} `);
|
||||
locationService.setTagQuery(tag.text);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { createRoot, Root } from "react-dom/client";
|
||||
import { TOAST_ANIMATION_DURATION } from "../helpers/consts";
|
||||
import "../less/toast.less";
|
||||
|
||||
type ToastType = "normal" | "success" | "info" | "error";
|
||||
@@ -36,42 +35,26 @@ const Toast: React.FC<ToastItemProps> = (props: ToastItemProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
class ToastHelper {
|
||||
private shownToastAmount = 0;
|
||||
private toastWrapper: HTMLDivElement;
|
||||
private shownToastContainers: [Root, HTMLDivElement][] = [];
|
||||
// toast animation duration.
|
||||
const TOAST_ANIMATION_DURATION = 400;
|
||||
|
||||
constructor() {
|
||||
const wrapperClassName = "toast-list-container";
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.className = wrapperClassName;
|
||||
document.body.appendChild(tempDiv);
|
||||
this.toastWrapper = tempDiv;
|
||||
}
|
||||
const initialToastHelper = () => {
|
||||
const shownToastContainers: [Root, HTMLDivElement][] = [];
|
||||
let shownToastAmount = 0;
|
||||
|
||||
public info = (content: string, duration = 3000) => {
|
||||
return this.showToast({ type: "normal", content, duration });
|
||||
};
|
||||
const wrapperClassName = "toast-list-container";
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.className = wrapperClassName;
|
||||
document.body.appendChild(tempDiv);
|
||||
const toastWrapper = tempDiv;
|
||||
|
||||
public success = (content: string, duration = 3000) => {
|
||||
return this.showToast({ type: "success", content, duration });
|
||||
};
|
||||
|
||||
public error = (content: string, duration = 3000) => {
|
||||
return this.showToast({ type: "error", content, duration });
|
||||
};
|
||||
|
||||
private showToast = (config: ToastConfig) => {
|
||||
const showToast = (config: ToastConfig) => {
|
||||
const tempDiv = document.createElement("div");
|
||||
const toast = createRoot(tempDiv);
|
||||
tempDiv.className = `toast-wrapper ${config.type}`;
|
||||
this.toastWrapper.appendChild(tempDiv);
|
||||
this.shownToastAmount++;
|
||||
this.shownToastContainers.push([toast, tempDiv]);
|
||||
|
||||
setTimeout(() => {
|
||||
tempDiv.classList.add("showup");
|
||||
}, 0);
|
||||
toastWrapper.appendChild(tempDiv);
|
||||
shownToastAmount++;
|
||||
shownToastContainers.push([toast, tempDiv]);
|
||||
|
||||
const cbs = {
|
||||
destory: () => {
|
||||
@@ -82,13 +65,13 @@ class ToastHelper {
|
||||
return;
|
||||
}
|
||||
|
||||
this.shownToastAmount--;
|
||||
if (this.shownToastAmount === 0) {
|
||||
for (const [root, tempDiv] of this.shownToastContainers) {
|
||||
shownToastAmount--;
|
||||
if (shownToastAmount === 0) {
|
||||
for (const [root, tempDiv] of shownToastContainers) {
|
||||
root.unmount();
|
||||
tempDiv.remove();
|
||||
}
|
||||
this.shownToastContainers.splice(0, this.shownToastContainers.length);
|
||||
shownToastContainers.splice(0, shownToastContainers.length);
|
||||
}
|
||||
}, TOAST_ANIMATION_DURATION);
|
||||
},
|
||||
@@ -96,10 +79,32 @@ class ToastHelper {
|
||||
|
||||
toast.render(<Toast {...config} destory={cbs.destory} />);
|
||||
|
||||
setTimeout(() => {
|
||||
tempDiv.classList.add("showup");
|
||||
}, 10);
|
||||
|
||||
return cbs;
|
||||
};
|
||||
}
|
||||
|
||||
const toastHelper = new ToastHelper();
|
||||
const info = (content: string, duration = 3000) => {
|
||||
return showToast({ type: "normal", content, duration });
|
||||
};
|
||||
|
||||
const success = (content: string, duration = 3000) => {
|
||||
return showToast({ type: "success", content, duration });
|
||||
};
|
||||
|
||||
const error = (content: string, duration = 3000) => {
|
||||
return showToast({ type: "error", content, duration });
|
||||
};
|
||||
|
||||
return {
|
||||
info,
|
||||
success,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
const toastHelper = initialToastHelper();
|
||||
|
||||
export default toastHelper;
|
||||
|
||||
@@ -26,9 +26,7 @@ interface DailyUsageStat {
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface Props {}
|
||||
|
||||
const UsageHeatMap: React.FC<Props> = () => {
|
||||
const UsageHeatMap = () => {
|
||||
const todayTimeStamp = utils.getDateStampByDate(Date.now());
|
||||
const todayDay = new Date(todayTimeStamp).getDay() + 1;
|
||||
const nullCell = new Array(7 - todayDay).fill(0);
|
||||
@@ -101,19 +99,25 @@ const UsageHeatMap: React.FC<Props> = () => {
|
||||
: "stat-day-L4-bg";
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`stat-container ${colorLevel} ${currentStat === v ? "current" : ""} ${
|
||||
todayTimeStamp === v.timestamp ? "today" : ""
|
||||
}`}
|
||||
<div
|
||||
className="stat-wrapper"
|
||||
key={i}
|
||||
onMouseEnter={(e) => handleUsageStatItemMouseEnter(e, v)}
|
||||
onMouseLeave={handleUsageStatItemMouseLeave}
|
||||
onClick={() => handleUsageStatItemClick(v)}
|
||||
></span>
|
||||
>
|
||||
<span
|
||||
className={`stat-container ${colorLevel} ${currentStat === v ? "current" : ""} ${
|
||||
todayTimeStamp === v.timestamp ? "today" : ""
|
||||
}`}
|
||||
></span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{nullCell.map((_, i) => (
|
||||
<span className="stat-container null" key={i}></span>
|
||||
<div className="stat-wrapper" key={i}>
|
||||
<span className="stat-container null"></span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import * as utils from "../helpers/utils";
|
||||
import userService from "../services/userService";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import { locationService } from "../services";
|
||||
import { useAppSelector } from "../store";
|
||||
import Icon from "./Icon";
|
||||
import MenuBtnsPopup from "./MenuBtnsPopup";
|
||||
import "../less/user-banner.less";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const UserBanner: React.FC<Props> = () => {
|
||||
const UserBanner = () => {
|
||||
const { t } = useI18n();
|
||||
const { user, owner } = useAppSelector((state) => state.user);
|
||||
const { memos, tags } = useAppSelector((state) => state.memo);
|
||||
const [shouldShowPopupBtns, setShouldShowPopupBtns] = useState(false);
|
||||
@@ -53,15 +53,15 @@ const UserBanner: React.FC<Props> = () => {
|
||||
<div className="amount-text-container">
|
||||
<div className="status-text memos-text">
|
||||
<span className="amount-text">{memos.length}</span>
|
||||
<span className="type-text">MEMO</span>
|
||||
<span className="type-text">{t("amount-text.memo")}</span>
|
||||
</div>
|
||||
<div className="status-text tags-text">
|
||||
<span className="amount-text">{tags.length}</span>
|
||||
<span className="type-text">TAG</span>
|
||||
<span className="type-text">{t("amount-text.tag")}</span>
|
||||
</div>
|
||||
<div className="status-text duration-text">
|
||||
<span className="amount-text">{createdDays}</span>
|
||||
<span className="type-text">DAY</span>
|
||||
<span className="type-text">{t("amount-text.day")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -58,6 +58,10 @@ export function deleteUser(userDelete: UserDelete) {
|
||||
return axios.delete(`/api/user/${userDelete.id}`);
|
||||
}
|
||||
|
||||
export function getAllMemos() {
|
||||
return axios.get<ResponseObject<Memo[]>>("/api/memo/all");
|
||||
}
|
||||
|
||||
export function getMemoList(memoFind?: MemoFind) {
|
||||
const queryList = [];
|
||||
if (memoFind?.creatorId) {
|
||||
|
||||
@@ -4,9 +4,6 @@ export const UNKNOWN_ID = -1;
|
||||
// default animation duration
|
||||
export const ANIMATION_DURATION = 200;
|
||||
|
||||
// toast animation duration
|
||||
export const TOAST_ANIMATION_DURATION = 400;
|
||||
|
||||
// millisecond in a day
|
||||
export const DAILY_TIMESTAMP = 3600 * 24 * 1000;
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@ export const MEMO_LINK_REG = /@\[(.+?)\]\((.+?)\)/g;
|
||||
|
||||
const parseMarkedToHtml = (markedStr: string): string => {
|
||||
const htmlText = markedStr
|
||||
.replace(/([\u4e00-\u9fa5])([A-Za-z0-9?.,;[\]]+)/g, "$1 $2")
|
||||
.replace(/([A-Za-z0-9?.,;[\]]+)([\u4e00-\u9fa5])/g, "$1 $2")
|
||||
.replace(CODE_BLOCK_REG, "<pre lang=''>$1</pre>")
|
||||
.replace(TODO_BLOCK_REG, "<span class='todo-block todo' data-value='TODO'></span>")
|
||||
.replace(DONE_BLOCK_REG, "<span class='todo-block done' data-value='DONE'>✓</span>")
|
||||
@@ -45,10 +43,10 @@ const defaultFormatterConfig: FormatterConfig = {
|
||||
inlineImage: false,
|
||||
};
|
||||
|
||||
const formatMemoContent = (content: string, addtionConfig?: Partial<FormatterConfig>) => {
|
||||
const formatMemoContent = (content: string, additionConfig?: Partial<FormatterConfig>) => {
|
||||
const config = {
|
||||
...defaultFormatterConfig,
|
||||
...addtionConfig,
|
||||
...additionConfig,
|
||||
};
|
||||
const tempElement = document.createElement("div");
|
||||
tempElement.innerHTML = parseMarkedToHtml(escape(content));
|
||||
|
||||
@@ -147,18 +147,6 @@ export function filterObjectNullKeys(object: KVObject): KVObject {
|
||||
return finalObject;
|
||||
}
|
||||
|
||||
export async function copyTextToClipboard(text: string) {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (error: unknown) {
|
||||
console.warn("Copy to clipboard failed.", error);
|
||||
}
|
||||
} else {
|
||||
console.warn("Copy to clipboard failed, methods not supports.");
|
||||
}
|
||||
}
|
||||
|
||||
export function getImageSize(src: string): Promise<{ width: number; height: number }> {
|
||||
return new Promise((resolve) => {
|
||||
const imgEl = new Image();
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// A custom hook that builds on useLocation to parse
|
||||
|
||||
import React from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
// the query string for you.
|
||||
const useQuery = () => {
|
||||
const { search } = useLocation();
|
||||
|
||||
return React.useMemo(() => new URLSearchParams(search), [search]);
|
||||
};
|
||||
|
||||
export default useQuery;
|
||||
@@ -2,10 +2,12 @@ import { useEffect, useState } from "react";
|
||||
import i18nStore from "./i18nStore";
|
||||
import enLocale from "../../locales/en.json";
|
||||
import zhLocale from "../../locales/zh.json";
|
||||
import viLocale from "../../locales/vi.json";
|
||||
|
||||
const resources: Record<string, any> = {
|
||||
en: enLocale,
|
||||
zh: zhLocale,
|
||||
vi: viLocale,
|
||||
};
|
||||
|
||||
const useI18n = () => {
|
||||
@@ -21,7 +23,7 @@ const useI18n = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const translate = (key: string) => {
|
||||
const translate = (key: string): string => {
|
||||
const keys = key.split(".");
|
||||
let value = resources[locale];
|
||||
for (const k of keys) {
|
||||
|
||||
@@ -19,13 +19,5 @@
|
||||
|
||||
> .memo-content-container {
|
||||
@apply flex flex-col justify-start items-start w-full overflow-x-hidden p-0 text-base;
|
||||
|
||||
> .images-container {
|
||||
@apply flex flex-col justify-start items-start mt-1 w-full;
|
||||
|
||||
> img {
|
||||
@apply w-full h-auto rounded mb-2 last:mb-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
@import "./mixin.less";
|
||||
|
||||
.page-wrapper.explore {
|
||||
@apply relative top-0 w-full h-screen overflow-y-auto overflow-x-hidden;
|
||||
background-color: #f6f5f4;
|
||||
|
||||
> .page-container {
|
||||
@apply relative w-full min-h-screen mx-auto flex flex-col justify-start items-center pb-8;
|
||||
|
||||
> .page-header {
|
||||
@apply sticky top-0 z-10 max-w-2xl w-full min-h-full flex flex-row justify-between items-center px-4 sm:pr-6 pt-6 mb-2;
|
||||
background-color: #f6f5f4;
|
||||
|
||||
> .title-container {
|
||||
@apply flex flex-row justify-start items-center;
|
||||
|
||||
> .logo-img {
|
||||
@apply h-12 sm:h-14 w-auto mr-1;
|
||||
}
|
||||
|
||||
> .title-text {
|
||||
@apply text-xl sm:text-3xl font-mono text-gray-700;
|
||||
}
|
||||
}
|
||||
|
||||
> .action-button-container {
|
||||
> .btn {
|
||||
@apply text-gray-600 font-mono text-base py-1 border px-3 rounded-xl hover:opacity-80 hover:underline;
|
||||
|
||||
> .icon {
|
||||
@apply text-lg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .memos-wrapper {
|
||||
@apply relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4 sm:pr-6;
|
||||
|
||||
> .memo-container {
|
||||
@apply flex flex-col justify-start items-start w-full p-4 mt-2 bg-white rounded-lg border border-white hover:border-gray-200;
|
||||
|
||||
> .memo-header {
|
||||
@apply mb-2 w-full flex flex-row justify-start items-center text-sm font-mono text-gray-400;
|
||||
|
||||
> .split-text {
|
||||
@apply mx-2;
|
||||
}
|
||||
|
||||
> .name-text {
|
||||
@apply hover:text-green-600 hover:underline;
|
||||
}
|
||||
}
|
||||
|
||||
> .memo-content {
|
||||
@apply cursor-default;
|
||||
|
||||
> * {
|
||||
@apply cursor-default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,29 +55,7 @@
|
||||
}
|
||||
|
||||
> .memo-container {
|
||||
.flex(column, flex-start, flex-start);
|
||||
@apply w-full pt-2;
|
||||
|
||||
> .memo-content-text {
|
||||
@apply w-full text-base;
|
||||
}
|
||||
|
||||
> .images-wrapper {
|
||||
.flex(row, flex-start, flex-start);
|
||||
@apply w-full mt-2 overflow-x-auto overflow-y-hidden;
|
||||
padding-bottom: 2px;
|
||||
.pretty-scroll-bar(0, 2px);
|
||||
|
||||
> .memo-img {
|
||||
@apply mr-2 w-auto h-32 shrink-0 grow-0 overflow-y-hidden hover:border-gray-400 last:mr-0;
|
||||
.hide-scroll-bar();
|
||||
|
||||
> img {
|
||||
@apply w-auto rounded-lg;
|
||||
max-height: 128px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@apply w-full flex flex-col justify-start items-start pt-2;
|
||||
}
|
||||
|
||||
> .normal-text {
|
||||
|
||||
@@ -1,41 +1,78 @@
|
||||
@import "./mixin.less";
|
||||
|
||||
.memo-content-text {
|
||||
@apply w-full whitespace-pre-wrap break-words text-base leading-7;
|
||||
.memo-content-wrapper {
|
||||
@apply w-full flex flex-col justify-start items-start;
|
||||
|
||||
> p {
|
||||
@apply inline-block w-full h-auto mb-1 last:mb-0 text-base leading-7 whitespace-pre-wrap break-words;
|
||||
> .memo-content-text {
|
||||
@apply w-full whitespace-pre-wrap break-words text-base leading-7;
|
||||
|
||||
&.expanded {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 8;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
> p {
|
||||
@apply inline-block w-full h-auto mb-1 last:mb-0 text-base leading-7 whitespace-pre-wrap break-words;
|
||||
}
|
||||
|
||||
.img {
|
||||
@apply float-left max-w-full w-full;
|
||||
}
|
||||
|
||||
.tag-span {
|
||||
@apply inline-block w-auto font-mono text-blue-600;
|
||||
}
|
||||
|
||||
.memo-link-text {
|
||||
@apply inline-block text-blue-600 cursor-pointer font-bold border-none no-underline hover:opacity-80;
|
||||
}
|
||||
|
||||
.link {
|
||||
@apply inline-block text-blue-600 cursor-pointer underline break-all hover:opacity-80;
|
||||
}
|
||||
|
||||
.counter-block,
|
||||
.todo-block {
|
||||
@apply float-left inline-block box-border text-center w-7 font-mono select-none;
|
||||
}
|
||||
|
||||
.todo-block {
|
||||
@apply w-4 h-4 leading-4 border rounded box-border text-lg cursor-pointer shadow-inner hover:opacity-80;
|
||||
margin-top: 6px;
|
||||
margin-left: 6px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply w-full mt-1 py-2 px-3 rounded text-sm bg-gray-100 whitespace-pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.img {
|
||||
@apply float-left max-w-full w-full;
|
||||
}
|
||||
> .expand-btn-container {
|
||||
@apply w-full relative flex flex-row justify-start items-center;
|
||||
|
||||
.tag-span {
|
||||
@apply inline-block w-auto font-mono text-blue-600;
|
||||
}
|
||||
> .btn {
|
||||
@apply flex flex-row justify-start items-center pl-2 pr-1 py-1 my-1 text-xs rounded-lg border bg-gray-100 border-gray-200 opacity-80 shadow hover:opacity-60;
|
||||
|
||||
.memo-link-text {
|
||||
@apply inline-block text-blue-600 cursor-pointer font-bold border-none no-underline hover:opacity-80;
|
||||
}
|
||||
&.expand-btn {
|
||||
@apply mt-2;
|
||||
|
||||
.link {
|
||||
@apply inline-block text-blue-600 cursor-pointer underline break-all hover:opacity-80;
|
||||
}
|
||||
> .icon-img {
|
||||
@apply rotate-90;
|
||||
}
|
||||
}
|
||||
|
||||
.counter-block,
|
||||
.todo-block {
|
||||
@apply float-left inline-block box-border text-center w-7 font-mono select-none;
|
||||
}
|
||||
&.fold-btn {
|
||||
> .icon-img {
|
||||
@apply -rotate-90;
|
||||
}
|
||||
}
|
||||
|
||||
.todo-block {
|
||||
@apply w-4 h-4 leading-4 border rounded box-border text-lg cursor-pointer shadow-inner hover:opacity-80;
|
||||
margin-top: 6px;
|
||||
margin-left: 6px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply w-full mt-1 py-2 px-3 rounded text-sm bg-gray-100 whitespace-pre-wrap;
|
||||
> .icon-img {
|
||||
@apply w-4 h-auto ml-1 transition-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
}
|
||||
|
||||
> .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;
|
||||
@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 font-mono bg-black;
|
||||
|
||||
> .item-container {
|
||||
@apply w-full text-white cursor-pointer rounded text-sm leading-6 px-2 hover:bg-gray-700;
|
||||
@@ -62,4 +62,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-picker-react {
|
||||
@apply absolute;
|
||||
|
||||
li.emoji::before {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
@import "./mixin.less";
|
||||
|
||||
.resource-wrapper {
|
||||
@apply w-full flex flex-col justify-start items-start;
|
||||
|
||||
> .images-wrapper {
|
||||
@apply flex flex-row justify-start items-start mt-2 w-full overflow-x-auto overflow-y-hidden pb-1;
|
||||
.pretty-scroll-bar(0, 2px);
|
||||
|
||||
> .memo-img {
|
||||
@apply mr-2 last:mr-0 w-auto h-auto shrink-0 grow-0 overflow-y-hidden;
|
||||
.hide-scroll-bar();
|
||||
|
||||
> img {
|
||||
@apply w-auto max-h-40 rounded-lg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,18 +140,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .images-wrapper {
|
||||
@apply flex flex-row justify-start items-start mt-2 w-full overflow-x-auto overflow-y-hidden pb-1;
|
||||
.pretty-scroll-bar(0, 2px);
|
||||
|
||||
> .memo-img {
|
||||
@apply mr-2 last:mr-0 w-auto h-auto shrink-0 grow-0 overflow-y-hidden;
|
||||
.hide-scroll-bar();
|
||||
|
||||
> img {
|
||||
@apply w-auto max-h-40 rounded-lg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,6 @@
|
||||
> .dialog-content-container {
|
||||
@apply flex flex-col justify-start items-start w-full;
|
||||
|
||||
> .tip-text-container {
|
||||
@apply w-full flex flex-row justify-start items-start border border-yellow-600 rounded px-2 py-1 mb-2 text-yellow-600 bg-yellow-50 text-sm;
|
||||
}
|
||||
|
||||
> .upload-resource-container {
|
||||
@apply mt-2 mb-4 py-8 cursor-pointer w-full rounded flex flex-row justify-center items-center bg-blue-50 border border-blue-600 hover:opacity-80;
|
||||
|
||||
|
||||
@@ -44,11 +44,11 @@
|
||||
}
|
||||
|
||||
> .time-text {
|
||||
@apply w-full px-6 pt-5 text-xs text-gray-500 bg-white;
|
||||
@apply w-full px-6 pt-5 pb-2 text-xs text-gray-500 bg-white;
|
||||
}
|
||||
|
||||
> .memo-content-text {
|
||||
@apply w-full pt-2 pb-4 px-6 text-base bg-white;
|
||||
> .memo-content-wrapper {
|
||||
@apply w-full px-6 text-base bg-white pb-2;
|
||||
}
|
||||
|
||||
> .images-container {
|
||||
|
||||
@@ -54,10 +54,14 @@
|
||||
@apply flex-row justify-end items-center hidden shrink-0;
|
||||
|
||||
> .action-btn {
|
||||
@apply flex flex-row justify-center items-center w-6 h-6 shrink-0;
|
||||
@apply flex flex-row justify-center items-center;
|
||||
|
||||
&.toggle-btn {
|
||||
@apply w-4 h-auto text-gray-600;
|
||||
@apply text-gray-600;
|
||||
|
||||
> .icon-img {
|
||||
@apply w-4 h-auto;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
& + .action-btns-wrapper {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
> .toast-wrapper {
|
||||
@apply flex flex-col justify-start items-start relative left-full invisible text-base cursor-pointer shadow-lg rounded bg-white mt-6 py-2 px-4;
|
||||
min-width: 6em;
|
||||
left: calc(100% + 32px);
|
||||
transition: all 0.4s ease;
|
||||
|
||||
&.showup {
|
||||
|
||||
@@ -23,35 +23,36 @@
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
grid-auto-flow: column;
|
||||
|
||||
> .stat-container {
|
||||
@apply block rounded-sm;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-color: @bg-lightgray;
|
||||
margin-bottom: 2px;
|
||||
> .stat-wrapper {
|
||||
> .stat-container {
|
||||
@apply block rounded-sm;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-color: @bg-lightgray;
|
||||
|
||||
&.null {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
&.null {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
&.stat-day-L1-bg {
|
||||
background-color: @stat-day-L1-bg !important;
|
||||
}
|
||||
&.stat-day-L1-bg {
|
||||
background-color: @stat-day-L1-bg !important;
|
||||
}
|
||||
|
||||
&.stat-day-L2-bg {
|
||||
background-color: @stat-day-L2-bg !important;
|
||||
}
|
||||
&.stat-day-L2-bg {
|
||||
background-color: @stat-day-L2-bg !important;
|
||||
}
|
||||
|
||||
&.stat-day-L3-bg {
|
||||
background-color: @stat-day-L3-bg !important;
|
||||
}
|
||||
&.stat-day-L3-bg {
|
||||
background-color: @stat-day-L3-bg !important;
|
||||
}
|
||||
|
||||
&.stat-day-L4-bg {
|
||||
background-color: @stat-day-L4-bg !important;
|
||||
}
|
||||
&.stat-day-L4-bg {
|
||||
background-color: @stat-day-L4-bg !important;
|
||||
}
|
||||
|
||||
&.today {
|
||||
border: 1px solid black;
|
||||
&.today {
|
||||
border: 1px solid black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+65
-2
@@ -3,6 +3,8 @@
|
||||
"about": "About",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"new-password": "New passworld",
|
||||
"repeat-new-password": "Repeat the new password",
|
||||
"username": "Username",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
@@ -22,9 +24,18 @@
|
||||
"archive": "Archive",
|
||||
"basic": "Basic",
|
||||
"admin": "Admin",
|
||||
"explore": "Explore",
|
||||
"sign-in": "Sign in",
|
||||
"sign-out": "Sign out",
|
||||
"back-to-home": "Back to Home"
|
||||
"back-to-home": "Back to Home",
|
||||
"type": "Type",
|
||||
"shortcuts": "Shortcuts",
|
||||
"title": "Title",
|
||||
"filter": "Filter",
|
||||
"tags": "Tags",
|
||||
"yourself": "Yourself",
|
||||
"archived-at": "Archived at",
|
||||
"changed": "changed"
|
||||
},
|
||||
"slogan": "An open source, self-hosted knowledge base that works with a SQLite db file.",
|
||||
"auth": {
|
||||
@@ -38,10 +49,28 @@
|
||||
"setting": "Setting",
|
||||
"archived": "Archived"
|
||||
},
|
||||
"daily-review": {
|
||||
"oops-nothing": "Oops, there is nothing."
|
||||
},
|
||||
"resources": {
|
||||
"description": "View your static resources in memos. e.g. images",
|
||||
"no-resources": "No resources.",
|
||||
"fetching-data": "fetching data...",
|
||||
"upload": "Upload",
|
||||
"preview": "Preview",
|
||||
"copy-link": "Copy Link"
|
||||
},
|
||||
"archived": {
|
||||
"archived-memos": "Archived Memos",
|
||||
"no-archived-memos": "No archived memos.",
|
||||
"fetching-data": "fetching data..."
|
||||
},
|
||||
"editor": {
|
||||
"editing": "Editing...",
|
||||
"save": "Save",
|
||||
"placeholder": "Any thoughts..."
|
||||
"placeholder": "Any thoughts...",
|
||||
"only-image-supported": "Only image file supported.",
|
||||
"cant-empty": "Content can't be empty"
|
||||
},
|
||||
"memo": {
|
||||
"view-story": "View Story"
|
||||
@@ -49,13 +78,26 @@
|
||||
"memo-list": {
|
||||
"fetching-data": "fetching data..."
|
||||
},
|
||||
"shortcut-list": {
|
||||
"shortcut-title": "shortcut title",
|
||||
"create-shortcut": "Create Shortcut",
|
||||
"edit-shortcut": "Edit Shortcut",
|
||||
"new-filter": "New Filter",
|
||||
"eligible-memo": "eligible memo",
|
||||
"fill-previous": "Please fill in previous filter value",
|
||||
"title-required": "Title is required"
|
||||
},
|
||||
"tag-list": {
|
||||
"tip-text": "Enter `#tag ` to create"
|
||||
},
|
||||
"search": {
|
||||
"quickly-filter": "Quickly filter"
|
||||
},
|
||||
"setting": {
|
||||
"my-account": "My Account",
|
||||
"preference": "Preference",
|
||||
"member": "Member",
|
||||
"member-list": "Member list",
|
||||
"account-section": {
|
||||
"title": "Account Information"
|
||||
},
|
||||
@@ -66,5 +108,26 @@
|
||||
"member-section": {
|
||||
"create-a-member": "Create a member"
|
||||
}
|
||||
},
|
||||
"amount-text": {
|
||||
"memo": "MEMO",
|
||||
"tag": "TAG",
|
||||
"day": "DAY"
|
||||
},
|
||||
"message": {
|
||||
"no-memos": "no memos 🌃",
|
||||
"memos-ready": "all memos are ready 🎉",
|
||||
"restored-successfully": "Restored successfully",
|
||||
"memo-updated-datetime": "Memo created datetime changed.",
|
||||
"invalid-created-datetime": "Invalid created datetime.",
|
||||
"change-memo-created-time": "Change memo created time",
|
||||
"memo-not-found": "Memo not found.",
|
||||
"fill-all": "Please fill in all fields.",
|
||||
"new-password-not-match": "New passwords do not match.",
|
||||
"image-load-failed": "Image load failed",
|
||||
"fill-form": "Please fill out this form",
|
||||
"login-failed": "Login failed",
|
||||
"signup-failed": "Signup failed",
|
||||
"user-not-found": "User not found"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"common": {
|
||||
"about": "Thông Tin",
|
||||
"email": "Email",
|
||||
"password": "Mật khẩu",
|
||||
"new-password": "Mật khẩu mới",
|
||||
"repeat-new-password": "Nhập lại mật khẩu mới",
|
||||
"username": "Tên đăng nhập",
|
||||
"save": "Lưu",
|
||||
"cancel": "Hủy",
|
||||
"create": "Tạo mới",
|
||||
"change": "Thay đổi",
|
||||
"reset": "Khôi phục",
|
||||
"language": "Ngôn Ngữ",
|
||||
"version": "Phiên bản",
|
||||
"pin": "Gắn lên trên cùng",
|
||||
"unpin": "Gỡ khỏi vị trị trên cùng",
|
||||
"edit": "Chỉnh sửa",
|
||||
"restore": "Khôi phục",
|
||||
"delete": "Xóa",
|
||||
"null": "Null",
|
||||
"share": "Chia sẻ",
|
||||
"mark": "Đánh dấu",
|
||||
"archive": "Lưu trữ",
|
||||
"basic": "Cơ bản",
|
||||
"admin": "Admin",
|
||||
"explore": "Khám phá",
|
||||
"sign-in": "Đăng nhập",
|
||||
"sign-out": "Đăng xuất",
|
||||
"back-to-home": "Quay về trang chủ",
|
||||
"type": "Kiểu",
|
||||
"shortcuts": "Lối tắt",
|
||||
"title": "Tên",
|
||||
"filter": "Bộ lọc",
|
||||
"tags": "Thẻ",
|
||||
"yourself": "Của bạn",
|
||||
"archived-at": "Lưu trữ lúc",
|
||||
"changed": "đã thay đổi"
|
||||
},
|
||||
"slogan": "Một mã nguồn mở, tự lưu lại mọi thứ bạn biết dựa trên SQLite db.",
|
||||
"auth": {
|
||||
"signup-as-host": "Đăng ký như chủ nhân",
|
||||
"host-tip": "Bạn đang đăng ký với tư cách chủ nhân của trang web này.",
|
||||
"not-host-tip": "Nếu chưa có tài khoản, bạn cần phải liên hệ chủ nhân của trang web."
|
||||
},
|
||||
"sidebar": {
|
||||
"daily-review": "Đánh giá hàng ngày",
|
||||
"resources": "Tài nguyên",
|
||||
"setting": "Cài đặt",
|
||||
"archived": "Lưu trữ"
|
||||
},
|
||||
"daily-review": {
|
||||
"oops-nothing": "Oops, chẳng có gì ở đây cả."
|
||||
},
|
||||
"resources": {
|
||||
"description": "Xem toàn bộ tài nguyên của trang web. ví dụ hình ảnh",
|
||||
"no-resources": "Không có tài nguyên.",
|
||||
"fetching-data": "đang tải dữ liệu...",
|
||||
"upload": "Tải lên",
|
||||
"preview": "Xem trước",
|
||||
"copy-link": "Sao chép"
|
||||
},
|
||||
"archived": {
|
||||
"archived-memos": "Memo đã lưu trữ",
|
||||
"no-archived-memos": "Không có memo nào cả.",
|
||||
"fetching-data": "đang tải dữ liệu..."
|
||||
},
|
||||
"editor": {
|
||||
"editing": "Đang chỉnh sửa...",
|
||||
"save": "Lưu",
|
||||
"placeholder": "Bất cứ gì bạn đang nghĩ...",
|
||||
"only-image-supported": "Chỉ hỗ trợ hình ảnh.",
|
||||
"cant-empty": "Nội dung không thể trống"
|
||||
},
|
||||
"memo": {
|
||||
"view-story": "Xem nội dung"
|
||||
},
|
||||
"memo-list": {
|
||||
"fetching-data": "đang tải dữ liệu..."
|
||||
},
|
||||
"shortcut-list": {
|
||||
"shortcut-title": "Tên lối tắt",
|
||||
"create-shortcut": "Tạo lối tắt",
|
||||
"edit-shortcut": "Chỉnh sửa lối tắt",
|
||||
"new-filter": "Bộ lọc mới",
|
||||
"eligible-memo": "memo đạt chuẩn",
|
||||
"fill-previous": "Vui lòng nhập vào giá trị của bộ lọc trước đó",
|
||||
"title-required": "Tiêu đề là bắt buộc"
|
||||
},
|
||||
"tag-list": {
|
||||
"tip-text": "Nhập `#tag` để tạo mới"
|
||||
},
|
||||
"search": {
|
||||
"quickly-filter": "Lọc nhanh"
|
||||
},
|
||||
"setting": {
|
||||
"my-account": "Tài khoản",
|
||||
"preference": "Sở thích",
|
||||
"member": "Thành viên",
|
||||
"member-list": "Danh sách thành viên",
|
||||
"account-section": {
|
||||
"title": "Thông tin tài khoản"
|
||||
},
|
||||
"preference-section": {
|
||||
"default-memo-visibility": "Chế độ memo mặc định",
|
||||
"editor-font-style": "Thay đổi font cho trình soạn thảo"
|
||||
},
|
||||
"member-section": {
|
||||
"create-a-member": "Tạo một thành viên"
|
||||
}
|
||||
},
|
||||
"amount-text": {
|
||||
"memo": "MEMO",
|
||||
"tag": "THẺ",
|
||||
"day": "NGÀY"
|
||||
},
|
||||
"message": {
|
||||
"no-memos": "Không có memo nào 🌃",
|
||||
"memos-ready": "Tất cả memo ở đây rồi 🎉",
|
||||
"restored-successfully": "Đã khôi phục thành công",
|
||||
"memo-updated-datetime": "Memo đã thay đổi ngày giờ tạo.",
|
||||
"invalid-created-datetime": "Ngày giờ tạo không hợp lệ.",
|
||||
"change-memo-created-time": "Thay đổi thời gian tạo memo",
|
||||
"memo-not-found": "Không tìm thấy memo.",
|
||||
"fill-all": "Vui lòng nhập tất cả các mục.",
|
||||
"new-password-not-match": "Mật khẩu mới không giống nhau.",
|
||||
"image-load-failed": "Tải ảnh bị lỗi",
|
||||
"fill-form": "Please fill out this form",
|
||||
"login-failed": "Đăng nhập thất bại",
|
||||
"signup-failed": "Đăng ký thất bại",
|
||||
"user-not-found": "Không tìm thấy người dùng này"
|
||||
}
|
||||
}
|
||||
+65
-2
@@ -3,6 +3,8 @@
|
||||
"about": "关于",
|
||||
"email": "邮箱",
|
||||
"password": "密码",
|
||||
"new-password": "新密码",
|
||||
"repeat-new-password": "重复新密码",
|
||||
"username": "用户名",
|
||||
"save": "保存",
|
||||
"cancel": "退出",
|
||||
@@ -22,9 +24,18 @@
|
||||
"archive": "归档",
|
||||
"basic": "基础",
|
||||
"admin": "管理员",
|
||||
"explore": "探索",
|
||||
"sign-in": "登录",
|
||||
"sign-out": "退出登录",
|
||||
"back-to-home": "回到主页"
|
||||
"back-to-home": "回到主页",
|
||||
"type": "类型",
|
||||
"shortcuts": "捷径",
|
||||
"title": "标题",
|
||||
"filter": "过滤器",
|
||||
"tags": "全部标签",
|
||||
"yourself": "你自己",
|
||||
"archived-at": "归档于",
|
||||
"changed": "已更改"
|
||||
},
|
||||
"slogan": "一个开源的、支持私有化部署的碎片化知识卡片管理工具。",
|
||||
"auth": {
|
||||
@@ -38,10 +49,28 @@
|
||||
"setting": "设置",
|
||||
"archived": "已归档"
|
||||
},
|
||||
"daily-review": {
|
||||
"oops-nothing": "啊哦,空空荡荡。"
|
||||
},
|
||||
"resources": {
|
||||
"description": "查看在 Memo 中的静态资源。例如:图片",
|
||||
"no-resources": "没有资源",
|
||||
"fetching-data": "请求数据中...",
|
||||
"upload": "上传",
|
||||
"preview": "预览",
|
||||
"copy-link": "拷贝链接"
|
||||
},
|
||||
"archived": {
|
||||
"archived-memos": "已归档的 Memo",
|
||||
"no-archived-memos": "没有归档的 Memo",
|
||||
"fetching-data": "请求数据中..."
|
||||
},
|
||||
"editor": {
|
||||
"editing": "编辑中...",
|
||||
"save": "记下",
|
||||
"placeholder": "现在的想法是..."
|
||||
"placeholder": "现在的想法是...",
|
||||
"only-image-supported": "仅支持图片文件。",
|
||||
"cant-empty": "Content can't be empty"
|
||||
},
|
||||
"memo": {
|
||||
"view-story": "查看详情"
|
||||
@@ -49,13 +78,26 @@
|
||||
"memo-list": {
|
||||
"fetching-data": "请求数据中..."
|
||||
},
|
||||
"shortcut-list": {
|
||||
"shortcut-title": "捷径标题",
|
||||
"create-shortcut": "创建捷径",
|
||||
"edit-shortcut": "编辑捷径",
|
||||
"new-filter": "新建过滤器",
|
||||
"eligible-memo": "符合条件的 memo",
|
||||
"fill-previous": "请填写之前的过滤值",
|
||||
"title-required": "标题是必填项。"
|
||||
},
|
||||
"tag-list": {
|
||||
"tip-text": "输入`#tag `来创建标签"
|
||||
},
|
||||
"search": {
|
||||
"quickly-filter": "快速过滤"
|
||||
},
|
||||
"setting": {
|
||||
"my-account": "我的账号",
|
||||
"preference": "偏好设置",
|
||||
"member": "成员",
|
||||
"member-list": "成员列表",
|
||||
"account-section": {
|
||||
"title": "账号信息"
|
||||
},
|
||||
@@ -66,5 +108,26 @@
|
||||
"member-section": {
|
||||
"create-a-member": "创建成员"
|
||||
}
|
||||
},
|
||||
"amount-text": {
|
||||
"memo": "MEMO",
|
||||
"tag": "TAG",
|
||||
"day": "DAY"
|
||||
},
|
||||
"message": {
|
||||
"no-memos": "没有 memo 了 🌃",
|
||||
"memos-ready": "所有 memo 已就绪 🎉",
|
||||
"restored-successfully": "恢复成功",
|
||||
"memo-updated-datetime": "Memo创建日期时间已更改。",
|
||||
"invalid-created-datetime": "创建的日期时间无效。",
|
||||
"change-memo-created-time": "更改Memo创建时间",
|
||||
"memo-not-found": "找不到Memo。",
|
||||
"fill-all": "请填写所有字段。",
|
||||
"new-password-not-match": "新密码不匹配。",
|
||||
"image-load-failed": "图片加载失败",
|
||||
"fill-form": "请填写此表单",
|
||||
"login-failed": "登录失败",
|
||||
"signup-failed": "注册失败",
|
||||
"user-not-found": "未找到用户"
|
||||
}
|
||||
}
|
||||
|
||||
+8
-5
@@ -1,5 +1,6 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { Provider } from "react-redux";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import I18nProvider from "./labs/i18n/I18nProvider";
|
||||
import store from "./store";
|
||||
import App from "./App";
|
||||
@@ -10,9 +11,11 @@ import "./css/index.css";
|
||||
const container = document.getElementById("root");
|
||||
const root = createRoot(container as HTMLElement);
|
||||
root.render(
|
||||
<I18nProvider>
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
</I18nProvider>
|
||||
<BrowserRouter>
|
||||
<I18nProvider>
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
</I18nProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
+11
-9
@@ -9,8 +9,6 @@ import Only from "../components/common/OnlyWhen";
|
||||
import toastHelper from "../components/Toast";
|
||||
import "../less/auth.less";
|
||||
|
||||
interface Props {}
|
||||
|
||||
const validateConfig: ValidatorConfig = {
|
||||
minLength: 4,
|
||||
maxLength: 24,
|
||||
@@ -18,7 +16,7 @@ const validateConfig: ValidatorConfig = {
|
||||
noChinese: true,
|
||||
};
|
||||
|
||||
const Auth: React.FC<Props> = () => {
|
||||
const Auth = () => {
|
||||
const { t, locale } = useI18n();
|
||||
const pageLoadingState = useLoading(true);
|
||||
const [siteHost, setSiteHost] = useState<User>();
|
||||
@@ -55,13 +53,13 @@ const Auth: React.FC<Props> = () => {
|
||||
|
||||
const emailValidResult = validate(email, validateConfig);
|
||||
if (!emailValidResult.result) {
|
||||
toastHelper.error("Email: " + emailValidResult.reason);
|
||||
toastHelper.error(t("common.email") + ": " + emailValidResult.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordValidResult = validate(password, validateConfig);
|
||||
if (!passwordValidResult.result) {
|
||||
toastHelper.error("Password: " + passwordValidResult.reason);
|
||||
toastHelper.error(t("common.password") + ": " + passwordValidResult.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -72,7 +70,7 @@ const Auth: React.FC<Props> = () => {
|
||||
if (user) {
|
||||
locationService.replaceHistory("/");
|
||||
} else {
|
||||
toastHelper.error("Login failed");
|
||||
toastHelper.error(t("message.login-failed"));
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
@@ -88,13 +86,13 @@ const Auth: React.FC<Props> = () => {
|
||||
|
||||
const emailValidResult = validate(email, validateConfig);
|
||||
if (!emailValidResult.result) {
|
||||
toastHelper.error("Email: " + emailValidResult.reason);
|
||||
toastHelper.error(t("common.email") + ": " + emailValidResult.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordValidResult = validate(password, validateConfig);
|
||||
if (!passwordValidResult.result) {
|
||||
toastHelper.error("Password: " + passwordValidResult.reason);
|
||||
toastHelper.error(t("common.password") + ": " + passwordValidResult.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -105,7 +103,7 @@ const Auth: React.FC<Props> = () => {
|
||||
if (user) {
|
||||
locationService.replaceHistory("/");
|
||||
} else {
|
||||
toastHelper.error("Signup failed");
|
||||
toastHelper.error(t("common.singup-failed"));
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
@@ -164,6 +162,10 @@ const Auth: React.FC<Props> = () => {
|
||||
<span className={`locale-item ${locale === "zh" ? "active" : ""}`} onClick={() => handleLocaleItemClick("zh")}>
|
||||
中文
|
||||
</span>
|
||||
<span className="split-line">/</span>
|
||||
<span className={`locale-item ${locale === "vi" ? "active" : ""}`} onClick={() => handleLocaleItemClick("vi")}>
|
||||
Tiếng Việt
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { locationService, memoService, userService } from "../services";
|
||||
import { useAppSelector } from "../store";
|
||||
import useI18n from "../hooks/useI18n";
|
||||
import useQuery from "../hooks/useQuery";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import Only from "../components/common/OnlyWhen";
|
||||
import MemoContent from "../components/MemoContent";
|
||||
import MemoResources from "../components/MemoResources";
|
||||
import "../less/explore.less";
|
||||
|
||||
interface State {
|
||||
memos: Memo[];
|
||||
}
|
||||
|
||||
const Explore = () => {
|
||||
const { t, locale } = useI18n();
|
||||
const query = useQuery();
|
||||
const user = useAppSelector((state) => state.user.user);
|
||||
const location = useAppSelector((state) => state.location);
|
||||
const [state, setState] = useState<State>({
|
||||
memos: [],
|
||||
});
|
||||
const loadingState = useLoading();
|
||||
|
||||
useEffect(() => {
|
||||
userService
|
||||
.initialState()
|
||||
.catch()
|
||||
.finally(async () => {
|
||||
const { host } = userService.getState();
|
||||
if (!host) {
|
||||
locationService.replaceHistory("/auth");
|
||||
return;
|
||||
}
|
||||
|
||||
memoService.fetchAllMemos().then((memos) => {
|
||||
let filteredMemos = memos;
|
||||
|
||||
const memoId = Number(query.get("memoId"));
|
||||
if (memoId && !isNaN(memoId)) {
|
||||
filteredMemos = filteredMemos.filter((memo) => {
|
||||
return memo.id === memoId;
|
||||
});
|
||||
}
|
||||
|
||||
setState({
|
||||
...state,
|
||||
memos: filteredMemos,
|
||||
});
|
||||
});
|
||||
loadingState.setFinish();
|
||||
});
|
||||
}, [location]);
|
||||
|
||||
return (
|
||||
<section className="page-wrapper explore">
|
||||
<div className="page-container">
|
||||
<div className="page-header">
|
||||
<div className="title-container">
|
||||
<img className="logo-img" src="/logo.webp" alt="" />
|
||||
<span className="title-text">Explore</span>
|
||||
</div>
|
||||
<div className="action-button-container">
|
||||
<Only when={!loadingState.isLoading}>
|
||||
{user ? (
|
||||
<button className="btn" onClick={() => (window.location.href = "/")}>
|
||||
<span className="icon">🏠</span> {t("common.back-to-home")}
|
||||
</button>
|
||||
) : (
|
||||
<button className="btn" onClick={() => (window.location.href = "/auth")}>
|
||||
<span className="icon">👉</span> {t("common.sign-in")}
|
||||
</button>
|
||||
)}
|
||||
</Only>
|
||||
</div>
|
||||
</div>
|
||||
<Only when={!loadingState.isLoading}>
|
||||
<main className="memos-wrapper">
|
||||
{state.memos.length > 0 ? (
|
||||
state.memos.map((memo) => {
|
||||
const createdAtStr = dayjs(memo.createdTs).locale(locale).format("YYYY/MM/DD HH:mm:ss");
|
||||
return (
|
||||
<div className="memo-container" key={memo.id}>
|
||||
<div className="memo-header">
|
||||
<span className="time-text">{createdAtStr}</span>
|
||||
<span className="split-text">by</span>
|
||||
<a className="name-text" href={`/u/${memo.creator.id}`}>
|
||||
{memo.creator.name}
|
||||
</a>
|
||||
</div>
|
||||
<MemoContent className="memo-content" content={memo.content} onMemoContentClick={() => undefined} />
|
||||
<MemoResources memo={memo} />
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="w-full text-center mt-12 text-gray-600">{t("message.no-memos")}</p>
|
||||
)}
|
||||
</main>
|
||||
</Only>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Explore;
|
||||
@@ -31,11 +31,11 @@ function Home() {
|
||||
|
||||
if (userService.isVisitorMode()) {
|
||||
if (!owner) {
|
||||
toastHelper.error("User not found");
|
||||
toastHelper.error(t("message.user-not-found"));
|
||||
}
|
||||
} else {
|
||||
if (!user) {
|
||||
locationService.replaceHistory(`/u/${host.id}`);
|
||||
locationService.replaceHistory(`/explore`);
|
||||
}
|
||||
}
|
||||
loadingState.setFinish();
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import Home from "../pages/Home";
|
||||
import Auth from "../pages/Auth";
|
||||
import Explore from "../pages/Explore";
|
||||
|
||||
const appRouter = {
|
||||
"/auth": <Auth />,
|
||||
"/explore": <Explore />,
|
||||
"*": <Home />,
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,16 @@ const memoService = {
|
||||
},
|
||||
|
||||
fetchAllMemos: async () => {
|
||||
const memoFind: MemoFind = {};
|
||||
if (userService.isVisitorMode()) {
|
||||
memoFind.creatorId = userService.getUserIdFromPath();
|
||||
}
|
||||
const { data } = (await api.getAllMemos()).data;
|
||||
const memos = data.map((m) => convertResponseModelMemo(m));
|
||||
return memos;
|
||||
},
|
||||
|
||||
fetchMemos: async () => {
|
||||
const timeoutIndex = setTimeout(() => {
|
||||
store.dispatch(setIsFetching(true));
|
||||
}, 1000);
|
||||
|
||||
@@ -21,7 +21,7 @@ interface State {
|
||||
|
||||
const getValidPathname = (pathname: string): string => {
|
||||
const userPageUrlRegex = /^\/u\/\d+.*/;
|
||||
if (["/", "/auth"].includes(pathname) || userPageUrlRegex.test(pathname)) {
|
||||
if (["/", "/auth", "/explore"].includes(pathname) || userPageUrlRegex.test(pathname)) {
|
||||
return pathname;
|
||||
} else {
|
||||
return "/";
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
type Locale = "en" | "zh";
|
||||
type Locale = "en" | "zh" | "vi";
|
||||
|
||||
Vendored
+1
@@ -6,6 +6,7 @@ interface Memo {
|
||||
id: MemoId;
|
||||
|
||||
creatorId: UserId;
|
||||
creator: User;
|
||||
createdTs: TimeStamp;
|
||||
updatedTs: TimeStamp;
|
||||
rowStatus: RowStatus;
|
||||
|
||||
+5
-1
@@ -6,13 +6,17 @@ import { resolve } from "path";
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
cors: true,
|
||||
host: "0.0.0.0",
|
||||
port: 3000,
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:8080/",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/o/": {
|
||||
target: "http://localhost:8080/",
|
||||
changeOrigin: true,
|
||||
},
|
||||
"/h/": {
|
||||
target: "http://localhost:8080/",
|
||||
changeOrigin: true,
|
||||
|
||||
@@ -341,6 +341,11 @@
|
||||
redux-thunk "^2.4.1"
|
||||
reselect "^4.1.5"
|
||||
|
||||
"@remix-run/router@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.0.0.tgz#a2189335a5f6428aa904ccc291988567018b6e01"
|
||||
integrity sha512-SCR1cxRSMNKjaVYptCzBApPDqGwa3FGdjVHc+rOToocNPHQdIYLZBfv/3f+KvYuXDkUGVIW9IAzmPNZDRL1I4A==
|
||||
|
||||
"@types/hoist-non-react-statics@^3.3.1":
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
|
||||
@@ -767,6 +772,13 @@ copy-anything@^2.0.1:
|
||||
dependencies:
|
||||
is-what "^3.14.1"
|
||||
|
||||
copy-to-clipboard@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.2.tgz#5b263ec2366224b100181dded7ce0579b340c107"
|
||||
integrity sha512-Vme1Z6RUDzrb6xAI7EZlVZ5uvOk2F//GaxKUxajDqm9LhOVM1inxNAD2vy+UZDYsd0uyA9s7b3/FVZPSxqrCfg==
|
||||
dependencies:
|
||||
toggle-selection "^1.0.6"
|
||||
|
||||
cross-spawn@^7.0.2:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||
@@ -873,6 +885,11 @@ electron-to-chromium@^1.4.172:
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.185.tgz#3432d7944f1c5fe20664bb45d9cced2151405ce2"
|
||||
integrity sha512-9kV/isoOGpKkBt04yYNaSWIBn3187Q5VZRtoReq8oz5NY/A4XmU6cAoqgQlDp7kKJCZMRjWZ8nsQyxfpFHvfyw==
|
||||
|
||||
emoji-picker-react@^3.6.2:
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-3.6.2.tgz#e414971bf9421b0825484f3b82623fef995c3c4b"
|
||||
integrity sha512-PK3dfljGxeyN8fDz2FAsDYKPYGgo6/tkRyzJjLVaw0fksJg7jA3OJPIlHq2IIzTmlC/NKhcI/oaf0uEo5azYGA==
|
||||
|
||||
errno@^0.1.1:
|
||||
version "0.1.8"
|
||||
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f"
|
||||
@@ -2106,6 +2123,20 @@ react-refresh@^0.14.0:
|
||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e"
|
||||
integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==
|
||||
|
||||
react-router-dom@^6.4.0:
|
||||
version "6.4.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.4.0.tgz#a7d7c394c9e730b045cdd4f5d6c2d1ccb9e26947"
|
||||
integrity sha512-4Aw1xmXKeleYYQ3x0Lcl2undHR6yMjXZjd9DKZd53SGOYqirrUThyUb0wwAX5VZAyvSuzjNJmZlJ3rR9+/vzqg==
|
||||
dependencies:
|
||||
react-router "6.4.0"
|
||||
|
||||
react-router@6.4.0:
|
||||
version "6.4.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.4.0.tgz#68449c23dc893fc7a57db068c19987be1de72edb"
|
||||
integrity sha512-B+5bEXFlgR1XUdHYR6P94g299SjrfCBMmEDJNcFbpAyRH1j1748yt9NdDhW3++nw1lk3zQJ6aOO66zUx3KlTZg==
|
||||
dependencies:
|
||||
"@remix-run/router" "1.0.0"
|
||||
|
||||
react@^18.1.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||
@@ -2400,6 +2431,11 @@ to-regex-range@^5.0.1:
|
||||
dependencies:
|
||||
is-number "^7.0.0"
|
||||
|
||||
toggle-selection@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
|
||||
integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==
|
||||
|
||||
tslib@^1.8.1:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
|
||||
Reference in New Issue
Block a user