mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd6e2337e6 | ||
|
|
66418d4210 | ||
|
|
ab8c7b9d8a | ||
|
|
387799b31c | ||
|
|
4a64a4dea8 | ||
|
|
964c58ac01 | ||
|
|
56716cdad4 | ||
|
|
a2ee750d1e | ||
|
|
3f0601f651 | ||
|
|
6f8e3432e9 | ||
|
|
b7ab6f8e7e | ||
|
|
36b92ad884 | ||
|
|
4d9857ce18 | ||
|
|
43b22ce55f | ||
|
|
147185309c | ||
|
|
f48226d4f2 | ||
|
|
e92407d9ec | ||
|
|
79bf365d78 | ||
|
|
492a1370ab | ||
|
|
e3ddf93c4d | ||
|
|
4a9314c476 | ||
|
|
4767ee3293 | ||
|
|
1ea74dfd0d | ||
|
|
53cf6ebb79 | ||
|
|
d1007950e0 | ||
|
|
331226ec68 | ||
|
|
a7374cf998 | ||
|
|
e3d76193b9 | ||
|
|
07f0c3f052 | ||
|
|
a467a7c173 | ||
|
|
14f9f29348 | ||
|
|
5451fd2d2c | ||
|
|
f092771ea1 | ||
|
|
7c6d7226f5 | ||
|
|
eaebc6dcef | ||
|
|
c5200ca31b | ||
|
|
1078132b12 | ||
|
|
6b058cd299 | ||
|
|
b8f24af5ae | ||
|
|
6384f5af74 | ||
|
|
52038d26d2 | ||
|
|
55f37664ef | ||
|
|
ab8e3473a1 | ||
|
|
eba23c4f6e | ||
|
|
00fe6d3862 | ||
|
|
3646b8f5dd | ||
|
|
d40639bf8e | ||
|
|
12b81781b9 | ||
|
|
b67a37453d | ||
|
|
b04e001db1 | ||
|
|
fbe7b604ef | ||
|
|
0402cb7b27 | ||
|
|
b72bfc9c24 | ||
|
|
40e92f9463 | ||
|
|
f883dd9c1d | ||
|
|
d8bf55efb2 | ||
|
|
f982e83d0a | ||
|
|
3472a6db26 | ||
|
|
c79e51a91b | ||
|
|
ce795a2a7d | ||
|
|
045819c312 | ||
|
|
2fa01886da | ||
|
|
dd7d322c47 | ||
|
|
dfe71f33c2 | ||
|
|
db1d223448 | ||
|
|
54271c1598 | ||
|
|
1ee8ebc9e1 | ||
|
|
6e5537d131 | ||
|
|
90c85103c3 | ||
|
|
2d5d734da4 | ||
|
|
e1e5121dd7 | ||
|
|
85db6721de |
97
.github/workflows/uffizzi-build.yml
vendored
Normal file
97
.github/workflows/uffizzi-build.yml
vendored
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
name: Build PR Image
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, closed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-memos:
|
||||||
|
name: Build and push `Memos`
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
if: ${{ github.event.action != 'closed' }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout git repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Generate UUID image name
|
||||||
|
id: uuid
|
||||||
|
run: echo "UUID_WORKER=$(uuidgen)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: registry.uffizzi.com/${{ env.UUID_WORKER }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=60d
|
||||||
|
|
||||||
|
- name: Build and Push Image to registry.uffizzi.com - Uffizzi's ephemeral Registry
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: ./
|
||||||
|
file: Dockerfile
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
push: true
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha, mode=max
|
||||||
|
|
||||||
|
render-compose-file:
|
||||||
|
name: Render Docker Compose File
|
||||||
|
# Pass output of this workflow to another triggered by `workflow_run` event.
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- build-memos
|
||||||
|
outputs:
|
||||||
|
compose-file-cache-key: ${{ steps.hash.outputs.hash }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout git repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Render Compose File
|
||||||
|
run: |
|
||||||
|
MEMOS_IMAGE=${{ needs.build-memos.outputs.tags }}
|
||||||
|
export MEMOS_IMAGE
|
||||||
|
# Render simple template from environment variables.
|
||||||
|
envsubst < docker-compose.uffizzi.yml > docker-compose.rendered.yml
|
||||||
|
cat docker-compose.rendered.yml
|
||||||
|
- name: Upload Rendered Compose File as Artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: preview-spec
|
||||||
|
path: docker-compose.rendered.yml
|
||||||
|
retention-days: 2
|
||||||
|
- name: Serialize PR Event to File
|
||||||
|
run: |
|
||||||
|
cat << EOF > event.json
|
||||||
|
${{ toJSON(github.event) }}
|
||||||
|
|
||||||
|
EOF
|
||||||
|
- name: Upload PR Event as Artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: preview-spec
|
||||||
|
path: event.json
|
||||||
|
retention-days: 2
|
||||||
|
|
||||||
|
delete-preview:
|
||||||
|
name: Call for Preview Deletion
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.action == 'closed' }}
|
||||||
|
steps:
|
||||||
|
# If this PR is closing, we will not render a compose file nor pass it to the next workflow.
|
||||||
|
- name: Serialize PR Event to File
|
||||||
|
run: |
|
||||||
|
cat << EOF > event.json
|
||||||
|
${{ toJSON(github.event) }}
|
||||||
|
|
||||||
|
EOF
|
||||||
|
- name: Upload PR Event as Artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: preview-spec
|
||||||
|
path: event.json
|
||||||
|
retention-days: 2
|
||||||
86
.github/workflows/uffizzi-preview.yml
vendored
Normal file
86
.github/workflows/uffizzi-preview.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
name: Deploy Uffizzi Preview
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows:
|
||||||
|
- "Build PR Image"
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cache-compose-file:
|
||||||
|
name: Cache Compose File
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
compose-file-cache-key: ${{ env.HASH }}
|
||||||
|
pr-number: ${{ env.PR_NUMBER }}
|
||||||
|
steps:
|
||||||
|
- name: 'Download artifacts'
|
||||||
|
# Fetch output (zip archive) from the workflow run that triggered this workflow.
|
||||||
|
uses: actions/github-script@v6
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
run_id: context.payload.workflow_run.id,
|
||||||
|
});
|
||||||
|
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
|
||||||
|
return artifact.name == "preview-spec"
|
||||||
|
})[0];
|
||||||
|
let download = await github.rest.actions.downloadArtifact({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
artifact_id: matchArtifact.id,
|
||||||
|
archive_format: 'zip',
|
||||||
|
});
|
||||||
|
let fs = require('fs');
|
||||||
|
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data));
|
||||||
|
|
||||||
|
- name: 'Unzip artifact'
|
||||||
|
run: unzip preview-spec.zip
|
||||||
|
- name: Read Event into ENV
|
||||||
|
run: |
|
||||||
|
echo 'EVENT_JSON<<EOF' >> $GITHUB_ENV
|
||||||
|
cat event.json >> $GITHUB_ENV
|
||||||
|
echo 'EOF' >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Hash Rendered Compose File
|
||||||
|
id: hash
|
||||||
|
# If the previous workflow was triggered by a PR close event, we will not have a compose file artifact.
|
||||||
|
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
|
||||||
|
run: echo "HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV
|
||||||
|
- name: Cache Rendered Compose File
|
||||||
|
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: docker-compose.rendered.yml
|
||||||
|
key: ${{ env.HASH }}
|
||||||
|
|
||||||
|
- name: Read PR Number From Event Object
|
||||||
|
id: pr
|
||||||
|
run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV
|
||||||
|
- name: DEBUG - Print Job Outputs
|
||||||
|
if: ${{ runner.debug }}
|
||||||
|
run: |
|
||||||
|
echo "PR number: ${{ env.PR_NUMBER }}"
|
||||||
|
echo "Compose file hash: ${{ env.HASH }}"
|
||||||
|
cat event.json
|
||||||
|
|
||||||
|
deploy-uffizzi-preview:
|
||||||
|
name: Use Remote Workflow to Preview on Uffizzi
|
||||||
|
needs:
|
||||||
|
- cache-compose-file
|
||||||
|
uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2
|
||||||
|
with:
|
||||||
|
# If this workflow was triggered by a PR close event, cache-key will be an empty string
|
||||||
|
# and this reusable workflow will delete the preview deployment.
|
||||||
|
compose-file-cache-key: ${{ needs.cache-compose-file.outputs.compose-file-cache-key }}
|
||||||
|
compose-file-cache-path: docker-compose.rendered.yml
|
||||||
|
server: https://app.uffizzi.com
|
||||||
|
pr-number: ${{ needs.cache-compose-file.outputs.pr-number }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
id-token: write
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -15,4 +15,7 @@ build
|
|||||||
# Jetbrains
|
# Jetbrains
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
# vscode
|
||||||
|
.vscode
|
||||||
|
|
||||||
bin/air
|
bin/air
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -1,6 +1,6 @@
|
|||||||
<p align="center"><a href="https://usememos.com"><img height="64px" src="https://raw.githubusercontent.com/usememos/memos/main/resources/logo-full.webp" alt="✍️ memos" /></a></p>
|
<p align="center"><a href="https://usememos.com"><img height="64px" src="https://raw.githubusercontent.com/usememos/memos/main/resources/logo-full.webp" alt="✍️ memos" /></a></p>
|
||||||
|
|
||||||
<p align="center">An open-source, self-hosted memo hub with knowledge management and collaboration.</p>
|
<p align="center">An open-source, self-hosted memo hub with knowledge management and socialization.</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/usememos/memos/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/usememos/memos" /></a>
|
<a href="https://github.com/usememos/memos/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/usememos/memos" /></a>
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
|
|
||||||
- 🦄 Open source and free forever;
|
- 🦄 Open source and free forever;
|
||||||
- 🚀 Support for self-hosting with `Docker` in seconds;
|
- 🚀 Support for self-hosting with `Docker` in seconds;
|
||||||
- 📜 Plain textarea first and support some useful markdown syntax;
|
- 📜 Plain textarea first and support some useful Markdown syntax;
|
||||||
- 👥 Collaborate and share with your teammates;
|
- 👥 Set memo private or public to others;
|
||||||
- 🧑💻 RESTful API for self-service.
|
- 🧑💻 RESTful API for self-service.
|
||||||
|
|
||||||
## Deploy with Docker in seconds
|
## Deploy with Docker in seconds
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
docker run -d --name memos -p 5230:5230 -v ~/.memos/:/var/opt/memos neosmemo/memos:latest
|
docker run -d --name memos -p 5230:5230 -v ~/.memos/:/var/opt/memos neosmemo/memos:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
If the `~/.memos/` does not have a `memos_prod.db` file, then memos will auto generate it. Memos will be running at [http://localhost:5230](http://localhost:5230).
|
||||||
|
|
||||||
### Docker Compose
|
### Docker Compose
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ docker-compose down && docker image rm neosmemo/memos:latest && docker-compose u
|
|||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
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. 🥰
|
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated. 🥰
|
||||||
|
|
||||||
See more in [development guide](https://github.com/usememos/memos/tree/main/docs/development.md).
|
See more in [development guide](https://github.com/usememos/memos/tree/main/docs/development.md).
|
||||||
|
|
||||||
@@ -53,8 +53,16 @@ See more in [development guide](https://github.com/usememos/memos/tree/main/docs
|
|||||||
|
|
||||||
- [Moe Memos](https://memos.moe/) - Third party client for iOS and Android
|
- [Moe Memos](https://memos.moe/) - Third party client for iOS and Android
|
||||||
- [lmm214/memos-bber](https://github.com/lmm214/memos-bber) - Chrome extension
|
- [lmm214/memos-bber](https://github.com/lmm214/memos-bber) - Chrome extension
|
||||||
- [Rabithua/memos_wmp](https://github.com/Rabithua/memos_wmp) - Wechat miniprogram
|
- [Rabithua/memos_wmp](https://github.com/Rabithua/memos_wmp) - WeChat MiniProgram
|
||||||
- [qazxcdswe123/telegramMemoBot](https://github.com/qazxcdswe123/telegramMemoBot) - Telegram bot
|
- [qazxcdswe123/telegramMemoBot](https://github.com/qazxcdswe123/telegramMemoBot) - Telegram bot
|
||||||
|
- [eallion/memos.top](https://github.com/eallion/memos.top) - A static page rendered with the Memos API
|
||||||
|
- [eindex/logseq-memos-sync](https://github.com/EINDEX/logseq-memos-sync) - A Logseq plugin
|
||||||
|
|
||||||
|
### Join the community to build memos together!
|
||||||
|
|
||||||
|
<a href="https://github.com/usememos/memos/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=usememos/memos" />
|
||||||
|
</a>
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ const (
|
|||||||
Public Visibility = "PUBLIC"
|
Public Visibility = "PUBLIC"
|
||||||
// Protected is the PROTECTED visibility.
|
// Protected is the PROTECTED visibility.
|
||||||
Protected Visibility = "PROTECTED"
|
Protected Visibility = "PROTECTED"
|
||||||
// Privite is the PRIVATE visibility.
|
// Private is the PRIVATE visibility.
|
||||||
Privite Visibility = "PRIVATE"
|
Private Visibility = "PRIVATE"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e Visibility) String() string {
|
func (e Visibility) String() string {
|
||||||
@@ -18,7 +18,7 @@ func (e Visibility) String() string {
|
|||||||
return "PUBLIC"
|
return "PUBLIC"
|
||||||
case Protected:
|
case Protected:
|
||||||
return "PROTECTED"
|
return "PROTECTED"
|
||||||
case Privite:
|
case Private:
|
||||||
return "PRIVATE"
|
return "PRIVATE"
|
||||||
}
|
}
|
||||||
return "PRIVATE"
|
return "PRIVATE"
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ type UserSettingKey string
|
|||||||
const (
|
const (
|
||||||
// UserSettingLocaleKey is the key type for user locale.
|
// UserSettingLocaleKey is the key type for user locale.
|
||||||
UserSettingLocaleKey UserSettingKey = "locale"
|
UserSettingLocaleKey UserSettingKey = "locale"
|
||||||
|
// UserSettingAppearanceKey is the key type for user appearance.
|
||||||
|
UserSettingAppearanceKey UserSettingKey = "appearance"
|
||||||
// UserSettingMemoVisibilityKey is the key type for user preference memo default visibility.
|
// UserSettingMemoVisibilityKey is the key type for user preference memo default visibility.
|
||||||
UserSettingMemoVisibilityKey UserSettingKey = "memoVisibility"
|
UserSettingMemoVisibilityKey UserSettingKey = "memoVisibility"
|
||||||
// UserSettingMemoDisplayTsOptionKey is the key type for memo display ts option.
|
// UserSettingMemoDisplayTsOptionKey is the key type for memo display ts option.
|
||||||
@@ -21,6 +23,8 @@ func (key UserSettingKey) String() string {
|
|||||||
switch key {
|
switch key {
|
||||||
case UserSettingLocaleKey:
|
case UserSettingLocaleKey:
|
||||||
return "locale"
|
return "locale"
|
||||||
|
case UserSettingAppearanceKey:
|
||||||
|
return "appearance"
|
||||||
case UserSettingMemoVisibilityKey:
|
case UserSettingMemoVisibilityKey:
|
||||||
return "memoVisibility"
|
return "memoVisibility"
|
||||||
case UserSettingMemoDisplayTsOptionKey:
|
case UserSettingMemoDisplayTsOptionKey:
|
||||||
@@ -30,9 +34,9 @@ func (key UserSettingKey) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
UserSettingLocaleValue = []string{"en", "zh", "vi", "fr"}
|
UserSettingLocaleValue = []string{"en", "zh", "vi", "fr", "nl", "sv", "de"}
|
||||||
UserSettingMemoVisibilityValue = []Visibility{Privite, Protected, Public}
|
UserSettingAppearanceValue = []string{"system", "light", "dark"}
|
||||||
UserSettingEditorFontStyleValue = []string{"normal", "mono"}
|
UserSettingMemoVisibilityValue = []Visibility{Private, Protected, Public}
|
||||||
UserSettingMemoDisplayTsOptionKeyValue = []string{"created_ts", "updated_ts"}
|
UserSettingMemoDisplayTsOptionKeyValue = []string{"created_ts", "updated_ts"}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -67,8 +71,25 @@ func (upsert UserSettingUpsert) Validate() error {
|
|||||||
if invalid {
|
if invalid {
|
||||||
return fmt.Errorf("invalid user setting locale value")
|
return fmt.Errorf("invalid user setting locale value")
|
||||||
}
|
}
|
||||||
|
} else if upsert.Key == UserSettingAppearanceKey {
|
||||||
|
appearanceValue := "light"
|
||||||
|
err := json.Unmarshal([]byte(upsert.Value), &appearanceValue)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal user setting appearance value")
|
||||||
|
}
|
||||||
|
|
||||||
|
invalid := true
|
||||||
|
for _, value := range UserSettingAppearanceValue {
|
||||||
|
if appearanceValue == value {
|
||||||
|
invalid = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if invalid {
|
||||||
|
return fmt.Errorf("invalid user setting appearance value")
|
||||||
|
}
|
||||||
} else if upsert.Key == UserSettingMemoVisibilityKey {
|
} else if upsert.Key == UserSettingMemoVisibilityKey {
|
||||||
memoVisibilityValue := Privite
|
memoVisibilityValue := Private
|
||||||
err := json.Unmarshal([]byte(upsert.Value), &memoVisibilityValue)
|
err := json.Unmarshal([]byte(upsert.Value), &memoVisibilityValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to unmarshal user setting memo visibility value")
|
return fmt.Errorf("failed to unmarshal user setting memo visibility value")
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ func run(profile *profile.Profile) error {
|
|||||||
serverInstance.Store = storeInstance
|
serverInstance.Store = storeInstance
|
||||||
|
|
||||||
metricCollector := server.NewMetricCollector(profile, storeInstance)
|
metricCollector := server.NewMetricCollector(profile, storeInstance)
|
||||||
|
// Disable metrics collector.
|
||||||
|
metricCollector.Enabled = false
|
||||||
serverInstance.Collector = &metricCollector
|
serverInstance.Collector = &metricCollector
|
||||||
|
|
||||||
println(greetingBanner)
|
println(greetingBanner)
|
||||||
|
|||||||
18
docker-compose.uffizzi.yml
Normal file
18
docker-compose.uffizzi.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
version: "3.0"
|
||||||
|
|
||||||
|
# uffizzi integration
|
||||||
|
x-uffizzi:
|
||||||
|
ingress:
|
||||||
|
service: memos
|
||||||
|
port: 5230
|
||||||
|
|
||||||
|
services:
|
||||||
|
memos:
|
||||||
|
image: "${MEMOS_IMAGE}"
|
||||||
|
volumes:
|
||||||
|
- memos_volume:/var/opt/memos
|
||||||
|
command: ["--mode", "dev"]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
memos_volume:
|
||||||
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
@@ -45,7 +45,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if userMemoVisibilitySetting != nil {
|
if userMemoVisibilitySetting != nil {
|
||||||
memoVisibility := api.Privite
|
memoVisibility := api.Private
|
||||||
err := json.Unmarshal([]byte(userMemoVisibilitySetting.Value), &memoVisibility)
|
err := json.Unmarshal([]byte(userMemoVisibilitySetting.Value), &memoVisibility)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal user setting value").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal user setting value").SetInternal(err)
|
||||||
@@ -53,7 +53,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
|||||||
memoCreate.Visibility = memoVisibility
|
memoCreate.Visibility = memoVisibility
|
||||||
} else {
|
} else {
|
||||||
// Private is the default memo visibility.
|
// Private is the default memo visibility.
|
||||||
memoCreate.Visibility = api.Privite
|
memoCreate.Visibility = api.Private
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,10 +176,10 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
|||||||
contentSearch := "#" + tag + " "
|
contentSearch := "#" + tag + " "
|
||||||
memoFind.ContentSearch = &contentSearch
|
memoFind.ContentSearch = &contentSearch
|
||||||
}
|
}
|
||||||
visibilitListStr := c.QueryParam("visibility")
|
visibilityListStr := c.QueryParam("visibility")
|
||||||
if visibilitListStr != "" {
|
if visibilityListStr != "" {
|
||||||
visibilityList := []api.Visibility{}
|
visibilityList := []api.Visibility{}
|
||||||
for _, visibility := range strings.Split(visibilitListStr, ",") {
|
for _, visibility := range strings.Split(visibilityListStr, ",") {
|
||||||
visibilityList = append(visibilityList, api.Visibility(visibility))
|
visibilityList = append(visibilityList, api.Visibility(visibility))
|
||||||
}
|
}
|
||||||
memoFind.VisibilityList = visibilityList
|
memoFind.VisibilityList = visibilityList
|
||||||
@@ -271,7 +271,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
|||||||
if *memoFind.CreatorID != currentUserID {
|
if *memoFind.CreatorID != currentUserID {
|
||||||
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
|
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected}
|
||||||
} else {
|
} else {
|
||||||
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected, api.Privite}
|
memoFind.VisibilityList = []api.Visibility{api.Public, api.Protected, api.Private}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,10 +313,10 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
|||||||
contentSearch := "#" + tag + " "
|
contentSearch := "#" + tag + " "
|
||||||
memoFind.ContentSearch = &contentSearch
|
memoFind.ContentSearch = &contentSearch
|
||||||
}
|
}
|
||||||
visibilitListStr := c.QueryParam("visibility")
|
visibilityListStr := c.QueryParam("visibility")
|
||||||
if visibilitListStr != "" {
|
if visibilityListStr != "" {
|
||||||
visibilityList := []api.Visibility{}
|
visibilityList := []api.Visibility{}
|
||||||
for _, visibility := range strings.Split(visibilitListStr, ",") {
|
for _, visibility := range strings.Split(visibilityListStr, ",") {
|
||||||
visibilityList = append(visibilityList, api.Visibility(visibility))
|
visibilityList = append(visibilityList, api.Visibility(visibility))
|
||||||
}
|
}
|
||||||
memoFind.VisibilityList = visibilityList
|
memoFind.VisibilityList = visibilityList
|
||||||
@@ -372,7 +372,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||||
if memo.Visibility == api.Privite {
|
if memo.Visibility == api.Private {
|
||||||
if !ok || memo.CreatorID != userID {
|
if !ok || memo.CreatorID != userID {
|
||||||
return echo.NewHTTPError(http.StatusForbidden, "this memo is private only")
|
return echo.NewHTTPError(http.StatusForbidden, "this memo is private only")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ import (
|
|||||||
|
|
||||||
// MetricCollector is the metric collector.
|
// MetricCollector is the metric collector.
|
||||||
type MetricCollector struct {
|
type MetricCollector struct {
|
||||||
collector metric.Collector
|
Collector metric.Collector
|
||||||
profile *profile.Profile
|
Enabled bool
|
||||||
store *store.Store
|
Profile *profile.Profile
|
||||||
|
Store *store.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -26,23 +27,28 @@ func NewMetricCollector(profile *profile.Profile, store *store.Store) MetricColl
|
|||||||
c := segment.NewCollector(segmentMetricWriteKey)
|
c := segment.NewCollector(segmentMetricWriteKey)
|
||||||
|
|
||||||
return MetricCollector{
|
return MetricCollector{
|
||||||
collector: c,
|
Collector: c,
|
||||||
profile: profile,
|
Enabled: true,
|
||||||
store: store,
|
Profile: profile,
|
||||||
|
Store: store,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mc *MetricCollector) Collect(_ context.Context, metric *metric.Metric) {
|
func (mc *MetricCollector) Collect(_ context.Context, metric *metric.Metric) {
|
||||||
if mc.profile.Mode == "dev" {
|
if !mc.Enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if mc.Profile.Mode == "dev" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if metric.Labels == nil {
|
if metric.Labels == nil {
|
||||||
metric.Labels = map[string]string{}
|
metric.Labels = map[string]string{}
|
||||||
}
|
}
|
||||||
metric.Labels["version"] = version.GetCurrentVersion(mc.profile.Mode)
|
metric.Labels["version"] = version.GetCurrentVersion(mc.Profile.Mode)
|
||||||
|
|
||||||
err := mc.collector.Collect(metric)
|
err := mc.Collector.Collect(metric)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to request segment, error: %+v\n", err)
|
fmt.Printf("Failed to request segment, error: %+v\n", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,13 +93,13 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, resource := range list {
|
for _, resource := range list {
|
||||||
memoResoureceList, err := s.Store.FindMemoResourceList(ctx, &api.MemoResourceFind{
|
memoResourceList, err := s.Store.FindMemoResourceList(ctx, &api.MemoResourceFind{
|
||||||
ResourceID: &resource.ID,
|
ResourceID: &resource.ID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo resource list").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo resource list").SetInternal(err)
|
||||||
}
|
}
|
||||||
resource.LinkedMemoAmount = len(memoResoureceList)
|
resource.LinkedMemoAmount = len(memoResourceList)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||||
|
// Get database size for host user.
|
||||||
if ok {
|
if ok {
|
||||||
user, err := s.Store.FindUser(ctx, &api.UserFind{
|
user, err := s.Store.FindUser(ctx, &api.UserFind{
|
||||||
ID: &userID,
|
ID: &userID,
|
||||||
@@ -148,4 +149,26 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
g.POST("/system/vacuum", func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||||
|
if !ok {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||||
|
}
|
||||||
|
user, err := s.Store.FindUser(ctx, &api.UserFind{
|
||||||
|
ID: &userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
|
||||||
|
}
|
||||||
|
if user == nil || user.Role != api.Host {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||||
|
}
|
||||||
|
if err := s.Store.Vacuum(ctx); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to vacuum database").SetInternal(err)
|
||||||
|
}
|
||||||
|
c.Response().WriteHeader(http.StatusOK)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,6 +223,14 @@ func (s *Server) registerUserRoutes(g *echo.Group) {
|
|||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch user").SetInternal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userSettingList, err := s.Store.FindUserSettingList(ctx, &api.UserSettingFind{
|
||||||
|
UserID: userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find userSettingList").SetInternal(err)
|
||||||
|
}
|
||||||
|
user.UserSettingList = userSettingList
|
||||||
|
|
||||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||||
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
|
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user)); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err)
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import (
|
|||||||
|
|
||||||
// Version is the service current released version.
|
// Version is the service current released version.
|
||||||
// Semantic versioning: https://semver.org/
|
// Semantic versioning: https://semver.org/
|
||||||
var Version = "0.8.0"
|
var Version = "0.8.2"
|
||||||
|
|
||||||
// DevVersion is the service current development version.
|
// DevVersion is the service current development version.
|
||||||
var DevVersion = "0.8.0"
|
var DevVersion = "0.8.2"
|
||||||
|
|
||||||
func GetCurrentVersion(mode string) string {
|
func GetCurrentVersion(mode string) string {
|
||||||
if mode == "dev" {
|
if mode == "dev" {
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ func (db *DB) Open(ctx context.Context) (err error) {
|
|||||||
}
|
}
|
||||||
db.Db = sqlDB
|
db.Db = sqlDB
|
||||||
|
|
||||||
// If mode is dev, we should migrate and seed the database.
|
|
||||||
if db.profile.Mode == "dev" {
|
if db.profile.Mode == "dev" {
|
||||||
|
// In dev mode, we should migrate and seed the database.
|
||||||
if _, err := os.Stat(db.profile.DSN); errors.Is(err, os.ErrNotExist) {
|
if _, err := os.Stat(db.profile.DSN); errors.Is(err, os.ErrNotExist) {
|
||||||
if err := db.applyLatestSchema(ctx); err != nil {
|
if err := db.applyLatestSchema(ctx); err != nil {
|
||||||
return fmt.Errorf("failed to apply latest schema: %w", err)
|
return fmt.Errorf("failed to apply latest schema: %w", err)
|
||||||
@@ -65,51 +65,51 @@ func (db *DB) Open(ctx context.Context) (err error) {
|
|||||||
if err := db.applyLatestSchema(ctx); err != nil {
|
if err := db.applyLatestSchema(ctx); err != nil {
|
||||||
return fmt.Errorf("failed to apply latest schema: %w", err)
|
return fmt.Errorf("failed to apply latest schema: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
currentVersion := version.GetCurrentVersion(db.profile.Mode)
|
|
||||||
migrationHistory, err := db.FindMigrationHistory(ctx, &MigrationHistoryFind{})
|
currentVersion := version.GetCurrentVersion(db.profile.Mode)
|
||||||
|
migrationHistory, err := db.FindMigrationHistory(ctx, &MigrationHistoryFind{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to find migration history, err: %w", err)
|
||||||
|
}
|
||||||
|
if migrationHistory == nil {
|
||||||
|
if _, err = db.UpsertMigrationHistory(ctx, &MigrationHistoryUpsert{
|
||||||
|
Version: currentVersion,
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("failed to upsert migration history, err: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if version.IsVersionGreaterThan(version.GetSchemaVersion(currentVersion), migrationHistory.Version) {
|
||||||
|
minorVersionList := getMinorVersionList()
|
||||||
|
|
||||||
|
// backup the raw database file before migration
|
||||||
|
rawBytes, err := os.ReadFile(db.profile.DSN)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to read raw database file, err: %w", err)
|
||||||
}
|
}
|
||||||
if migrationHistory == nil {
|
backupDBFilePath := fmt.Sprintf("%s/memos_%s_%d_backup.db", db.profile.Data, db.profile.Version, time.Now().Unix())
|
||||||
migrationHistory, err = db.UpsertMigrationHistory(ctx, &MigrationHistoryUpsert{
|
if err := os.WriteFile(backupDBFilePath, rawBytes, 0644); err != nil {
|
||||||
Version: currentVersion,
|
return fmt.Errorf("failed to write raw database file, err: %w", err)
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
println("succeed to copy a backup database file")
|
||||||
|
|
||||||
if version.IsVersionGreaterThan(version.GetSchemaVersion(currentVersion), migrationHistory.Version) {
|
println("start migrate")
|
||||||
minorVersionList := getMinorVersionList()
|
for _, minorVersion := range minorVersionList {
|
||||||
|
normalizedVersion := minorVersion + ".0"
|
||||||
// backup the raw database file before migration
|
if version.IsVersionGreaterThan(normalizedVersion, migrationHistory.Version) && version.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
|
||||||
rawBytes, err := os.ReadFile(db.profile.DSN)
|
println("applying migration for", normalizedVersion)
|
||||||
if err != nil {
|
if err := db.applyMigrationForMinorVersion(ctx, minorVersion); err != nil {
|
||||||
return fmt.Errorf("failed to read raw database file, err: %w", err)
|
return fmt.Errorf("failed to apply minor version migration: %w", err)
|
||||||
}
|
|
||||||
backupDBFilePath := fmt.Sprintf("%s/memos_%s_%d_backup.db", db.profile.Data, db.profile.Version, time.Now().Unix())
|
|
||||||
if err := os.WriteFile(backupDBFilePath, rawBytes, 0644); err != nil {
|
|
||||||
return fmt.Errorf("failed to write raw database file, err: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
println("succeed to copy a backup database file")
|
|
||||||
println("start migrate")
|
|
||||||
for _, minorVersion := range minorVersionList {
|
|
||||||
normalizedVersion := minorVersion + ".0"
|
|
||||||
if 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
println("end migrate")
|
||||||
|
|
||||||
println("end migrate")
|
// remove the created backup db file after migrate succeed
|
||||||
// remove the created backup db file after migrate succeed
|
if err := os.Remove(backupDBFilePath); err != nil {
|
||||||
if err := os.Remove(backupDBFilePath); err != nil {
|
println(fmt.Sprintf("Failed to remove temp database file, err %v", err))
|
||||||
println(fmt.Sprintf("Failed to remove temp database file, err %v", err))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,7 +137,7 @@ func (db *DB) applyLatestSchema(ctx context.Context) error {
|
|||||||
func (db *DB) applyMigrationForMinorVersion(ctx context.Context, minorVersion string) error {
|
func (db *DB) applyMigrationForMinorVersion(ctx context.Context, minorVersion string) error {
|
||||||
filenames, err := fs.Glob(migrationFS, fmt.Sprintf("%s/%s/*.sql", "migration/prod", minorVersion))
|
filenames, err := fs.Glob(migrationFS, fmt.Sprintf("%s/%s/*.sql", "migration/prod", minorVersion))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to read ddl files, err: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Strings(filenames)
|
sort.Strings(filenames)
|
||||||
@@ -163,10 +163,11 @@ func (db *DB) applyMigrationForMinorVersion(ctx context.Context, minorVersion st
|
|||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
// upsert the newest version to migration_history
|
// upsert the newest version to migration_history
|
||||||
|
version := minorVersion + ".0"
|
||||||
if _, err = upsertMigrationHistory(ctx, tx, &MigrationHistoryUpsert{
|
if _, err = upsertMigrationHistory(ctx, tx, &MigrationHistoryUpsert{
|
||||||
Version: minorVersion + ".0",
|
Version: version,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to upsert migration history with version: %s, err: %w", version, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
@@ -175,7 +176,7 @@ func (db *DB) applyMigrationForMinorVersion(ctx context.Context, minorVersion st
|
|||||||
func (db *DB) seed(ctx context.Context) error {
|
func (db *DB) seed(ctx context.Context) error {
|
||||||
filenames, err := fs.Glob(seedFS, fmt.Sprintf("%s/*.sql", "seed"))
|
filenames, err := fs.Glob(seedFS, fmt.Sprintf("%s/*.sql", "seed"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to read seed files, err: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Strings(filenames)
|
sort.Strings(filenames)
|
||||||
@@ -203,7 +204,7 @@ func (db *DB) execute(ctx context.Context, stmt string) error {
|
|||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
if _, err := tx.ExecContext(ctx, stmt); err != nil {
|
if _, err := tx.ExecContext(ctx, stmt); err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to execute statement, err: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func (s *Store) Vacuum(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exec vacuum records in a transcation.
|
// Exec vacuum records in a transaction.
|
||||||
func vacuum(ctx context.Context, tx *sql.Tx) error {
|
func vacuum(ctx context.Context, tx *sql.Tx) error {
|
||||||
if err := vacuumMemo(ctx, tx); err != nil {
|
if err := vacuumMemo(ctx, tx); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/logo.webp" type="image/*" />
|
<link rel="icon" href="/logo.webp" type="image/*" />
|
||||||
<meta name="theme-color" content="#f6f5f4" />
|
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f6f5f4" />
|
||||||
|
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#27272a" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<title>Memos</title>
|
<title>Memos</title>
|
||||||
|
|||||||
@@ -10,12 +10,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.5",
|
||||||
"@emotion/styled": "^11.10.5",
|
"@emotion/styled": "^11.10.5",
|
||||||
"@mui/joy": "^5.0.0-alpha.52",
|
"@mui/joy": "^5.0.0-alpha.56",
|
||||||
"@reduxjs/toolkit": "^1.8.1",
|
"@reduxjs/toolkit": "^1.8.1",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"copy-to-clipboard": "^3.3.2",
|
"copy-to-clipboard": "^3.3.2",
|
||||||
"dayjs": "^1.11.3",
|
"dayjs": "^1.11.3",
|
||||||
"emoji-picker-react": "^3.6.2",
|
|
||||||
"highlight.js": "^11.6.0",
|
"highlight.js": "^11.6.0",
|
||||||
"i18next": "^21.9.2",
|
"i18next": "^21.9.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
@@ -25,7 +24,8 @@
|
|||||||
"react-feather": "^2.0.10",
|
"react-feather": "^2.0.10",
|
||||||
"react-i18next": "^11.18.6",
|
"react-i18next": "^11.18.6",
|
||||||
"react-redux": "^8.0.1",
|
"react-redux": "^8.0.1",
|
||||||
"react-router-dom": "^6.4.0"
|
"react-router-dom": "^6.4.0",
|
||||||
|
"tailwindcss": "^3.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "^29.1.2",
|
"@jest/globals": "^29.1.2",
|
||||||
@@ -47,7 +47,6 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"postcss": "^8.4.5",
|
"postcss": "^8.4.5",
|
||||||
"prettier": "2.5.1",
|
"prettier": "2.5.1",
|
||||||
"tailwindcss": "^3.0.18",
|
|
||||||
"ts-jest": "^29.0.3",
|
"ts-jest": "^29.0.3",
|
||||||
"typescript": "^4.3.2",
|
"typescript": "^4.3.2",
|
||||||
"vite": "^3.0.0"
|
"vite": "^3.0.0"
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { CssVarsProvider } from "@mui/joy/styles";
|
import { useColorScheme } from "@mui/joy";
|
||||||
import { useEffect } from "react";
|
import { useEffect, Suspense } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { RouterProvider } from "react-router-dom";
|
import { RouterProvider } from "react-router-dom";
|
||||||
import { locationService } from "./services";
|
import { globalService, locationService } from "./services";
|
||||||
import { useAppSelector } from "./store";
|
import { useAppSelector } from "./store";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
import * as storage from "./helpers/storage";
|
import * as storage from "./helpers/storage";
|
||||||
|
import { getSystemColorScheme } from "./helpers/utils";
|
||||||
|
import Loading from "./pages/Loading";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const { locale, systemStatus } = useAppSelector((state) => state.global);
|
const { appearance, locale, systemStatus } = useAppSelector((state) => state.global);
|
||||||
|
const { mode, setMode } = useColorScheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
locationService.updateStateWithLocation();
|
locationService.updateStateWithLocation();
|
||||||
@@ -18,6 +21,15 @@ function App() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
|
||||||
|
if (globalService.getState().appearance === "system") {
|
||||||
|
const mode = e.matches ? "dark" : "light";
|
||||||
|
setMode(mode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Inject additional style and script codes.
|
// Inject additional style and script codes.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (systemStatus.additionalStyle) {
|
if (systemStatus.additionalStyle) {
|
||||||
@@ -34,16 +46,39 @@ function App() {
|
|||||||
}, [systemStatus]);
|
}, [systemStatus]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
document.documentElement.setAttribute("lang", locale);
|
||||||
i18n.changeLanguage(locale);
|
i18n.changeLanguage(locale);
|
||||||
storage.set({
|
storage.set({
|
||||||
locale: locale,
|
locale: locale,
|
||||||
});
|
});
|
||||||
}, [locale]);
|
}, [locale]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
storage.set({
|
||||||
|
appearance: appearance,
|
||||||
|
});
|
||||||
|
|
||||||
|
let currentAppearance = appearance;
|
||||||
|
if (appearance === "system") {
|
||||||
|
currentAppearance = getSystemColorScheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
setMode(currentAppearance);
|
||||||
|
}, [appearance]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
if (mode === "light") {
|
||||||
|
root.classList.remove("dark");
|
||||||
|
} else if (mode === "dark") {
|
||||||
|
root.classList.add("dark");
|
||||||
|
}
|
||||||
|
}, [mode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CssVarsProvider>
|
<Suspense fallback={<Loading />}>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</CssVarsProvider>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,27 +18,36 @@ const AboutSiteDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dialog-header-container">
|
<div className="dialog-header-container">
|
||||||
<p className="title-text">
|
<p className="title-text flex items-center">
|
||||||
<span className="icon-text">🤠</span>
|
<img className="w-7 h-auto mr-1" src="/logo.webp" alt="" />
|
||||||
{t("common.about")}
|
{t("common.about")} memos
|
||||||
</p>
|
</p>
|
||||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||||
<Icon.X />
|
<Icon.X />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="dialog-content-container">
|
<div className="dialog-content-container">
|
||||||
<img className="logo-img" src="/logo-full.webp" alt="" />
|
|
||||||
<p>{t("slogan")}</p>
|
<p>{t("slogan")}</p>
|
||||||
<br />
|
<div className="border-t mt-1 pt-2 flex flex-row justify-start items-center">
|
||||||
<div className="addtion-info-container">
|
<span className=" text-gray-500 mr-2">Other projects:</span>
|
||||||
|
<a href="https://github.com/boojack/sticky-notes" className="flex items-center underline text-blue-600 hover:opacity-80">
|
||||||
|
<img
|
||||||
|
className="w-5 h-auto mr-1"
|
||||||
|
src="https://raw.githubusercontent.com/boojack/sticky-notes/main/public/sticky-notes.ico"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<span>Sticky notes</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex flex-row text-sm justify-start items-center">
|
||||||
<GitHubBadge />
|
<GitHubBadge />
|
||||||
<>
|
<span className="ml-2">
|
||||||
{t("common.version")}:
|
{t("common.version")}:
|
||||||
<span className="pre-text">
|
<span className="font-mono">
|
||||||
{profile.version}-{profile.mode}
|
{profile.version}-{profile.mode}
|
||||||
</span>
|
</span>
|
||||||
🎉
|
🎉
|
||||||
</>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
52
web/src/components/AppearanceSelect.tsx
Normal file
52
web/src/components/AppearanceSelect.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { Option, Select } from "@mui/joy";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { globalService, userService } from "../services";
|
||||||
|
import { useAppSelector } from "../store";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
|
||||||
|
const appearanceList = ["system", "light", "dark"];
|
||||||
|
|
||||||
|
const AppearanceSelect = () => {
|
||||||
|
const user = useAppSelector((state) => state.user.user);
|
||||||
|
const appearance = useAppSelector((state) => state.global.appearance);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const getPrefixIcon = (apperance: Appearance) => {
|
||||||
|
const className = "w-4 h-auto";
|
||||||
|
if (apperance === "light") {
|
||||||
|
return <Icon.Sun className={className} />;
|
||||||
|
} else if (apperance === "dark") {
|
||||||
|
return <Icon.Moon className={className} />;
|
||||||
|
} else {
|
||||||
|
return <Icon.Smile className={className} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectChange = async (appearance: Appearance) => {
|
||||||
|
if (user) {
|
||||||
|
await userService.upsertUserSetting("appearance", appearance);
|
||||||
|
}
|
||||||
|
globalService.setAppearance(appearance);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
className="!min-w-[10rem] w-auto text-sm"
|
||||||
|
value={appearance}
|
||||||
|
onChange={(_, appearance) => {
|
||||||
|
if (appearance) {
|
||||||
|
handleSelectChange(appearance);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
startDecorator={getPrefixIcon(appearance)}
|
||||||
|
>
|
||||||
|
{appearanceList.map((item) => (
|
||||||
|
<Option key={item} value={item} className="whitespace-nowrap">
|
||||||
|
{t(`setting.apperance-option.${item}`)}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppearanceSelect;
|
||||||
@@ -51,7 +51,7 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`memo-wrapper archived-memo ${"memos-" + memo.id}`} onMouseLeave={handleMouseLeaveMemoWrapper}>
|
<div className={`memo-wrapper archived ${"memos-" + memo.id}`} onMouseLeave={handleMouseLeaveMemoWrapper}>
|
||||||
<div className="memo-top-wrapper">
|
<div className="memo-top-wrapper">
|
||||||
<span className="time-text">
|
<span className="time-text">
|
||||||
{t("common.archived-at")} {utils.getDateTimeString(memo.updatedTs)}
|
{t("common.archived-at")} {utils.getDateTimeString(memo.updatedTs)}
|
||||||
|
|||||||
125
web/src/components/ChangeMemberPasswordDialog.tsx
Normal file
125
web/src/components/ChangeMemberPasswordDialog.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { validate, ValidatorConfig } from "../helpers/validator";
|
||||||
|
import { userService } from "../services";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
import { generateDialog } from "./Dialog";
|
||||||
|
import toastHelper from "./Toast";
|
||||||
|
|
||||||
|
const validateConfig: ValidatorConfig = {
|
||||||
|
minLength: 4,
|
||||||
|
maxLength: 320,
|
||||||
|
noSpace: true,
|
||||||
|
noChinese: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props extends DialogProps {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChangeMemberPasswordDialog: React.FC<Props> = (props: Props) => {
|
||||||
|
const { user: propsUser, destroy } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [newPassword, setNewPassword] = useState("");
|
||||||
|
const [newPasswordAgain, setNewPasswordAgain] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// do nth
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCloseBtnClick = () => {
|
||||||
|
destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNewPasswordChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const text = e.target.value as string;
|
||||||
|
setNewPassword(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNewPasswordAgainChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const text = e.target.value as string;
|
||||||
|
setNewPasswordAgain(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveBtnClick = async () => {
|
||||||
|
if (newPassword === "" || newPasswordAgain === "") {
|
||||||
|
toastHelper.error(t("message.fill-all"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword !== newPasswordAgain) {
|
||||||
|
toastHelper.error(t("message.new-password-not-match"));
|
||||||
|
setNewPasswordAgain("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordValidResult = validate(newPassword, validateConfig);
|
||||||
|
if (!passwordValidResult.result) {
|
||||||
|
toastHelper.error(`${t("common.password")} ${t(passwordValidResult.reason as string)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await userService.patchUser({
|
||||||
|
id: propsUser.id,
|
||||||
|
password: newPassword,
|
||||||
|
});
|
||||||
|
toastHelper.info(t("message.password-changed"));
|
||||||
|
handleCloseBtnClick();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
toastHelper.error(error.response.data.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="dialog-header-container !w-64">
|
||||||
|
<p className="title-text">
|
||||||
|
{t("setting.account-section.change-password")} ({propsUser.username})
|
||||||
|
</p>
|
||||||
|
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||||
|
<Icon.X />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="dialog-content-container">
|
||||||
|
<p className="text-sm mb-1">{t("common.new-password")}</p>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="input-text"
|
||||||
|
placeholder={t("common.repeat-new-password")}
|
||||||
|
value={newPassword}
|
||||||
|
onChange={handleNewPasswordChanged}
|
||||||
|
/>
|
||||||
|
<p className="text-sm mb-1 mt-2">{t("common.repeat-new-password")}</p>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="input-text"
|
||||||
|
placeholder={t("common.repeat-new-password")}
|
||||||
|
value={newPasswordAgain}
|
||||||
|
onChange={handleNewPasswordAgainChanged}
|
||||||
|
/>
|
||||||
|
<div className="mt-4 w-full flex flex-row justify-end items-center space-x-2">
|
||||||
|
<span className="btn-text" onClick={handleCloseBtnClick}>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</span>
|
||||||
|
<span className="btn-primary" onClick={handleSaveBtnClick}>
|
||||||
|
{t("common.save")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function showChangeMemberPasswordDialog(user: User) {
|
||||||
|
generateDialog(
|
||||||
|
{
|
||||||
|
className: "change-member-password-dialog",
|
||||||
|
},
|
||||||
|
ChangeMemberPasswordDialog,
|
||||||
|
{ user }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default showChangeMemberPasswordDialog;
|
||||||
@@ -8,7 +8,7 @@ import toastHelper from "./Toast";
|
|||||||
|
|
||||||
const validateConfig: ValidatorConfig = {
|
const validateConfig: ValidatorConfig = {
|
||||||
minLength: 4,
|
minLength: 4,
|
||||||
maxLength: 24,
|
maxLength: 320,
|
||||||
noSpace: true,
|
noSpace: true,
|
||||||
noChinese: true,
|
noChinese: true,
|
||||||
};
|
};
|
||||||
@@ -52,7 +52,7 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
|
|
||||||
const passwordValidResult = validate(newPassword, validateConfig);
|
const passwordValidResult = validate(newPassword, validateConfig);
|
||||||
if (!passwordValidResult.result) {
|
if (!passwordValidResult.result) {
|
||||||
toastHelper.error(`${t("common.password")} ${passwordValidResult.reason}`);
|
toastHelper.error(`${t("common.password")} ${t(passwordValidResult.reason as string)}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +82,7 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
<p className="text-sm mb-1">{t("common.new-password")}</p>
|
<p className="text-sm mb-1">{t("common.new-password")}</p>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
className="input-text"
|
className="input-text"
|
||||||
placeholder={t("common.repeat-new-password")}
|
placeholder={t("common.repeat-new-password")}
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
@@ -90,6 +91,7 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
<p className="text-sm mb-1 mt-2">{t("common.repeat-new-password")}</p>
|
<p className="text-sm mb-1 mt-2">{t("common.repeat-new-password")}</p>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
className="input-text"
|
className="input-text"
|
||||||
placeholder={t("common.repeat-new-password")}
|
placeholder={t("common.repeat-new-password")}
|
||||||
value={newPasswordAgain}
|
value={newPasswordAgain}
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ const MemoFilterInputer: React.FC<MemoFilterInputerProps> = (props: MemoFilterIn
|
|||||||
if (["AND", "OR"].includes(value)) {
|
if (["AND", "OR"].includes(value)) {
|
||||||
handleFilterChange(index, {
|
handleFilterChange(index, {
|
||||||
...filter,
|
...filter,
|
||||||
relation: value as MemoFilterRalation,
|
relation: value as MemoFilterRelation,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ interface Props extends DialogProps {
|
|||||||
currentDateStamp: DateStamp;
|
currentDateStamp: DateStamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
const monthChineseStrArray = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dev"];
|
const monthChineseStrArray = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"];
|
||||||
const weekdayChineseStrArray = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
const weekdayChineseStrArray = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||||
|
|
||||||
const DailyReviewDialog: React.FC<Props> = (props: Props) => {
|
const DailyReviewDialog: React.FC<Props> = (props: Props) => {
|
||||||
@@ -43,7 +43,6 @@ const DailyReviewDialog: React.FC<Props> = (props: Props) => {
|
|||||||
toggleShowDatePicker(false);
|
toggleShowDatePicker(false);
|
||||||
|
|
||||||
toImage(memosElRef.current, {
|
toImage(memosElRef.current, {
|
||||||
backgroundColor: "#ffffff",
|
|
||||||
pixelRatio: window.devicePixelRatio * 2,
|
pixelRatio: window.devicePixelRatio * 2,
|
||||||
})
|
})
|
||||||
.then((url) => {
|
.then((url) => {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { Provider } from "react-redux";
|
|||||||
import { ANIMATION_DURATION } from "../../helpers/consts";
|
import { ANIMATION_DURATION } from "../../helpers/consts";
|
||||||
import store from "../../store";
|
import store from "../../store";
|
||||||
import "../../less/base-dialog.less";
|
import "../../less/base-dialog.less";
|
||||||
|
import { CssVarsProvider } from "@mui/joy";
|
||||||
|
import theme from "../../theme";
|
||||||
|
|
||||||
interface DialogConfig {
|
interface DialogConfig {
|
||||||
className: string;
|
className: string;
|
||||||
@@ -77,9 +79,11 @@ export function generateDialog<T extends DialogProps>(
|
|||||||
|
|
||||||
const Fragment = (
|
const Fragment = (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<BaseDialog destroy={cbs.destroy} clickSpaceDestroy={true} {...config}>
|
<CssVarsProvider theme={theme}>
|
||||||
<DialogComponent {...dialogProps} />
|
<BaseDialog destroy={cbs.destroy} clickSpaceDestroy={true} {...config}>
|
||||||
</BaseDialog>
|
<DialogComponent {...dialogProps} />
|
||||||
|
</BaseDialog>
|
||||||
|
</CssVarsProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import "../../less/editor.less";
|
|||||||
export interface EditorRefActions {
|
export interface EditorRefActions {
|
||||||
focus: FunctionType;
|
focus: FunctionType;
|
||||||
insertText: (text: string, prefix?: string, suffix?: string) => void;
|
insertText: (text: string, prefix?: string, suffix?: string) => void;
|
||||||
|
removeText: (start: number, length: number) => void;
|
||||||
setContent: (text: string) => void;
|
setContent: (text: string) => void;
|
||||||
getContent: () => string;
|
getContent: () => string;
|
||||||
getSelectedContent: () => string;
|
getSelectedContent: () => string;
|
||||||
@@ -67,6 +68,19 @@ const Editor = forwardRef(function Editor(props: Props, ref: React.ForwardedRef<
|
|||||||
handleContentChangeCallback(editorRef.current.value);
|
handleContentChangeCallback(editorRef.current.value);
|
||||||
refresh();
|
refresh();
|
||||||
},
|
},
|
||||||
|
removeText: (start: number, length: number) => {
|
||||||
|
if (!editorRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevValue = editorRef.current.value;
|
||||||
|
const value = prevValue.slice(0, start) + prevValue.slice(start + length);
|
||||||
|
editorRef.current.value = value;
|
||||||
|
editorRef.current.focus();
|
||||||
|
editorRef.current.selectionEnd = start;
|
||||||
|
handleContentChangeCallback(editorRef.current.value);
|
||||||
|
refresh();
|
||||||
|
},
|
||||||
setContent: (text: string) => {
|
setContent: (text: string) => {
|
||||||
if (editorRef.current) {
|
if (editorRef.current) {
|
||||||
editorRef.current.value = text;
|
editorRef.current.value = text;
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import Picker, { IEmojiPickerProps } from "emoji-picker-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
shouldShow: boolean;
|
|
||||||
onEmojiClick: IEmojiPickerProps["onEmojiClick"];
|
|
||||||
onShouldShowEmojiPickerChange: (status: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
hasShown: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EmojiPicker: React.FC<Props> = (props: Props) => {
|
|
||||||
const { shouldShow, onEmojiClick, onShouldShowEmojiPickerChange } = props;
|
|
||||||
const [state, setState] = useState<State>({
|
|
||||||
hasShown: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (shouldShow) {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
const emojiWrapper = document.querySelector(".emoji-picker-react");
|
|
||||||
const isContains = emojiWrapper?.contains(event.target as Node);
|
|
||||||
if (!isContains) {
|
|
||||||
onShouldShowEmojiPickerChange(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("click", handleClickOutside, {
|
|
||||||
capture: true,
|
|
||||||
once: true,
|
|
||||||
});
|
|
||||||
setState({
|
|
||||||
hasShown: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [shouldShow]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{state.hasShown && (
|
|
||||||
<div className={`emoji-picker ${shouldShow ? "" : "hidden"}`}>
|
|
||||||
<Picker onEmojiClick={onEmojiClick} disableSearchBar />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EmojiPicker;
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import * as api from "../helpers/api";
|
import * as api from "../helpers/api";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import "../less/github-badge.less";
|
|
||||||
|
|
||||||
const GitHubBadge = () => {
|
const GitHubBadge = () => {
|
||||||
const [starCount, setStarCount] = useState(0);
|
const [starCount, setStarCount] = useState(0);
|
||||||
@@ -13,12 +12,15 @@ const GitHubBadge = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a className="github-badge-container" href="https://github.com/usememos/memos">
|
<a
|
||||||
<div className="github-icon">
|
className="h-7 flex flex-row justify-start items-center border dark:border-zinc-600 rounded cursor-pointer hover:opacity-80"
|
||||||
<Icon.GitHub className="icon-img" />
|
href="https://github.com/usememos/memos"
|
||||||
|
>
|
||||||
|
<div className="apply w-auto h-full px-2 flex flex-row justify-center items-center text-xs bg-gray-100 dark:bg-zinc-700">
|
||||||
|
<Icon.GitHub className="mr-1 w-4 h-4" />
|
||||||
Star
|
Star
|
||||||
</div>
|
</div>
|
||||||
<div className="count-text">{starCount || ""}</div>
|
<div className="w-auto h-full flex flex-row justify-center items-center px-3 text-xs font-bold">{starCount || ""}</div>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
|
||||||
import { memo, useEffect, useRef, useState } from "react";
|
import { memo, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import "dayjs/locale/zh";
|
|
||||||
import { editorStateService, locationService, memoService, userService } from "../services";
|
import { editorStateService, locationService, memoService, userService } from "../services";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import toastHelper from "./Toast";
|
import toastHelper from "./Toast";
|
||||||
import MemoContent from "./MemoContent";
|
import MemoContent from "./MemoContent";
|
||||||
import MemoResources from "./MemoResources";
|
import MemoResources from "./MemoResources";
|
||||||
import showShareMemoImageDialog from "./ShareMemoImageDialog";
|
import showShareMemo from "./ShareMemoDialog";
|
||||||
import showPreviewImageDialog from "./PreviewImageDialog";
|
import showPreviewImageDialog from "./PreviewImageDialog";
|
||||||
import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog";
|
import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog";
|
||||||
import "../less/memo.less";
|
import "../less/memo.less";
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
memo: Memo;
|
memo: Memo;
|
||||||
highlightWord?: string;
|
highlightWord?: string;
|
||||||
@@ -93,7 +89,7 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleGenMemoImageBtnClick = () => {
|
const handleGenMemoImageBtnClick = () => {
|
||||||
showShareMemoImageDialog(memo);
|
showShareMemo(memo);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMemoContentClick = async (e: React.MouseEvent) => {
|
const handleMemoContentClick = async (e: React.MouseEvent) => {
|
||||||
@@ -139,22 +135,9 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (targetEl.tagName === "IMG") {
|
} else if (targetEl.tagName === "IMG") {
|
||||||
const currImgUrl = targetEl.getAttribute("src");
|
const imgUrl = targetEl.getAttribute("src");
|
||||||
|
if (imgUrl) {
|
||||||
if (currImgUrl) {
|
showPreviewImageDialog([imgUrl], 0);
|
||||||
// use regex to get all image urls from memo content
|
|
||||||
const imageUrls =
|
|
||||||
memo.content.match(/!\[.*?\]\((.*?)\)/g)?.map(
|
|
||||||
(item) =>
|
|
||||||
item
|
|
||||||
.match(/\((.*?)\)/g)
|
|
||||||
?.slice(-1)[0]
|
|
||||||
.slice(1, -1) ?? ""
|
|
||||||
) ?? [];
|
|
||||||
showPreviewImageDialog(
|
|
||||||
imageUrls,
|
|
||||||
imageUrls.findIndex((item) => item === currImgUrl)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -162,9 +145,7 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
const handleMemoContentDoubleClick = (e: React.MouseEvent) => {
|
const handleMemoContentDoubleClick = (e: React.MouseEvent) => {
|
||||||
const targetEl = e.target as HTMLElement;
|
const targetEl = e.target as HTMLElement;
|
||||||
|
|
||||||
if (targetEl.className === "memo-link-text") {
|
if (targetEl.className === "tag-span") {
|
||||||
return;
|
|
||||||
} else if (targetEl.className === "tag-span") {
|
|
||||||
return;
|
return;
|
||||||
} else if (targetEl.classList.contains("todo-block")) {
|
} else if (targetEl.classList.contains("todo-block")) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { marked } from "../labs/marked";
|
import { marked } from "../labs/marked";
|
||||||
import { highlightWithWord } from "../labs/highlighter";
|
import { highlightWithWord } from "../labs/highlighter";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import { SETTING_IS_FOLDING_ENABLED_KEY, IS_FOLDING_ENABLED_DEFAULT_VALUE } from "../helpers/consts";
|
import { useAppSelector } from "../store";
|
||||||
import useLocalStorage from "../hooks/useLocalStorage";
|
|
||||||
import "../less/memo-content.less";
|
import "../less/memo-content.less";
|
||||||
|
|
||||||
export interface DisplayConfig {
|
export interface DisplayConfig {
|
||||||
@@ -37,7 +36,8 @@ const MemoContent: React.FC<Props> = (props: Props) => {
|
|||||||
return firstHorizontalRuleIndex !== -1 ? content.slice(0, firstHorizontalRuleIndex) : content;
|
return firstHorizontalRuleIndex !== -1 ? content.slice(0, firstHorizontalRuleIndex) : content;
|
||||||
}, [content]);
|
}, [content]);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isFoldingEnabled] = useLocalStorage(SETTING_IS_FOLDING_ENABLED_KEY, IS_FOLDING_ENABLED_DEFAULT_VALUE);
|
const user = useAppSelector((state) => state.user.user);
|
||||||
|
|
||||||
const [state, setState] = useState<State>({
|
const [state, setState] = useState<State>({
|
||||||
expandButtonStatus: -1,
|
expandButtonStatus: -1,
|
||||||
});
|
});
|
||||||
@@ -52,15 +52,20 @@ const MemoContent: React.FC<Props> = (props: Props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (displayConfig.enableExpand && isFoldingEnabled) {
|
if (displayConfig.enableExpand && user && user.localSetting.enableFoldMemo) {
|
||||||
if (foldedContent.length !== content.length) {
|
if (foldedContent.length !== content.length) {
|
||||||
setState({
|
setState({
|
||||||
...state,
|
...state,
|
||||||
expandButtonStatus: 0,
|
expandButtonStatus: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
expandButtonStatus: -1,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, [user?.localSetting.enableFoldMemo]);
|
||||||
|
|
||||||
const handleMemoContentClick = async (e: React.MouseEvent) => {
|
const handleMemoContentClick = async (e: React.MouseEvent) => {
|
||||||
if (onMemoContentClick) {
|
if (onMemoContentClick) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { IEmojiData } from "emoji-picker-react";
|
import { last, toLower } from "lodash";
|
||||||
import { toLower } from "lodash";
|
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { deleteMemoResource, upsertMemoResource } from "../helpers/api";
|
import { deleteMemoResource, upsertMemoResource } from "../helpers/api";
|
||||||
@@ -11,10 +10,12 @@ import Icon from "./Icon";
|
|||||||
import toastHelper from "./Toast";
|
import toastHelper from "./Toast";
|
||||||
import Selector from "./common/Selector";
|
import Selector from "./common/Selector";
|
||||||
import Editor, { EditorRefActions } from "./Editor/Editor";
|
import Editor, { EditorRefActions } from "./Editor/Editor";
|
||||||
import EmojiPicker from "./Editor/EmojiPicker";
|
|
||||||
import ResourceIcon from "./ResourceIcon";
|
import ResourceIcon from "./ResourceIcon";
|
||||||
|
import showResourcesSelectorDialog from "./ResourcesSelectorDialog";
|
||||||
import "../less/memo-editor.less";
|
import "../less/memo-editor.less";
|
||||||
|
|
||||||
|
const listItemSymbolList = ["* ", "- ", "- [ ] ", "- [x] ", "- [X] "];
|
||||||
|
|
||||||
const getEditorContentCache = (): string => {
|
const getEditorContentCache = (): string => {
|
||||||
return storage.get(["editorContentCache"]).editorContentCache ?? "";
|
return storage.get(["editorContentCache"]).editorContentCache ?? "";
|
||||||
};
|
};
|
||||||
@@ -34,7 +35,6 @@ const setEditingMemoVisibilityCache = (visibility: Visibility) => {
|
|||||||
interface State {
|
interface State {
|
||||||
fullscreen: boolean;
|
fullscreen: boolean;
|
||||||
isUploadingResource: boolean;
|
isUploadingResource: boolean;
|
||||||
resourceList: Resource[];
|
|
||||||
shouldShowEmojiPicker: boolean;
|
shouldShowEmojiPicker: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,12 +48,11 @@ const MemoEditor = () => {
|
|||||||
isUploadingResource: false,
|
isUploadingResource: false,
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
shouldShowEmojiPicker: false,
|
shouldShowEmojiPicker: false,
|
||||||
resourceList: [],
|
|
||||||
});
|
});
|
||||||
const [allowSave, setAllowSave] = useState<boolean>(false);
|
const [allowSave, setAllowSave] = useState<boolean>(false);
|
||||||
const prevGlobalStateRef = useRef(editorState);
|
const prevEditorStateRef = useRef(editorState);
|
||||||
const editorRef = useRef<EditorRefActions>(null);
|
const editorRef = useRef<EditorRefActions>(null);
|
||||||
const tagSeletorRef = useRef<HTMLDivElement>(null);
|
const tagSelectorRef = useRef<HTMLDivElement>(null);
|
||||||
const memoVisibilityOptionSelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => {
|
const memoVisibilityOptionSelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => {
|
||||||
return {
|
return {
|
||||||
value: item.value,
|
value: item.value,
|
||||||
@@ -79,13 +78,8 @@ const MemoEditor = () => {
|
|||||||
if (memo) {
|
if (memo) {
|
||||||
handleEditorFocus();
|
handleEditorFocus();
|
||||||
editorStateService.setMemoVisibility(memo.visibility);
|
editorStateService.setMemoVisibility(memo.visibility);
|
||||||
|
editorStateService.setResourceList(memo.resourceList);
|
||||||
editorRef.current?.setContent(memo.content ?? "");
|
editorRef.current?.setContent(memo.content ?? "");
|
||||||
setState((state) => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
resourceList: memo.resourceList,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
storage.set({
|
storage.set({
|
||||||
@@ -95,23 +89,14 @@ const MemoEditor = () => {
|
|||||||
storage.remove(["editingMemoIdCache"]);
|
storage.remove(["editingMemoIdCache"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
prevGlobalStateRef.current = editorState;
|
prevEditorStateRef.current = editorState;
|
||||||
}, [editorState.editMemoId]);
|
}, [editorState.editMemoId]);
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||||
if (event.key === "Escape") {
|
if (!editorRef.current) {
|
||||||
if (state.fullscreen) {
|
|
||||||
handleFullscreenBtnClick();
|
|
||||||
} else {
|
|
||||||
handleCancelEdit();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.key === "Tab") {
|
|
||||||
event.preventDefault();
|
|
||||||
editorRef.current?.insertText(" ".repeat(TAB_SPACE_WIDTH));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.ctrlKey || event.metaKey) {
|
if (event.ctrlKey || event.metaKey) {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
handleSaveBtnClick();
|
handleSaveBtnClick();
|
||||||
@@ -119,20 +104,53 @@ const MemoEditor = () => {
|
|||||||
}
|
}
|
||||||
if (event.key === "b") {
|
if (event.key === "b") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
editorRef.current?.insertText("", "**", "**");
|
editorRef.current.insertText("", "**", "**");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.key === "i") {
|
if (event.key === "i") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
editorRef.current?.insertText("", "*", "*");
|
editorRef.current.insertText("", "*", "*");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.key === "e") {
|
if (event.key === "e") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
editorRef.current?.insertText("", "`", "`");
|
editorRef.current.insertText("", "`", "`");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
const cursorPosition = editorRef.current.getCursorPosition();
|
||||||
|
const contentBeforeCursor = editorRef.current.getContent().slice(0, cursorPosition);
|
||||||
|
const rowValue = last(contentBeforeCursor.split("\n"));
|
||||||
|
if (rowValue) {
|
||||||
|
if (listItemSymbolList.includes(rowValue)) {
|
||||||
|
event.preventDefault();
|
||||||
|
editorRef.current.removeText(cursorPosition - rowValue.length, rowValue.length);
|
||||||
|
} else {
|
||||||
|
for (const listItemSymbol of listItemSymbolList) {
|
||||||
|
if (rowValue.startsWith(listItemSymbol)) {
|
||||||
|
event.preventDefault();
|
||||||
|
editorRef.current.insertText("", `\n${listItemSymbol}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
if (state.fullscreen) {
|
||||||
|
handleFullscreenBtnClick();
|
||||||
|
} else if (editorState.editMemoId) {
|
||||||
|
handleCancelEdit();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === "Tab") {
|
||||||
|
event.preventDefault();
|
||||||
|
editorRef.current.insertText(" ".repeat(TAB_SPACE_WIDTH));
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDropEvent = async (event: React.DragEvent) => {
|
const handleDropEvent = async (event: React.DragEvent) => {
|
||||||
@@ -148,12 +166,7 @@ const MemoEditor = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setState((state) => {
|
editorStateService.setResourceList([...editorState.resourceList, ...resourceList]);
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
resourceList: [...state.resourceList, ...resourceList],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -163,12 +176,7 @@ const MemoEditor = () => {
|
|||||||
const file = event.clipboardData.files[0];
|
const file = event.clipboardData.files[0];
|
||||||
const resource = await handleUploadResource(file);
|
const resource = await handleUploadResource(file);
|
||||||
if (resource) {
|
if (resource) {
|
||||||
setState((state) => {
|
editorStateService.setResourceList([...editorState.resourceList, resource]);
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
resourceList: [...state.resourceList, resource],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -216,7 +224,7 @@ const MemoEditor = () => {
|
|||||||
id: prevMemo.id,
|
id: prevMemo.id,
|
||||||
content,
|
content,
|
||||||
visibility: editorState.memoVisibility,
|
visibility: editorState.memoVisibility,
|
||||||
resourceIdList: state.resourceList.map((resource) => resource.id),
|
resourceIdList: editorState.resourceList.map((resource) => resource.id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
editorStateService.clearEditMemo();
|
editorStateService.clearEditMemo();
|
||||||
@@ -224,7 +232,7 @@ const MemoEditor = () => {
|
|||||||
await memoService.createMemo({
|
await memoService.createMemo({
|
||||||
content,
|
content,
|
||||||
visibility: editorState.memoVisibility,
|
visibility: editorState.memoVisibility,
|
||||||
resourceIdList: state.resourceList.map((resource) => resource.id),
|
resourceIdList: editorState.resourceList.map((resource) => resource.id),
|
||||||
});
|
});
|
||||||
locationService.clearQuery();
|
locationService.clearQuery();
|
||||||
}
|
}
|
||||||
@@ -237,23 +245,22 @@ const MemoEditor = () => {
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
resourceList: [],
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
editorStateService.clearResourceList();
|
||||||
setEditorContentCache("");
|
setEditorContentCache("");
|
||||||
storage.remove(["editingMemoVisibilityCache"]);
|
storage.remove(["editingMemoVisibilityCache"]);
|
||||||
editorRef.current?.setContent("");
|
editorRef.current?.setContent("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancelEdit = () => {
|
const handleCancelEdit = () => {
|
||||||
setState({
|
if (editorState.editMemoId) {
|
||||||
...state,
|
editorStateService.clearEditMemo();
|
||||||
resourceList: [],
|
editorStateService.clearResourceList();
|
||||||
});
|
editorRef.current?.setContent("");
|
||||||
editorStateService.clearEditMemo();
|
setEditorContentCache("");
|
||||||
editorRef.current?.setContent("");
|
storage.remove(["editingMemoVisibilityCache"]);
|
||||||
setEditorContentCache("");
|
}
|
||||||
storage.remove(["editingMemoVisibilityCache"]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleContentChange = (content: string) => {
|
const handleContentChange = (content: string) => {
|
||||||
@@ -261,10 +268,6 @@ const MemoEditor = () => {
|
|||||||
setEditorContentCache(content);
|
setEditorContentCache(content);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEmojiPickerBtnClick = () => {
|
|
||||||
handleChangeShouldShowEmojiPicker(!state.shouldShowEmojiPicker);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCheckBoxBtnClick = () => {
|
const handleCheckBoxBtnClick = () => {
|
||||||
if (!editorRef.current) {
|
if (!editorRef.current) {
|
||||||
return;
|
return;
|
||||||
@@ -317,12 +320,7 @@ const MemoEditor = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setState((state) => {
|
editorStateService.setResourceList([...editorState.resourceList, ...resourceList]);
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
resourceList: [...state.resourceList, ...resourceList],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
document.body.removeChild(inputEl);
|
document.body.removeChild(inputEl);
|
||||||
};
|
};
|
||||||
inputEl.click();
|
inputEl.click();
|
||||||
@@ -337,37 +335,15 @@ const MemoEditor = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTagSeletorClick = useCallback((event: React.MouseEvent) => {
|
const handleTagSelectorClick = useCallback((event: React.MouseEvent) => {
|
||||||
if (tagSeletorRef.current !== event.target && tagSeletorRef.current?.contains(event.target as Node)) {
|
if (tagSelectorRef.current !== event.target && tagSelectorRef.current?.contains(event.target as Node)) {
|
||||||
editorRef.current?.insertText(`#${(event.target as HTMLElement).textContent} ` ?? "");
|
editorRef.current?.insertText(`#${(event.target as HTMLElement).textContent} ` ?? "");
|
||||||
handleEditorFocus();
|
handleEditorFocus();
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleChangeShouldShowEmojiPicker = (status: boolean) => {
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
shouldShowEmojiPicker: status,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEmojiClick = (_: any, emojiObject: IEmojiData) => {
|
|
||||||
if (!editorRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
editorRef.current.insertText(`${emojiObject.emoji}`);
|
|
||||||
handleChangeShouldShowEmojiPicker(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteResource = async (resourceId: ResourceId) => {
|
const handleDeleteResource = async (resourceId: ResourceId) => {
|
||||||
setState((state) => {
|
editorStateService.setResourceList(editorState.resourceList.filter((resource) => resource.id !== resourceId));
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
resourceList: state.resourceList.filter((resource) => resource.id !== resourceId),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (editorState.editMemoId) {
|
if (editorState.editMemoId) {
|
||||||
await deleteMemoResource(editorState.editMemoId, resourceId);
|
await deleteMemoResource(editorState.editMemoId, resourceId);
|
||||||
}
|
}
|
||||||
@@ -415,7 +391,7 @@ const MemoEditor = () => {
|
|||||||
<div className="common-tools-container">
|
<div className="common-tools-container">
|
||||||
<div className="action-btn tag-action">
|
<div className="action-btn tag-action">
|
||||||
<Icon.Hash className="icon-img" />
|
<Icon.Hash className="icon-img" />
|
||||||
<div ref={tagSeletorRef} className="tag-list" onClick={handleTagSeletorClick}>
|
<div ref={tagSelectorRef} className="tag-list" onClick={handleTagSelectorClick}>
|
||||||
{tags.length > 0 ? (
|
{tags.length > 0 ? (
|
||||||
tags.map((tag) => {
|
tags.map((tag) => {
|
||||||
return (
|
return (
|
||||||
@@ -431,32 +407,34 @@ const MemoEditor = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="action-btn !hidden sm:!flex ">
|
|
||||||
<Icon.Smile className="icon-img" onClick={handleEmojiPickerBtnClick} />
|
|
||||||
</button>
|
|
||||||
<button className="action-btn">
|
<button className="action-btn">
|
||||||
<Icon.CheckSquare className="icon-img" onClick={handleCheckBoxBtnClick} />
|
<Icon.CheckSquare className="icon-img" onClick={handleCheckBoxBtnClick} />
|
||||||
</button>
|
</button>
|
||||||
<button className="action-btn">
|
<button className="action-btn">
|
||||||
<Icon.Code className="icon-img" onClick={handleCodeBlockBtnClick} />
|
<Icon.Code className="icon-img" onClick={handleCodeBlockBtnClick} />
|
||||||
</button>
|
</button>
|
||||||
<button className="action-btn">
|
<div className="action-btn resource-btn">
|
||||||
<Icon.FileText className="icon-img" onClick={handleUploadFileBtnClick} />
|
<Icon.FileText className="icon-img" />
|
||||||
<span className={`tip-text ${state.isUploadingResource ? "!block" : ""}`}>Uploading</span>
|
<span className={`tip-text ${state.isUploadingResource ? "!block" : ""}`}>Uploading</span>
|
||||||
</button>
|
<div className="resource-action-list">
|
||||||
|
<div className="resource-action-item" onClick={handleUploadFileBtnClick}>
|
||||||
|
<Icon.Upload className="icon-img" />
|
||||||
|
<span>{t("editor.local")}</span>
|
||||||
|
</div>
|
||||||
|
<div className="resource-action-item" onClick={showResourcesSelectorDialog}>
|
||||||
|
<Icon.Database className="icon-img" />
|
||||||
|
<span>{t("editor.resources")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button className="action-btn" onClick={handleFullscreenBtnClick}>
|
<button className="action-btn" onClick={handleFullscreenBtnClick}>
|
||||||
{state.fullscreen ? <Icon.Minimize className="icon-img" /> : <Icon.Maximize className="icon-img" />}
|
{state.fullscreen ? <Icon.Minimize className="icon-img" /> : <Icon.Maximize className="icon-img" />}
|
||||||
</button>
|
</button>
|
||||||
<EmojiPicker
|
|
||||||
shouldShow={state.shouldShowEmojiPicker}
|
|
||||||
onEmojiClick={handleEmojiClick}
|
|
||||||
onShouldShowEmojiPickerChange={handleChangeShouldShowEmojiPicker}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{state.resourceList.length > 0 && (
|
{editorState.resourceList && editorState.resourceList.length > 0 && (
|
||||||
<div className="resource-list-wrapper">
|
<div className="resource-list-wrapper">
|
||||||
{state.resourceList.map((resource) => {
|
{editorState.resourceList.map((resource) => {
|
||||||
return (
|
return (
|
||||||
<div key={resource.id} className="resource-container">
|
<div key={resource.id} className="resource-container">
|
||||||
<ResourceIcon resourceType="resource.type" className="icon-img" />
|
<ResourceIcon resourceType="resource.type" className="icon-img" />
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const MemoResources: React.FC<Props> = (props: Props) => {
|
|||||||
const availableResourceList = resourceList.filter((resource) => resource.type.startsWith("image") || resource.type.startsWith("video"));
|
const availableResourceList = resourceList.filter((resource) => resource.type.startsWith("image") || resource.type.startsWith("video"));
|
||||||
const otherResourceList = resourceList.filter((resource) => !availableResourceList.includes(resource));
|
const otherResourceList = resourceList.filter((resource) => !availableResourceList.includes(resource));
|
||||||
|
|
||||||
const handlPreviewBtnClick = (resource: Resource) => {
|
const handlePreviewBtnClick = (resource: Resource) => {
|
||||||
const resourceUrl = `${window.location.origin}/o/r/${resource.id}/${resource.filename}`;
|
const resourceUrl = `${window.location.origin}/o/r/${resource.id}/${resource.filename}`;
|
||||||
window.open(resourceUrl);
|
window.open(resourceUrl);
|
||||||
};
|
};
|
||||||
@@ -45,8 +45,10 @@ const MemoResources: React.FC<Props> = (props: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Image className="memo-resource" key={resource.id} imgUrls={imgUrls} index={imgUrls.findIndex((item) => item === url)} />
|
<Image className="memo-resource" key={resource.id} imgUrls={imgUrls} index={imgUrls.findIndex((item) => item === url)} />
|
||||||
);
|
);
|
||||||
} else {
|
} else if (resource.type.startsWith("video")) {
|
||||||
return <video className="memo-resource" controls key={resource.id} src={url} />;
|
return <video className="memo-resource" controls key={resource.id} src={url} />;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -54,7 +56,7 @@ const MemoResources: React.FC<Props> = (props: Props) => {
|
|||||||
<div className="other-resource-wrapper">
|
<div className="other-resource-wrapper">
|
||||||
{otherResourceList.map((resource) => {
|
{otherResourceList.map((resource) => {
|
||||||
return (
|
return (
|
||||||
<div className="other-resource-container" key={resource.id} onClick={() => handlPreviewBtnClick(resource)}>
|
<div className="other-resource-container" key={resource.id} onClick={() => handlePreviewBtnClick(resource)}>
|
||||||
<Icon.FileText className="icon-img" />
|
<Icon.FileText className="icon-img" />
|
||||||
<span className="name-text">{resource.filename}</span>
|
<span className="name-text">{resource.filename}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { memoService, shortcutService } from "../services";
|
|||||||
import { useAppSelector } from "../store";
|
import { useAppSelector } from "../store";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import SearchBar from "./SearchBar";
|
import SearchBar from "./SearchBar";
|
||||||
import { toggleSiderbar } from "./Sidebar";
|
import { toggleSidebar } from "./Sidebar";
|
||||||
import "../less/memos-header.less";
|
import "../less/memos-header.less";
|
||||||
|
|
||||||
let prevRequestTimestamp = Date.now();
|
let prevRequestTimestamp = Date.now();
|
||||||
@@ -38,7 +38,7 @@ const MemosHeader = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="section-header-container memos-header-container">
|
<div className="section-header-container memos-header-container">
|
||||||
<div className="title-container">
|
<div className="title-container">
|
||||||
<div className="action-btn" onClick={() => toggleSiderbar(true)}>
|
<div className="action-btn" onClick={() => toggleSidebar(true)}>
|
||||||
<Icon.Menu className="icon-img" />
|
<Icon.Menu className="icon-img" />
|
||||||
</div>
|
</div>
|
||||||
<span className="title-text" onClick={handleTitleTextClick}>
|
<span className="title-text" onClick={handleTitleTextClick}>
|
||||||
|
|||||||
@@ -1,16 +1,33 @@
|
|||||||
import { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import * as utils from "../helpers/utils";
|
import * as utils from "../helpers/utils";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import { generateDialog } from "./Dialog";
|
import { generateDialog } from "./Dialog";
|
||||||
import "../less/preview-image-dialog.less";
|
import "../less/preview-image-dialog.less";
|
||||||
|
|
||||||
|
const MIN_SCALE = 0.5;
|
||||||
|
const MAX_SCALE = 5;
|
||||||
|
const SCALE_UNIT = 0.25;
|
||||||
|
|
||||||
interface Props extends DialogProps {
|
interface Props extends DialogProps {
|
||||||
imgUrls: string[];
|
imgUrls: string[];
|
||||||
initialIndex: number;
|
initialIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
angle: number;
|
||||||
|
scale: number;
|
||||||
|
originX: number;
|
||||||
|
originY: number;
|
||||||
|
}
|
||||||
|
|
||||||
const PreviewImageDialog: React.FC<Props> = ({ destroy, imgUrls, initialIndex }: Props) => {
|
const PreviewImageDialog: React.FC<Props> = ({ destroy, imgUrls, initialIndex }: Props) => {
|
||||||
const [currentIndex, setCurrentIndex] = useState(initialIndex);
|
const [currentIndex, setCurrentIndex] = useState(initialIndex);
|
||||||
|
const [state, setState] = useState<State>({
|
||||||
|
angle: 0,
|
||||||
|
scale: 1,
|
||||||
|
originX: -1,
|
||||||
|
originY: -1,
|
||||||
|
});
|
||||||
|
|
||||||
const handleCloseBtnClick = () => {
|
const handleCloseBtnClick = () => {
|
||||||
destroy();
|
destroy();
|
||||||
@@ -39,6 +56,38 @@ const PreviewImageDialog: React.FC<Props> = ({ destroy, imgUrls, initialIndex }:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleImgRotate = (event: React.MouseEvent, angle: number) => {
|
||||||
|
const curImgAngle = (state.angle + angle + 360) % 360;
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
originX: -1,
|
||||||
|
originY: -1,
|
||||||
|
angle: curImgAngle,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImgContainerScroll = (event: React.WheelEvent) => {
|
||||||
|
const offsetX = event.nativeEvent.offsetX;
|
||||||
|
const offsetY = event.nativeEvent.offsetY;
|
||||||
|
const sign = event.deltaY < 0 ? 1 : -1;
|
||||||
|
const curAngle = Math.max(MIN_SCALE, Math.min(MAX_SCALE, state.scale + sign * SCALE_UNIT));
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
originX: offsetX,
|
||||||
|
originY: offsetY,
|
||||||
|
scale: curAngle,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImageComputedStyle = () => {
|
||||||
|
return {
|
||||||
|
transform: `scale(${state.scale}) rotate(${state.angle}deg)`,
|
||||||
|
transformOrigin: `${state.originX === -1 ? "center" : `${state.originX}px`} ${
|
||||||
|
state.originY === -1 ? "center" : `${state.originY}px`
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="btns-container">
|
<div className="btns-container">
|
||||||
@@ -48,9 +97,20 @@ const PreviewImageDialog: React.FC<Props> = ({ destroy, imgUrls, initialIndex }:
|
|||||||
<button className="btn" onClick={handleDownloadBtnClick}>
|
<button className="btn" onClick={handleDownloadBtnClick}>
|
||||||
<Icon.Download className="icon-img" />
|
<Icon.Download className="icon-img" />
|
||||||
</button>
|
</button>
|
||||||
|
<button className="btn" onClick={(e) => handleImgRotate(e, -90)}>
|
||||||
|
<Icon.RotateCcw className="icon-img" />
|
||||||
|
</button>
|
||||||
|
<button className="btn" onClick={(e) => handleImgRotate(e, 90)}>
|
||||||
|
<Icon.RotateCw className="icon-img" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="img-container" onClick={handleImgContainerClick}>
|
<div className="img-container" onClick={handleImgContainerClick}>
|
||||||
<img onClick={(e) => e.stopPropagation()} src={imgUrls[currentIndex]} />
|
<img
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
src={imgUrls[currentIndex]}
|
||||||
|
onWheel={handleImgContainerScroll}
|
||||||
|
style={getImageComputedStyle()}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { Tooltip } from "@mui/joy";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import * as utils from "../helpers/utils";
|
|
||||||
import useLoading from "../hooks/useLoading";
|
import useLoading from "../hooks/useLoading";
|
||||||
import { resourceService } from "../services";
|
import { resourceService } from "../services";
|
||||||
import { useAppSelector } from "../store";
|
import { useAppSelector } from "../store";
|
||||||
@@ -83,15 +83,15 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
|
|||||||
inputEl.click();
|
inputEl.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getResouceUrl = useCallback((resource: Resource) => {
|
const getResourceUrl = useCallback((resource: Resource) => {
|
||||||
return `${window.location.origin}/o/r/${resource.id}/${resource.filename}`;
|
return `${window.location.origin}/o/r/${resource.id}/${resource.filename}`;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handlePreviewBtnClick = (resource: Resource) => {
|
const handlePreviewBtnClick = (resource: Resource) => {
|
||||||
const resourceUrl = getResouceUrl(resource);
|
const resourceUrl = getResourceUrl(resource);
|
||||||
if (resource.type.startsWith("image")) {
|
if (resource.type.startsWith("image")) {
|
||||||
showPreviewImageDialog(
|
showPreviewImageDialog(
|
||||||
resources.filter((r) => r.type.startsWith("image")).map((r) => getResouceUrl(r)),
|
resources.filter((r) => r.type.startsWith("image")).map((r) => getResourceUrl(r)),
|
||||||
resources.findIndex((r) => r.id === resource.id)
|
resources.findIndex((r) => r.id === resource.id)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -149,20 +149,6 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResourceNameOrTypeMouseEnter = useCallback((event: React.MouseEvent, nameOrType: string) => {
|
|
||||||
const tempDiv = document.createElement("div");
|
|
||||||
tempDiv.className = "usage-detail-container pop-up";
|
|
||||||
const bounding = utils.getElementBounding(event.target as HTMLElement);
|
|
||||||
tempDiv.style.left = bounding.left + "px";
|
|
||||||
tempDiv.style.top = bounding.top - 2 + "px";
|
|
||||||
tempDiv.innerHTML = `<span>${nameOrType}</span>`;
|
|
||||||
document.body.appendChild(tempDiv);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleResourceNameOrTypeMouseLeave = useCallback(() => {
|
|
||||||
document.body.querySelectorAll("div.usage-detail-container.pop-up").forEach((node) => node.remove());
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dialog-header-container">
|
<div className="dialog-header-container">
|
||||||
@@ -206,39 +192,34 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
|
|||||||
resources.map((resource) => (
|
resources.map((resource) => (
|
||||||
<div key={resource.id} className="resource-container">
|
<div key={resource.id} className="resource-container">
|
||||||
<span className="field-text id-text">{resource.id}</span>
|
<span className="field-text id-text">{resource.id}</span>
|
||||||
<span className="field-text name-text">
|
<Tooltip title={resource.filename}>
|
||||||
<span
|
<span className="field-text name-text">{resource.filename}</span>
|
||||||
onMouseEnter={(e) => handleResourceNameOrTypeMouseEnter(e, resource.filename)}
|
</Tooltip>
|
||||||
onMouseLeave={handleResourceNameOrTypeMouseLeave}
|
|
||||||
>
|
|
||||||
{resource.filename}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<div className="buttons-container">
|
<div className="buttons-container">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
actionsClassName="!w-28"
|
actionsClassName="!w-28"
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100"
|
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
|
||||||
onClick={() => handlePreviewBtnClick(resource)}
|
onClick={() => handlePreviewBtnClick(resource)}
|
||||||
>
|
>
|
||||||
{t("resources.preview")}
|
{t("resources.preview")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100"
|
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
|
||||||
onClick={() => handleRenameBtnClick(resource)}
|
onClick={() => handleRenameBtnClick(resource)}
|
||||||
>
|
>
|
||||||
{t("resources.rename")}
|
{t("resources.rename")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100"
|
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
|
||||||
onClick={() => handleCopyResourceLinkBtnClick(resource)}
|
onClick={() => handleCopyResourceLinkBtnClick(resource)}
|
||||||
>
|
>
|
||||||
{t("resources.copy-link")}
|
{t("resources.copy-link")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded text-red-600 hover:bg-gray-100"
|
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded text-red-600 hover:bg-gray-100 dark:hover:bg-zinc-600"
|
||||||
onClick={() => handleDeleteResourceBtnClick(resource)}
|
onClick={() => handleDeleteResourceBtnClick(resource)}
|
||||||
>
|
>
|
||||||
{t("common.delete")}
|
{t("common.delete")}
|
||||||
|
|||||||
155
web/src/components/ResourcesSelectorDialog.tsx
Normal file
155
web/src/components/ResourcesSelectorDialog.tsx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { Checkbox, Tooltip } from "@mui/joy";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import useLoading from "../hooks/useLoading";
|
||||||
|
import { editorStateService, resourceService } from "../services";
|
||||||
|
import { useAppSelector } from "../store";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
import toastHelper from "./Toast";
|
||||||
|
import { generateDialog } from "./Dialog";
|
||||||
|
import showPreviewImageDialog from "./PreviewImageDialog";
|
||||||
|
import "../less/resources-selector-dialog.less";
|
||||||
|
|
||||||
|
type Props = DialogProps;
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
checkedArray: boolean[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResourcesSelectorDialog: React.FC<Props> = (props: Props) => {
|
||||||
|
const { destroy } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const loadingState = useLoading();
|
||||||
|
const { resources } = useAppSelector((state) => state.resource);
|
||||||
|
const editorState = useAppSelector((state) => state.editor);
|
||||||
|
const [state, setState] = useState<State>({
|
||||||
|
checkedArray: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
resourceService
|
||||||
|
.fetchResourceList()
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
toastHelper.error(error.response.data.message);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loadingState.setFinish();
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkedResourceIdArray = editorState.resourceList.map((resource) => resource.id);
|
||||||
|
setState({
|
||||||
|
checkedArray: resources.map((resource) => {
|
||||||
|
return checkedResourceIdArray.includes(resource.id);
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}, [resources]);
|
||||||
|
|
||||||
|
const getResourceUrl = useCallback((resource: Resource) => {
|
||||||
|
return `${window.location.origin}/o/r/${resource.id}/${resource.filename}`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePreviewBtnClick = (resource: Resource) => {
|
||||||
|
const resourceUrl = getResourceUrl(resource);
|
||||||
|
if (resource.type.startsWith("image")) {
|
||||||
|
showPreviewImageDialog(
|
||||||
|
resources.filter((r) => r.type.startsWith("image")).map((r) => getResourceUrl(r)),
|
||||||
|
resources.findIndex((r) => r.id === resource.id)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
window.open(resourceUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckboxChange = (index: number) => {
|
||||||
|
const newCheckedArr = state.checkedArray;
|
||||||
|
newCheckedArr[index] = !newCheckedArr[index];
|
||||||
|
setState({
|
||||||
|
checkedArray: newCheckedArr,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmBtnClick = () => {
|
||||||
|
const resourceList = resources.filter((_, index) => {
|
||||||
|
return state.checkedArray[index];
|
||||||
|
});
|
||||||
|
editorStateService.setResourceList(resourceList);
|
||||||
|
destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="dialog-header-container">
|
||||||
|
<p className="title-text">
|
||||||
|
<span className="icon-text">🌄</span>
|
||||||
|
{t("sidebar.resources")}
|
||||||
|
</p>
|
||||||
|
<button className="btn close-btn" onClick={destroy}>
|
||||||
|
<Icon.X className="icon-img" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="dialog-content-container">
|
||||||
|
{loadingState.isLoading ? (
|
||||||
|
<div className="loading-text-container">
|
||||||
|
<p className="tip-text">{t("resources.fetching-data")}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="resource-table-container">
|
||||||
|
<div className="fields-container">
|
||||||
|
<span className="field-text id-text">ID</span>
|
||||||
|
<span className="field-text name-text">NAME</span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
{resources.length === 0 ? (
|
||||||
|
<p className="tip-text">{t("resources.no-resources")}</p>
|
||||||
|
) : (
|
||||||
|
resources.map((resource, index) => (
|
||||||
|
<div key={resource.id} className="resource-container">
|
||||||
|
<span className="field-text id-text">{resource.id}</span>
|
||||||
|
<Tooltip placement="top-start" title={resource.filename}>
|
||||||
|
<span className="field-text name-text">{resource.filename}</span>
|
||||||
|
</Tooltip>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Icon.Eye
|
||||||
|
className=" text-left text-sm leading-6 px-1 mr-2 cursor-pointer hover:opacity-80"
|
||||||
|
onClick={() => handlePreviewBtnClick(resource)}
|
||||||
|
>
|
||||||
|
{t("resources.preview")}
|
||||||
|
</Icon.Eye>
|
||||||
|
<Checkbox checked={state.checkedArray[index]} onChange={() => handleCheckboxChange(index)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-between w-full mt-2 px-2">
|
||||||
|
<span className="text-sm font-mono text-gray-400 leading-8">
|
||||||
|
{t("message.count-selected-resources")}: {state.checkedArray.filter((checked) => checked).length}
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-row justify-start items-center">
|
||||||
|
<div
|
||||||
|
className="text-sm cursor-pointer px-3 py-1 rounded flex flex-row justify-center items-center border border-blue-600 text-blue-600 bg-blue-50 hover:opacity-80"
|
||||||
|
onClick={handleConfirmBtnClick}
|
||||||
|
>
|
||||||
|
<Icon.PlusSquare className=" w-4 h-auto mr-1" />
|
||||||
|
<span>{t("common.confirm")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function showResourcesSelectorDialog() {
|
||||||
|
generateDialog(
|
||||||
|
{
|
||||||
|
className: "resources-selector-dialog",
|
||||||
|
},
|
||||||
|
ResourcesSelectorDialog,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -34,7 +34,14 @@ const SearchBar = () => {
|
|||||||
<div className="search-bar-container">
|
<div className="search-bar-container">
|
||||||
<div className="search-bar-inputer">
|
<div className="search-bar-inputer">
|
||||||
<Icon.Search className="icon-img" />
|
<Icon.Search className="icon-img" />
|
||||||
<input className="text-input" type="text" placeholder="" value={queryText} onChange={handleTextQueryInput} />
|
<input
|
||||||
|
className="text-input"
|
||||||
|
autoComplete="new-password"
|
||||||
|
type="text"
|
||||||
|
placeholder=""
|
||||||
|
value={queryText}
|
||||||
|
onChange={handleTextQueryInput}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="quickly-action-wrapper">
|
<div className="quickly-action-wrapper">
|
||||||
<div className="quickly-action-container">
|
<div className="quickly-action-container">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import * as api from "../../helpers/api";
|
|||||||
import toastHelper from "../Toast";
|
import toastHelper from "../Toast";
|
||||||
import Dropdown from "../common/Dropdown";
|
import Dropdown from "../common/Dropdown";
|
||||||
import { showCommonDialog } from "../Dialog/CommonDialog";
|
import { showCommonDialog } from "../Dialog/CommonDialog";
|
||||||
|
import showChangeMemberPasswordDialog from "../ChangeMemberPasswordDialog";
|
||||||
import "../../less/settings/member-section.less";
|
import "../../less/settings/member-section.less";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@@ -60,7 +61,6 @@ const PreferencesSection = () => {
|
|||||||
try {
|
try {
|
||||||
await api.createUser(userCreate);
|
await api.createUser(userCreate);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
|
||||||
toastHelper.error(error.response.data.message);
|
toastHelper.error(error.response.data.message);
|
||||||
}
|
}
|
||||||
await fetchUserList();
|
await fetchUserList();
|
||||||
@@ -70,6 +70,10 @@ const PreferencesSection = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChangePasswordClick = (user: User) => {
|
||||||
|
showChangeMemberPasswordDialog(user);
|
||||||
|
};
|
||||||
|
|
||||||
const handleArchiveUserClick = (user: User) => {
|
const handleArchiveUserClick = (user: User) => {
|
||||||
showCommonDialog({
|
showCommonDialog({
|
||||||
title: `Archive Member`,
|
title: `Archive Member`,
|
||||||
@@ -113,17 +117,33 @@ const PreferencesSection = () => {
|
|||||||
<div className="create-member-container">
|
<div className="create-member-container">
|
||||||
<div className="input-form-container">
|
<div className="input-form-container">
|
||||||
<span className="field-text">{t("common.username")}</span>
|
<span className="field-text">{t("common.username")}</span>
|
||||||
<input type="text" placeholder={t("common.username")} value={state.createUserUsername} onChange={handleUsernameInputChange} />
|
<input
|
||||||
|
type="text"
|
||||||
|
autoComplete="new-password"
|
||||||
|
placeholder={t("common.username")}
|
||||||
|
value={state.createUserUsername}
|
||||||
|
onChange={handleUsernameInputChange}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="input-form-container">
|
<div className="input-form-container">
|
||||||
<span className="field-text">{t("common.password")}</span>
|
<span className="field-text">{t("common.password")}</span>
|
||||||
<input type="text" placeholder={t("common.password")} value={state.createUserPassword} onChange={handlePasswordInputChange} />
|
<input
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
placeholder={t("common.password")}
|
||||||
|
value={state.createUserPassword}
|
||||||
|
onChange={handlePasswordInputChange}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="btns-container">
|
<div className="btns-container">
|
||||||
<button onClick={handleCreateUserBtnClick}>{t("common.create")}</button>
|
<button className="btn-normal" onClick={handleCreateUserBtnClick}>
|
||||||
|
{t("common.create")}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="title-text">{t("setting.member-list")}</p>
|
<div className="w-full flex flex-row justify-between items-center">
|
||||||
|
<div className="title-text">{t("setting.member-list")}</div>
|
||||||
|
</div>
|
||||||
<div className="member-container field-container">
|
<div className="member-container field-container">
|
||||||
<span className="field-text">ID</span>
|
<span className="field-text">ID</span>
|
||||||
<span className="field-text username-field">{t("common.username")}</span>
|
<span className="field-text username-field">{t("common.username")}</span>
|
||||||
@@ -138,12 +158,17 @@ const PreferencesSection = () => {
|
|||||||
<span className="tip-text">{t("common.yourself")}</span>
|
<span className="tip-text">{t("common.yourself")}</span>
|
||||||
) : (
|
) : (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
actionsClassName="!w-24"
|
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
|
<button
|
||||||
|
className="w-full text-left text-sm whitespace-nowrap leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
|
||||||
|
onClick={() => handleChangePasswordClick(user)}
|
||||||
|
>
|
||||||
|
{t("setting.account-section.change-password")}
|
||||||
|
</button>
|
||||||
{user.rowStatus === "NORMAL" ? (
|
{user.rowStatus === "NORMAL" ? (
|
||||||
<button
|
<button
|
||||||
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100"
|
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
|
||||||
onClick={() => handleArchiveUserClick(user)}
|
onClick={() => handleArchiveUserClick(user)}
|
||||||
>
|
>
|
||||||
{t("common.archive")}
|
{t("common.archive")}
|
||||||
@@ -151,13 +176,13 @@ const PreferencesSection = () => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100"
|
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-zinc-600"
|
||||||
onClick={() => handleRestoreUserClick(user)}
|
onClick={() => handleRestoreUserClick(user)}
|
||||||
>
|
>
|
||||||
{t("common.restore")}
|
{t("common.restore")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded text-red-600 hover:bg-gray-100"
|
className="w-full text-left text-sm leading-6 py-1 px-3 cursor-pointer rounded text-red-600 hover:bg-gray-100 dark:hover:bg-zinc-600"
|
||||||
onClick={() => handleDeleteUserClick(user)}
|
onClick={() => handleDeleteUserClick(user)}
|
||||||
>
|
>
|
||||||
{t("common.delete")}
|
{t("common.delete")}
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ const MyAccountSection = () => {
|
|||||||
<div className="flex flex-row justify-start items-center text-base text-gray-600">{user.email}</div>
|
<div className="flex flex-row justify-start items-center text-base text-gray-600">{user.email}</div>
|
||||||
<div className="w-full flex flex-row justify-start items-center mt-2 space-x-2">
|
<div className="w-full flex flex-row justify-start items-center mt-2 space-x-2">
|
||||||
<button className="btn-normal" onClick={showUpdateAccountDialog}>
|
<button className="btn-normal" onClick={showUpdateAccountDialog}>
|
||||||
Update Information
|
{t("setting.account-section.update-information")}
|
||||||
</button>
|
</button>
|
||||||
<button className="btn-normal" onClick={showChangePasswordDialog}>
|
<button className="btn-normal" onClick={showChangePasswordDialog}>
|
||||||
Change Password
|
{t("setting.account-section.change-password")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
|
import { Select, Switch, Option } from "@mui/joy";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Switch from "@mui/joy/Switch";
|
|
||||||
import { globalService, userService } from "../../services";
|
import { globalService, userService } from "../../services";
|
||||||
import { useAppSelector } from "../../store";
|
import { useAppSelector } from "../../store";
|
||||||
import {
|
import { VISIBILITY_SELECTOR_ITEMS, MEMO_DISPLAY_TS_OPTION_SELECTOR_ITEMS } from "../../helpers/consts";
|
||||||
VISIBILITY_SELECTOR_ITEMS,
|
import Icon from "../Icon";
|
||||||
MEMO_DISPLAY_TS_OPTION_SELECTOR_ITEMS,
|
import AppearanceSelect from "../AppearanceSelect";
|
||||||
SETTING_IS_FOLDING_ENABLED_KEY,
|
|
||||||
IS_FOLDING_ENABLED_DEFAULT_VALUE,
|
|
||||||
} from "../../helpers/consts";
|
|
||||||
import useLocalStorage from "../../hooks/useLocalStorage";
|
|
||||||
import Selector from "../common/Selector";
|
|
||||||
import "../../less/settings/preferences-section.less";
|
import "../../less/settings/preferences-section.less";
|
||||||
|
|
||||||
const localeSelectorItems = [
|
const localeSelectorItems = [
|
||||||
@@ -29,11 +24,23 @@ const localeSelectorItems = [
|
|||||||
text: "French",
|
text: "French",
|
||||||
value: "fr",
|
value: "fr",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: "Nederlands",
|
||||||
|
value: "nl",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Svenska",
|
||||||
|
value: "sv",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "German",
|
||||||
|
value: "de",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const PreferencesSection = () => {
|
const PreferencesSection = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setting } = useAppSelector((state) => state.user.user as User);
|
const { setting, localSetting } = useAppSelector((state) => state.user.user as User);
|
||||||
const visibilitySelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => {
|
const visibilitySelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => {
|
||||||
return {
|
return {
|
||||||
value: item.value,
|
value: item.value,
|
||||||
@@ -48,8 +55,6 @@ const PreferencesSection = () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isFoldingEnabled, setIsFoldingEnabled] = useLocalStorage(SETTING_IS_FOLDING_ENABLED_KEY, IS_FOLDING_ENABLED_DEFAULT_VALUE);
|
|
||||||
|
|
||||||
const handleLocaleChanged = async (value: string) => {
|
const handleLocaleChanged = async (value: string) => {
|
||||||
await userService.upsertUserSetting("locale", value);
|
await userService.upsertUserSetting("locale", value);
|
||||||
globalService.setLocale(value as Locale);
|
globalService.setLocale(value as Locale);
|
||||||
@@ -64,38 +69,75 @@ const PreferencesSection = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleIsFoldingEnabledChanged = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleIsFoldingEnabledChanged = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setIsFoldingEnabled(event.target.checked);
|
userService.upsertLocalSetting("enableFoldMemo", event.target.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="section-container preferences-section-container">
|
<div className="section-container preferences-section-container">
|
||||||
<p className="title-text">{t("common.basic")}</p>
|
<p className="title-text">{t("common.basic")}</p>
|
||||||
<label className="form-label selector">
|
<div className="form-label selector">
|
||||||
<span className="normal-text">{t("common.language")}</span>
|
<span className="normal-text">{t("common.language")}</span>
|
||||||
<Selector className="ml-2 w-32" value={setting.locale} dataSource={localeSelectorItems} handleValueChanged={handleLocaleChanged} />
|
<Select
|
||||||
</label>
|
className="!min-w-[10rem] w-auto text-sm"
|
||||||
|
value={setting.locale}
|
||||||
|
onChange={(_, locale) => {
|
||||||
|
if (locale) {
|
||||||
|
handleLocaleChanged(locale);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
startDecorator={<Icon.Globe className="w-4 h-auto" />}
|
||||||
|
>
|
||||||
|
{localeSelectorItems.map((item) => (
|
||||||
|
<Option key={item.value} value={item.value} className="whitespace-nowrap">
|
||||||
|
{item.text}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="form-label selector">
|
||||||
|
<span className="normal-text">Theme</span>
|
||||||
|
<AppearanceSelect />
|
||||||
|
</div>
|
||||||
<p className="title-text">{t("setting.preference")}</p>
|
<p className="title-text">{t("setting.preference")}</p>
|
||||||
<label className="form-label selector">
|
<div className="form-label selector">
|
||||||
<span className="normal-text">{t("setting.preference-section.default-memo-visibility")}</span>
|
<span className="normal-text">{t("setting.preference-section.default-memo-visibility")}</span>
|
||||||
<Selector
|
<Select
|
||||||
className="ml-2 w-32"
|
className="!min-w-[10rem] w-auto text-sm"
|
||||||
value={setting.memoVisibility}
|
value={setting.memoVisibility}
|
||||||
dataSource={visibilitySelectorItems}
|
onChange={(_, visibility) => {
|
||||||
handleValueChanged={handleDefaultMemoVisibilityChanged}
|
if (visibility) {
|
||||||
/>
|
handleDefaultMemoVisibilityChanged(visibility);
|
||||||
</label>
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{visibilitySelectorItems.map((item) => (
|
||||||
|
<Option key={item.value} value={item.value} className="whitespace-nowrap">
|
||||||
|
{item.text}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
<label className="form-label selector">
|
<label className="form-label selector">
|
||||||
<span className="normal-text">{t("setting.preference-section.default-memo-sort-option")}</span>
|
<span className="normal-text">{t("setting.preference-section.default-memo-sort-option")}</span>
|
||||||
<Selector
|
<Select
|
||||||
className="ml-2 w-32"
|
className="!min-w-[10rem] w-auto text-sm"
|
||||||
value={setting.memoDisplayTsOption}
|
value={setting.memoDisplayTsOption}
|
||||||
dataSource={memoDisplayTsOptionSelectorItems}
|
onChange={(_, value) => {
|
||||||
handleValueChanged={handleMemoDisplayTsOptionChanged}
|
if (value) {
|
||||||
/>
|
handleMemoDisplayTsOptionChanged(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{memoDisplayTsOptionSelectorItems.map((item) => (
|
||||||
|
<Option key={item.value} value={item.value} className="whitespace-nowrap">
|
||||||
|
{item.text}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
</label>
|
</label>
|
||||||
<label className="form-label selector">
|
<label className="form-label selector">
|
||||||
<span className="normal-text">{t("setting.preference-section.enable-folding-memo")}</span>
|
<span className="normal-text">{t("setting.preference-section.enable-folding-memo")}</span>
|
||||||
<Switch className="ml-2" checked={isFoldingEnabled} onChange={handleIsFoldingEnabledChanged} />
|
<Switch className="ml-2" checked={localSetting.enableFoldMemo} onChange={handleIsFoldingEnabledChanged} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -60,6 +60,23 @@ const SystemSection = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleVacuumBtnClick = async () => {
|
||||||
|
try {
|
||||||
|
await api.vacuumDatabase();
|
||||||
|
const { data: status } = (await api.getSystemStatus()).data;
|
||||||
|
setState({
|
||||||
|
dbSize: status.dbSize,
|
||||||
|
allowSignUp: status.allowSignUp,
|
||||||
|
additionalStyle: status.additionalStyle,
|
||||||
|
additionalScript: status.additionalScript,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toastHelper.success("Succeed to vacuum database");
|
||||||
|
};
|
||||||
|
|
||||||
const handleSaveAdditionalStyle = async () => {
|
const handleSaveAdditionalStyle = async () => {
|
||||||
try {
|
try {
|
||||||
await api.upsertSystemSetting({
|
await api.upsertSystemSetting({
|
||||||
@@ -96,19 +113,20 @@ const SystemSection = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="section-container system-section-container">
|
<div className="section-container system-section-container">
|
||||||
<p className="title-text">{t("common.basic")}</p>
|
<p className="title-text">{t("common.basic")}</p>
|
||||||
<p className="text-value">
|
<label className="form-label">
|
||||||
{t("setting.system-section.database-file-size")}: <span className="font-mono font-medium">{formatBytes(state.dbSize)}</span>
|
<span className="normal-text">
|
||||||
</p>
|
{t("setting.system-section.database-file-size")}: <span className="font-mono font-medium">{formatBytes(state.dbSize)}</span>
|
||||||
|
</span>
|
||||||
|
<Button onClick={handleVacuumBtnClick}>{t("common.vacuum")}</Button>
|
||||||
|
</label>
|
||||||
<p className="title-text">{t("sidebar.setting")}</p>
|
<p className="title-text">{t("sidebar.setting")}</p>
|
||||||
<label className="form-label">
|
<label className="form-label">
|
||||||
<span className="normal-text">{t("setting.system-section.allow-user-signup")}</span>
|
<span className="normal-text">{t("setting.system-section.allow-user-signup")}</span>
|
||||||
<Switch size="sm" checked={state.allowSignUp} onChange={(event) => handleAllowSignUpChanged(event.target.checked)} />
|
<Switch checked={state.allowSignUp} onChange={(event) => handleAllowSignUpChanged(event.target.checked)} />
|
||||||
</label>
|
</label>
|
||||||
<div className="form-label">
|
<div className="form-label">
|
||||||
<span className="normal-text">{t("setting.system-section.additional-style")}</span>
|
<span className="normal-text">{t("setting.system-section.additional-style")}</span>
|
||||||
<Button size="sm" onClick={handleSaveAdditionalStyle}>
|
<Button onClick={handleSaveAdditionalStyle}>{t("common.save")}</Button>
|
||||||
{t("common.save")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<Textarea
|
<Textarea
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -117,25 +135,24 @@ const SystemSection = () => {
|
|||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
}}
|
}}
|
||||||
minRows={4}
|
minRows={4}
|
||||||
maxRows={10}
|
maxRows={4}
|
||||||
placeholder={t("setting.system-section.additional-style-placeholder")}
|
placeholder={t("setting.system-section.additional-style-placeholder")}
|
||||||
value={state.additionalStyle}
|
value={state.additionalStyle}
|
||||||
onChange={(event) => handleAdditionalStyleChanged(event.target.value)}
|
onChange={(event) => handleAdditionalStyleChanged(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<div className="form-label mt-2">
|
<div className="form-label mt-2">
|
||||||
<span className="normal-text">{t("setting.system-section.additional-script")}</span>
|
<span className="normal-text">{t("setting.system-section.additional-script")}</span>
|
||||||
<Button size="sm" onClick={handleSaveAdditionalScript}>
|
<Button onClick={handleSaveAdditionalScript}>{t("common.save")}</Button>
|
||||||
{t("common.save")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<Textarea
|
<Textarea
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
color="neutral"
|
||||||
sx={{
|
sx={{
|
||||||
fontFamily: "monospace",
|
fontFamily: "monospace",
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
}}
|
}}
|
||||||
minRows={4}
|
minRows={4}
|
||||||
maxRows={10}
|
maxRows={4}
|
||||||
placeholder={t("setting.system-section.additional-script-placeholder")}
|
placeholder={t("setting.system-section.additional-script-placeholder")}
|
||||||
value={state.additionalScript}
|
value={state.additionalScript}
|
||||||
onChange={(event) => handleAdditionalScriptChanged(event.target.value)}
|
onChange={(event) => handleAdditionalScriptChanged(event.target.value)}
|
||||||
|
|||||||
195
web/src/components/ShareMemoDialog.tsx
Normal file
195
web/src/components/ShareMemoDialog.tsx
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import { Select, Option } from "@mui/joy";
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import { toLower } from "lodash";
|
||||||
|
import toImage from "../labs/html2image";
|
||||||
|
import { VISIBILITY_SELECTOR_ITEMS } from "../helpers/consts";
|
||||||
|
import * as utils from "../helpers/utils";
|
||||||
|
import { getMemoStats } from "../helpers/api";
|
||||||
|
import { memoService, userService } from "../services";
|
||||||
|
import useLoading from "../hooks/useLoading";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
import { generateDialog } from "./Dialog";
|
||||||
|
import toastHelper from "./Toast";
|
||||||
|
import MemoContent from "./MemoContent";
|
||||||
|
import MemoResources from "./MemoResources";
|
||||||
|
import "../less/share-memo-dialog.less";
|
||||||
|
|
||||||
|
interface Props extends DialogProps {
|
||||||
|
memo: Memo;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
memoAmount: number;
|
||||||
|
memoVisibility: string;
|
||||||
|
generatedImgUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShareMemoDialog: React.FC<Props> = (props: Props) => {
|
||||||
|
const { memo: propsMemo, destroy } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const user = userService.getState().user as User;
|
||||||
|
const [state, setState] = useState<State>({
|
||||||
|
memoAmount: 0,
|
||||||
|
memoVisibility: propsMemo.visibility,
|
||||||
|
generatedImgUrl: "",
|
||||||
|
});
|
||||||
|
const loadingState = useLoading();
|
||||||
|
const memoElRef = useRef<HTMLDivElement>(null);
|
||||||
|
const memo = {
|
||||||
|
...propsMemo,
|
||||||
|
createdAtStr: utils.getDateTimeString(propsMemo.displayTs),
|
||||||
|
};
|
||||||
|
const createdDays = Math.ceil((Date.now() - utils.getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getMemoStats(user.id)
|
||||||
|
.then(({ data: { data } }) => {
|
||||||
|
setState((state) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
memoAmount: data.length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
loadingState.setFinish();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loadingState.isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!memoElRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toImage(memoElRef.current, {
|
||||||
|
pixelRatio: window.devicePixelRatio * 2,
|
||||||
|
})
|
||||||
|
.then((url) => {
|
||||||
|
setState((state) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
generatedImgUrl: url,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}, [loadingState.isLoading]);
|
||||||
|
|
||||||
|
const handleCloseBtnClick = () => {
|
||||||
|
destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadBtnClick = () => {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = state.generatedImgUrl;
|
||||||
|
a.download = `memos-${utils.getDateTimeString(Date.now())}.png`;
|
||||||
|
a.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyLinkBtnClick = () => {
|
||||||
|
copy(`${window.location.origin}/m/${memo.id}`);
|
||||||
|
toastHelper.success(t("message.succeed-copy-content"));
|
||||||
|
};
|
||||||
|
|
||||||
|
const memoVisibilityOptionSelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => {
|
||||||
|
return {
|
||||||
|
value: item.value,
|
||||||
|
text: t(`memo.visibility.${toLower(item.value)}`),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleMemoVisibilityOptionChanged = async (value: string) => {
|
||||||
|
const visibilityValue = value as Visibility;
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
memoVisibility: visibilityValue,
|
||||||
|
});
|
||||||
|
await memoService.patchMemo({
|
||||||
|
id: memo.id,
|
||||||
|
visibility: visibilityValue,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="dialog-header-container">
|
||||||
|
<p className="title-text">
|
||||||
|
<span className="icon-text">🌄</span>
|
||||||
|
{t("common.share")} Memo
|
||||||
|
</p>
|
||||||
|
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||||
|
<Icon.X className="icon-img" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="dialog-content-container">
|
||||||
|
<div className="memo-container" ref={memoElRef}>
|
||||||
|
{state.generatedImgUrl !== "" && <img className="memo-shortcut-img" src={state.generatedImgUrl} />}
|
||||||
|
<span className="time-text">{memo.createdAtStr}</span>
|
||||||
|
<div className="memo-content-wrapper">
|
||||||
|
<MemoContent content={memo.content} displayConfig={{ enableExpand: false }} />
|
||||||
|
<MemoResources style="col" resourceList={memo.resourceList} />
|
||||||
|
</div>
|
||||||
|
<div className="watermark-container">
|
||||||
|
<div className="userinfo-container">
|
||||||
|
<span className="name-text">{user.nickname || user.username}</span>
|
||||||
|
<span className="usage-text">
|
||||||
|
{state.memoAmount} MEMOS / {createdDays} DAYS
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<img className="logo-img" src="/logo.webp" alt="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="px-4 py-3 w-full flex flex-row justify-between items-center">
|
||||||
|
<Select
|
||||||
|
className="!min-w-[10rem] w-auto text-sm"
|
||||||
|
value={state.memoVisibility}
|
||||||
|
onChange={(_, visibility) => {
|
||||||
|
if (visibility) {
|
||||||
|
handleMemoVisibilityOptionChanged(visibility);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{memoVisibilityOptionSelectorItems.map((item) => (
|
||||||
|
<Option key={item.value} value={item.value} className="whitespace-nowrap">
|
||||||
|
{item.text}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<div className="flex flex-row justify-end items-center">
|
||||||
|
<button disabled={state.generatedImgUrl === ""} className="btn-normal mr-2" onClick={handleDownloadBtnClick}>
|
||||||
|
{state.generatedImgUrl === "" ? (
|
||||||
|
<Icon.Loader className="w-4 h-auto mr-1 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Icon.Download className="w-4 h-auto mr-1" />
|
||||||
|
)}
|
||||||
|
<span>{t("common.image")}</span>
|
||||||
|
</button>
|
||||||
|
<button className="btn-normal" onClick={handleCopyLinkBtnClick}>
|
||||||
|
<Icon.Link className="w-4 h-auto mr-1" />
|
||||||
|
<span>{t("common.link")}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function showShareMemoDialog(memo: Memo): void {
|
||||||
|
generateDialog(
|
||||||
|
{
|
||||||
|
className: "share-memo-dialog",
|
||||||
|
},
|
||||||
|
ShareMemoDialog,
|
||||||
|
{ memo }
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { userService } from "../services";
|
|
||||||
import toImage from "../labs/html2image";
|
|
||||||
import { ANIMATION_DURATION } from "../helpers/consts";
|
|
||||||
import * as utils from "../helpers/utils";
|
|
||||||
import { getMemoStats } from "../helpers/api";
|
|
||||||
import useLoading from "../hooks/useLoading";
|
|
||||||
import Icon from "./Icon";
|
|
||||||
import { generateDialog } from "./Dialog";
|
|
||||||
import MemoContent from "./MemoContent";
|
|
||||||
import MemoResources from "./MemoResources";
|
|
||||||
import "../less/share-memo-image-dialog.less";
|
|
||||||
|
|
||||||
interface Props extends DialogProps {
|
|
||||||
memo: Memo;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
memoAmount: number;
|
|
||||||
shortcutImgUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ShareMemoImageDialog: React.FC<Props> = (props: Props) => {
|
|
||||||
const { memo: propsMemo, destroy } = props;
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const user = userService.getState().user as User;
|
|
||||||
const [state, setState] = useState<State>({
|
|
||||||
memoAmount: 0,
|
|
||||||
shortcutImgUrl: "",
|
|
||||||
});
|
|
||||||
const loadingState = useLoading();
|
|
||||||
const memoElRef = useRef<HTMLDivElement>(null);
|
|
||||||
const memo = {
|
|
||||||
...propsMemo,
|
|
||||||
createdAtStr: utils.getDateTimeString(propsMemo.displayTs),
|
|
||||||
};
|
|
||||||
const createdDays = Math.ceil((Date.now() - utils.getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getMemoStats(user.id)
|
|
||||||
.then(({ data: { data } }) => {
|
|
||||||
setState((state) => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
memoAmount: data.length,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
loadingState.setFinish();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (loadingState.isLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!memoElRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toImage(memoElRef.current, {
|
|
||||||
backgroundColor: "#eaeaea",
|
|
||||||
pixelRatio: window.devicePixelRatio * 2,
|
|
||||||
})
|
|
||||||
.then((url) => {
|
|
||||||
setState((state) => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
shortcutImgUrl: url,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
}, ANIMATION_DURATION);
|
|
||||||
}, [loadingState.isLoading]);
|
|
||||||
|
|
||||||
const handleCloseBtnClick = () => {
|
|
||||||
destroy();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDownloadBtnClick = () => {
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.href = state.shortcutImgUrl;
|
|
||||||
a.download = `memos-${utils.getDateTimeString(Date.now())}.png`;
|
|
||||||
a.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="dialog-header-container">
|
|
||||||
<p className="title-text">
|
|
||||||
<span className="icon-text">🌄</span>
|
|
||||||
{t("common.share")} Memo
|
|
||||||
</p>
|
|
||||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
|
||||||
<Icon.X className="icon-img" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="dialog-content-container">
|
|
||||||
<div className={`tip-words-container ${state.shortcutImgUrl ? "finish" : "loading"}`}>
|
|
||||||
<p className="tip-text">
|
|
||||||
{state.shortcutImgUrl ? t("message.click-to-save-the-image") + " 👇" : t("message.generating-the-screenshot")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="memo-container" ref={memoElRef}>
|
|
||||||
{state.shortcutImgUrl !== "" && <img className="memo-shortcut-img" onClick={handleDownloadBtnClick} src={state.shortcutImgUrl} />}
|
|
||||||
<span className="time-text">{memo.createdAtStr}</span>
|
|
||||||
<div className="memo-content-wrapper">
|
|
||||||
<MemoContent content={memo.content} displayConfig={{ enableExpand: false }} />
|
|
||||||
<MemoResources style="col" resourceList={memo.resourceList} />
|
|
||||||
</div>
|
|
||||||
<div className="watermark-container">
|
|
||||||
<div className="userinfo-container">
|
|
||||||
<span className="name-text">{user.nickname || user.username}</span>
|
|
||||||
<span className="usage-text">
|
|
||||||
{createdDays} DAYS / {state.memoAmount} MEMOS
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<img className="logo-img" src="/logo.webp" alt="" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function showShareMemoImageDialog(memo: Memo): void {
|
|
||||||
generateDialog(
|
|
||||||
{
|
|
||||||
className: "share-memo-image-dialog",
|
|
||||||
},
|
|
||||||
ShareMemoImageDialog,
|
|
||||||
{ memo }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -76,6 +76,10 @@ const ShortcutContainer: React.FC<ShortcutContainerProps> = (props: ShortcutCont
|
|||||||
if (showConfirmDeleteBtn) {
|
if (showConfirmDeleteBtn) {
|
||||||
try {
|
try {
|
||||||
await shortcutService.deleteShortcutById(shortcut.id);
|
await shortcutService.deleteShortcutById(shortcut.id);
|
||||||
|
if (locationService.getState().query?.shortcutId === shortcut.id) {
|
||||||
|
// need clear shortcut filter
|
||||||
|
locationService.setMemoShortcut(undefined);
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toastHelper.error(error.response.data.message);
|
toastHelper.error(error.response.data.message);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const Sidebar = () => {
|
|||||||
const location = useAppSelector((state) => state.location);
|
const location = useAppSelector((state) => state.location);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
toggleSiderbar(false);
|
toggleSidebar(false);
|
||||||
}, [location.query]);
|
}, [location.query]);
|
||||||
|
|
||||||
const handleSettingBtnClick = () => {
|
const handleSettingBtnClick = () => {
|
||||||
@@ -26,7 +26,7 @@ const Sidebar = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mask" onClick={() => toggleSiderbar(false)}></div>
|
<div className="mask" onClick={() => toggleSidebar(false)}></div>
|
||||||
<aside className="sidebar-wrapper">
|
<aside className="sidebar-wrapper">
|
||||||
<UserBanner />
|
<UserBanner />
|
||||||
<UsageHeatMap />
|
<UsageHeatMap />
|
||||||
@@ -52,7 +52,7 @@ const Sidebar = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toggleSiderbar = (show?: boolean) => {
|
export const toggleSidebar = (show?: boolean) => {
|
||||||
const sidebarEl = document.body.querySelector(".sidebar-wrapper") as HTMLDivElement;
|
const sidebarEl = document.body.querySelector(".sidebar-wrapper") as HTMLDivElement;
|
||||||
const maskEl = document.body.querySelector(".mask") as HTMLDivElement;
|
const maskEl = document.body.querySelector(".mask") as HTMLDivElement;
|
||||||
|
|
||||||
|
|||||||
@@ -14,22 +14,22 @@ type ToastItemProps = {
|
|||||||
type: ToastType;
|
type: ToastType;
|
||||||
content: string;
|
content: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
destory: FunctionType;
|
destroy: FunctionType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Toast: React.FC<ToastItemProps> = (props: ToastItemProps) => {
|
const Toast: React.FC<ToastItemProps> = (props: ToastItemProps) => {
|
||||||
const { destory, duration } = props;
|
const { destroy, duration } = props;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (duration > 0) {
|
if (duration > 0) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
destory();
|
destroy();
|
||||||
}, duration);
|
}, duration);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="toast-container" onClick={destory}>
|
<div className="toast-container" onClick={destroy}>
|
||||||
<p className="content-text">{props.content}</p>
|
<p className="content-text">{props.content}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -57,8 +57,8 @@ const initialToastHelper = () => {
|
|||||||
shownToastContainers.push([toast, tempDiv]);
|
shownToastContainers.push([toast, tempDiv]);
|
||||||
|
|
||||||
const cbs = {
|
const cbs = {
|
||||||
destory: () => {
|
destroy: () => {
|
||||||
tempDiv.classList.add("destory");
|
tempDiv.classList.add("destroy");
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!tempDiv.parentElement) {
|
if (!tempDiv.parentElement) {
|
||||||
@@ -77,7 +77,7 @@ const initialToastHelper = () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
toast.render(<Toast {...config} destory={cbs.destory} />);
|
toast.render(<Toast {...config} destroy={cbs.destroy} />);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
tempDiv.classList.add("showup");
|
tempDiv.classList.add("showup");
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ import { userService } from "../services";
|
|||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import { generateDialog } from "./Dialog";
|
import { generateDialog } from "./Dialog";
|
||||||
import toastHelper from "./Toast";
|
import toastHelper from "./Toast";
|
||||||
|
import { validate, ValidatorConfig } from "../helpers/validator";
|
||||||
|
|
||||||
|
const validateConfig: ValidatorConfig = {
|
||||||
|
minLength: 4,
|
||||||
|
maxLength: 320,
|
||||||
|
noSpace: true,
|
||||||
|
noChinese: true,
|
||||||
|
};
|
||||||
|
|
||||||
type Props = DialogProps;
|
type Props = DialogProps;
|
||||||
|
|
||||||
@@ -63,6 +71,12 @@ const UpdateAccountDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const usernameValidResult = validate(state.username, validateConfig);
|
||||||
|
if (!usernameValidResult.result) {
|
||||||
|
toastHelper.error(t("common.username") + ": " + t(usernameValidResult.reason as string));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = userService.getState().user as User;
|
const user = userService.getState().user as User;
|
||||||
const userPatch: UserPatch = {
|
const userPatch: UserPatch = {
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ const tableConfig = {
|
|||||||
height: 7,
|
height: 7,
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInitialUsageStat = (usedDaysAmount: number, beginDayTimestemp: number): DailyUsageStat[] => {
|
const getInitialUsageStat = (usedDaysAmount: number, beginDayTimestamp: number): DailyUsageStat[] => {
|
||||||
const initialUsageStat: DailyUsageStat[] = [];
|
const initialUsageStat: DailyUsageStat[] = [];
|
||||||
for (let i = 1; i <= usedDaysAmount; i++) {
|
for (let i = 1; i <= usedDaysAmount; i++) {
|
||||||
initialUsageStat.push({
|
initialUsageStat.push({
|
||||||
timestamp: beginDayTimestemp + DAILY_TIMESTAMP * i,
|
timestamp: beginDayTimestamp + DAILY_TIMESTAMP * i,
|
||||||
count: 0,
|
count: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -32,21 +32,25 @@ const UsageHeatMap = () => {
|
|||||||
const todayDay = new Date(todayTimeStamp).getDay() + 1;
|
const todayDay = new Date(todayTimeStamp).getDay() + 1;
|
||||||
const nullCell = new Array(7 - todayDay).fill(0);
|
const nullCell = new Array(7 - todayDay).fill(0);
|
||||||
const usedDaysAmount = (tableConfig.width - 1) * tableConfig.height + todayDay;
|
const usedDaysAmount = (tableConfig.width - 1) * tableConfig.height + todayDay;
|
||||||
const beginDayTimestemp = todayTimeStamp - usedDaysAmount * DAILY_TIMESTAMP;
|
const beginDayTimestamp = todayTimeStamp - usedDaysAmount * DAILY_TIMESTAMP;
|
||||||
|
|
||||||
const { memos } = useAppSelector((state) => state.memo);
|
const { memos } = useAppSelector((state) => state.memo);
|
||||||
const [allStat, setAllStat] = useState<DailyUsageStat[]>(getInitialUsageStat(usedDaysAmount, beginDayTimestemp));
|
const [allStat, setAllStat] = useState<DailyUsageStat[]>(getInitialUsageStat(usedDaysAmount, beginDayTimestamp));
|
||||||
const [currentStat, setCurrentStat] = useState<DailyUsageStat | null>(null);
|
const [currentStat, setCurrentStat] = useState<DailyUsageStat | null>(null);
|
||||||
const containerElRef = useRef<HTMLDivElement>(null);
|
const containerElRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getMemoStats(userService.getCurrentUserId())
|
getMemoStats(userService.getCurrentUserId())
|
||||||
.then(({ data: { data } }) => {
|
.then(({ data: { data } }) => {
|
||||||
const newStat: DailyUsageStat[] = getInitialUsageStat(usedDaysAmount, beginDayTimestemp);
|
const newStat: DailyUsageStat[] = getInitialUsageStat(usedDaysAmount, beginDayTimestamp);
|
||||||
for (const record of data) {
|
for (const record of data) {
|
||||||
const index = (utils.getDateStampByDate(record * 1000) - beginDayTimestemp) / (1000 * 3600 * 24) - 1;
|
const index = (utils.getDateStampByDate(record * 1000) - beginDayTimestamp) / (1000 * 3600 * 24) - 1;
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
newStat[index].count += 1;
|
// because of dailight savings, some days may be 23 hours long instead of 24 hours long
|
||||||
|
// this causes the calculations to yield weird indices such as 40.93333333333
|
||||||
|
// rounding them may not give you the exact day on the heat map, but it's not too bad
|
||||||
|
const exactIndex = +index.toFixed(0);
|
||||||
|
newStat[exactIndex].count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setAllStat([...newStat]);
|
setAllStat([...newStat]);
|
||||||
@@ -64,6 +68,11 @@ const UsageHeatMap = () => {
|
|||||||
tempDiv.style.top = bounding.top - 2 + "px";
|
tempDiv.style.top = bounding.top - 2 + "px";
|
||||||
tempDiv.innerHTML = `${item.count} memos on <span className="date-text">${new Date(item.timestamp as number).toDateString()}</span>`;
|
tempDiv.innerHTML = `${item.count} memos on <span className="date-text">${new Date(item.timestamp as number).toDateString()}</span>`;
|
||||||
document.body.appendChild(tempDiv);
|
document.body.appendChild(tempDiv);
|
||||||
|
|
||||||
|
if (tempDiv.offsetLeft - tempDiv.clientWidth / 2 < 0) {
|
||||||
|
tempDiv.style.left = bounding.left + tempDiv.clientWidth * 0.4 + "px";
|
||||||
|
tempDiv.className += " offset-left";
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleUsageStatItemMouseLeave = useCallback(() => {
|
const handleUsageStatItemMouseLeave = useCallback(() => {
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ const UserBanner = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSignOutBtnClick = async () => {
|
const handleSignOutBtnClick = async () => {
|
||||||
userService.doSignOut().catch();
|
|
||||||
navigate("/auth");
|
navigate("/auth");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,20 +74,20 @@ const UserBanner = () => {
|
|||||||
{!isVisitorMode && user?.role === "HOST" ? <span className="tag">MOD</span> : null}
|
{!isVisitorMode && user?.role === "HOST" ? <span className="tag">MOD</span> : null}
|
||||||
</div>
|
</div>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
trigger={<Icon.MoreHorizontal className="ml-2 w-5 h-auto cursor-pointer" />}
|
trigger={<Icon.MoreHorizontal className="ml-2 w-5 h-auto cursor-pointer dark:text-gray-200" />}
|
||||||
actionsClassName="min-w-36"
|
actionsClassName="min-w-36"
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
{!userService.isVisitorMode() && (
|
{!userService.isVisitorMode() && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded hover:bg-gray-100"
|
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800"
|
||||||
onClick={handleResourcesBtnClick}
|
onClick={handleResourcesBtnClick}
|
||||||
>
|
>
|
||||||
<span className="mr-1">🌄</span> {t("sidebar.resources")}
|
<span className="mr-1">🌄</span> {t("sidebar.resources")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded hover:bg-gray-100"
|
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800"
|
||||||
onClick={handleArchivedBtnClick}
|
onClick={handleArchivedBtnClick}
|
||||||
>
|
>
|
||||||
<span className="mr-1">🗂</span> {t("sidebar.archived")}
|
<span className="mr-1">🗂</span> {t("sidebar.archived")}
|
||||||
@@ -96,14 +95,14 @@ const UserBanner = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded hover:bg-gray-100"
|
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800"
|
||||||
onClick={handleAboutBtnClick}
|
onClick={handleAboutBtnClick}
|
||||||
>
|
>
|
||||||
<span className="mr-1">🤠</span> {t("common.about")}
|
<span className="mr-1">🤠</span> {t("common.about")}
|
||||||
</button>
|
</button>
|
||||||
{!userService.isVisitorMode() && (
|
{!userService.isVisitorMode() && (
|
||||||
<button
|
<button
|
||||||
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded hover:bg-gray-100"
|
className="w-full px-3 whitespace-nowrap text-left leading-10 cursor-pointer rounded dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800"
|
||||||
onClick={handleSignOutBtnClick}
|
onClick={handleSignOutBtnClick}
|
||||||
>
|
>
|
||||||
<span className="mr-1">👋</span> {t("common.sign-out")}
|
<span className="mr-1">👋</span> {t("common.sign-out")}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import "../../less/common/date-picker.less";
|
|||||||
interface DatePickerProps {
|
interface DatePickerProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
datestamp: DateStamp;
|
datestamp: DateStamp;
|
||||||
handleDateStampChange: (datastamp: DateStamp) => void;
|
handleDateStampChange: (datestamp: DateStamp) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DatePicker: React.FC<DatePickerProps> = (props: DatePickerProps) => {
|
const DatePicker: React.FC<DatePickerProps> = (props: DatePickerProps) => {
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ interface Props {
|
|||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
actionsClassName?: string;
|
actionsClassName?: string;
|
||||||
|
positionClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Dropdown: React.FC<Props> = (props: Props) => {
|
const Dropdown: React.FC<Props> = (props: Props) => {
|
||||||
const { trigger, actions, className, actionsClassName } = props;
|
const { trigger, actions, className, actionsClassName, positionClassName } = props;
|
||||||
const [dropdownStatus, toggleDropdownStatus] = useToggle(false);
|
const [dropdownStatus, toggleDropdownStatus] = useToggle(false);
|
||||||
const dropdownWrapperRef = useRef<HTMLDivElement>(null);
|
const dropdownWrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -37,14 +38,14 @@ const Dropdown: React.FC<Props> = (props: Props) => {
|
|||||||
{trigger ? (
|
{trigger ? (
|
||||||
trigger
|
trigger
|
||||||
) : (
|
) : (
|
||||||
<button className="flex flex-row justify-center items-center border p-1 rounded shadow text-gray-600 cursor-pointer hover:opacity-80">
|
<button className="flex flex-row justify-center items-center border dark:border-zinc-700 p-1 rounded shadow text-gray-600 dark:text-gray-200 cursor-pointer hover:opacity-80">
|
||||||
<Icon.MoreHorizontal className="w-4 h-auto" />
|
<Icon.MoreHorizontal className="w-4 h-auto" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`w-auto mt-1 absolute top-full right-0 flex flex-col justify-start items-start bg-white z-1 border p-1 rounded-md shadow ${
|
className={`w-auto absolute flex flex-col justify-start items-start bg-white dark:bg-zinc-700 z-10 p-1 rounded-md shadow ${
|
||||||
actionsClassName ?? ""
|
actionsClassName ?? ""
|
||||||
} ${dropdownStatus ? "" : "!hidden"}`}
|
} ${dropdownStatus ? "" : "!hidden"} ${positionClassName ?? "top-full right-0 mt-1"}`}
|
||||||
>
|
>
|
||||||
{actions}
|
{actions}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const Selector: React.FC<Props> = (props: Props) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [showSelector, toggleSelectorStatus] = useToggle(false);
|
const [showSelector, toggleSelectorStatus] = useToggle(false);
|
||||||
|
|
||||||
const seletorElRef = useRef<HTMLDivElement>(null);
|
const selectorElRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
let currentItem = nullItem;
|
let currentItem = nullItem;
|
||||||
for (const d of dataSource) {
|
for (const d of dataSource) {
|
||||||
@@ -39,7 +39,7 @@ const Selector: React.FC<Props> = (props: Props) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showSelector) {
|
if (showSelector) {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (!seletorElRef.current?.contains(event.target as Node)) {
|
if (!selectorElRef.current?.contains(event.target as Node)) {
|
||||||
toggleSelectorStatus(false);
|
toggleSelectorStatus(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -63,7 +63,7 @@ const Selector: React.FC<Props> = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`selector-wrapper ${className ?? ""}`} ref={seletorElRef}>
|
<div className={`selector-wrapper ${className ?? ""}`} ref={selectorElRef}>
|
||||||
<div className={`current-value-container ${showSelector ? "active" : ""}`} onClick={handleCurrentValueClick}>
|
<div className={`current-value-container ${showSelector ? "active" : ""}`} onClick={handleCurrentValueClick}>
|
||||||
<span className="value-text">{currentItem.text}</span>
|
<span className="value-text">{currentItem.text}</span>
|
||||||
<span className="arrow-text">
|
<span className="arrow-text">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
body,
|
html,
|
||||||
html {
|
body {
|
||||||
@apply text-base;
|
@apply text-base dark:bg-zinc-800;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Noto Sans", "Noto Sans CJK SC", "Microsoft YaHei UI", "Microsoft YaHei",
|
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Noto Sans", "Noto Sans CJK SC", "Microsoft YaHei UI", "Microsoft YaHei",
|
||||||
"WenQuanYi Micro Hei", sans-serif, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
"WenQuanYi Micro Hei", sans-serif, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
|
||||||
"Noto Color Emoji";
|
"Noto Color Emoji";
|
||||||
@@ -3,33 +3,40 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
.hide-scrollbar {
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
/* Chrome, Safari and Opera */
|
/* Chrome, Safari and Opera */
|
||||||
.hide-scrollbar::-webkit-scrollbar {
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-scrollbar {
|
.word-break {
|
||||||
-ms-overflow-style: none; /* IE and Edge */
|
overflow-wrap: anywhere;
|
||||||
scrollbar-width: none; /* Firefox */
|
word-break: normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-normal {
|
@layer components {
|
||||||
@apply select-none inline-flex border cursor-pointer px-3 text-sm leading-8 rounded-md hover:opacity-80 hover:shadow;
|
.btn-normal {
|
||||||
}
|
@apply select-none flex flex-row justify-center items-center border dark:border-zinc-700 cursor-pointer px-3 text-sm leading-8 rounded-md hover:opacity-80 hover:shadow disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:shadow-none;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
@apply btn-normal border-transparent bg-green-600 text-white;
|
@apply btn-normal border-transparent bg-green-600 text-white dark:border-transparent dark:text-gray-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
@apply btn-normal border-red-600 bg-red-50 text-red-600;
|
@apply btn-normal border-red-600 bg-red-50 text-red-600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-text {
|
.btn-text {
|
||||||
@apply btn-normal text-gray-600 border-none hover:shadow-none;
|
@apply btn-normal text-gray-600 border-none dark:text-gray-200 hover:shadow-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-text {
|
.input-text {
|
||||||
@apply w-full px-3 py-2 leading-6 text-sm border rounded;
|
@apply w-full px-3 py-2 leading-6 text-sm dark:text-gray-200 rounded border focus:outline focus:outline-2 dark:border-zinc-700 dark:bg-zinc-800;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ export function upsertSystemSetting(systemSetting: SystemSetting) {
|
|||||||
return axios.post<ResponseObject<SystemSetting>>("/api/system/setting", systemSetting);
|
return axios.post<ResponseObject<SystemSetting>>("/api/system/setting", systemSetting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function vacuumDatabase() {
|
||||||
|
return axios.post("/api/system/vacuum");
|
||||||
|
}
|
||||||
|
|
||||||
export function signin(username: string, password: string) {
|
export function signin(username: string, password: string) {
|
||||||
return axios.post<ResponseObject<User>>("/api/auth/signin", {
|
return axios.post<ResponseObject<User>>("/api/auth/signin", {
|
||||||
username,
|
username,
|
||||||
|
|||||||
@@ -8,17 +8,14 @@ export const ANIMATION_DURATION = 200;
|
|||||||
export const DAILY_TIMESTAMP = 3600 * 24 * 1000;
|
export const DAILY_TIMESTAMP = 3600 * 24 * 1000;
|
||||||
|
|
||||||
export const VISIBILITY_SELECTOR_ITEMS = [
|
export const VISIBILITY_SELECTOR_ITEMS = [
|
||||||
{ text: "PUBLIC", value: "PUBLIC" },
|
|
||||||
{ text: "PROTECTED", value: "PROTECTED" },
|
|
||||||
{ text: "PRIVATE", value: "PRIVATE" },
|
{ text: "PRIVATE", value: "PRIVATE" },
|
||||||
|
{ text: "PROTECTED", value: "PROTECTED" },
|
||||||
|
{ text: "PUBLIC", value: "PUBLIC" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const MEMO_DISPLAY_TS_OPTION_SELECTOR_ITEMS = [
|
export const MEMO_DISPLAY_TS_OPTION_SELECTOR_ITEMS = [
|
||||||
{ text: "created_ts", value: "created_ts" },
|
{ text: "created_ts", value: "created_ts" },
|
||||||
{ text: "created_ts", value: "updated_ts" },
|
{ text: "updated_ts", value: "updated_ts" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const IS_FOLDING_ENABLED_DEFAULT_VALUE = true;
|
|
||||||
export const SETTING_IS_FOLDING_ENABLED_KEY = "setting_IS_FOLDING_ENABLED";
|
|
||||||
|
|
||||||
export const TAB_SPACE_WIDTH = 2;
|
export const TAB_SPACE_WIDTH = 2;
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ export const checkShouldShowMemo = (memo: Memo, filter: Filter) => {
|
|||||||
if (type === "TAG") {
|
if (type === "TAG") {
|
||||||
let contained = true;
|
let contained = true;
|
||||||
const tagsSet = new Set<string>();
|
const tagsSet = new Set<string>();
|
||||||
for (const t of Array.from(memo.content.match(TAG_REG) ?? [])) {
|
for (const t of Array.from(memo.content.match(new RegExp(TAG_REG, "g")) ?? [])) {
|
||||||
const tag = t.replace(TAG_REG, "$1").trim();
|
const tag = t.replace(TAG_REG, "$1").trim();
|
||||||
const items = tag.split("/");
|
const items = tag.split("/");
|
||||||
let temp = "";
|
let temp = "";
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ interface StorageData {
|
|||||||
editingMemoVisibilityCache: Visibility;
|
editingMemoVisibilityCache: Visibility;
|
||||||
// locale
|
// locale
|
||||||
locale: Locale;
|
locale: Locale;
|
||||||
|
// appearance
|
||||||
|
appearance: Appearance;
|
||||||
|
// local setting
|
||||||
|
localSetting: LocalSetting;
|
||||||
// skipped version
|
// skipped version
|
||||||
skippedVersion: string;
|
skippedVersion: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,3 +134,17 @@ export const parseHTMLToRawText = (htmlStr: string): string => {
|
|||||||
const text = tempEl.innerText;
|
const text = tempEl.innerText;
|
||||||
return text;
|
return text;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function absolutifyLink(rel: string): string {
|
||||||
|
const anchor = document.createElement("a");
|
||||||
|
anchor.setAttribute("href", rel);
|
||||||
|
return anchor.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSystemColorScheme() {
|
||||||
|
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||||
|
return "dark";
|
||||||
|
} else {
|
||||||
|
return "light";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Validator
|
// Validator
|
||||||
// * use for validating form data
|
// * use for validating form data
|
||||||
|
|
||||||
const chineseReg = /[\u3000\u3400-\u4DBF\u4E00-\u9FFF]/;
|
const chineseReg = /[\u3000\u3400-\u4DBF\u4E00-\u9FFF]/;
|
||||||
|
|
||||||
export interface ValidatorConfig {
|
export interface ValidatorConfig {
|
||||||
@@ -18,7 +19,7 @@ export function validate(text: string, config: Partial<ValidatorConfig>): { resu
|
|||||||
if (text.length < config.minLength) {
|
if (text.length < config.minLength) {
|
||||||
return {
|
return {
|
||||||
result: false,
|
result: false,
|
||||||
reason: "Too short",
|
reason: "message.too-short",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,7 +28,7 @@ export function validate(text: string, config: Partial<ValidatorConfig>): { resu
|
|||||||
if (text.length > config.maxLength) {
|
if (text.length > config.maxLength) {
|
||||||
return {
|
return {
|
||||||
result: false,
|
result: false,
|
||||||
reason: "Too long",
|
reason: "message.too-long",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,14 +36,14 @@ export function validate(text: string, config: Partial<ValidatorConfig>): { resu
|
|||||||
if (config.noSpace && text.includes(" ")) {
|
if (config.noSpace && text.includes(" ")) {
|
||||||
return {
|
return {
|
||||||
result: false,
|
result: false,
|
||||||
reason: "Don't allow space",
|
reason: "message.not-allow-space",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.noChinese && chineseReg.test(text)) {
|
if (config.noChinese && chineseReg.test(text)) {
|
||||||
return {
|
return {
|
||||||
result: false,
|
result: false,
|
||||||
reason: "Don't allow chinese",
|
reason: "message.not-allow-chinese",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
const useLocalStorage = <T>(key: string, initialValue: T) => {
|
|
||||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
||||||
try {
|
|
||||||
const item = window.localStorage.getItem(key);
|
|
||||||
return item ? JSON.parse(item) : initialValue;
|
|
||||||
} catch (error) {
|
|
||||||
return initialValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const setValue = (value: T | ((val: T) => T)) => {
|
|
||||||
try {
|
|
||||||
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
||||||
setStoredValue(valueToStore);
|
|
||||||
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return [storedValue, setValue] as const;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useLocalStorage;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
// 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;
|
|
||||||
@@ -5,7 +5,7 @@ const useToggle = (initialState = false): [boolean, (nextState?: boolean) => voi
|
|||||||
// Initialize the state
|
// Initialize the state
|
||||||
const [state, setState] = useState(initialState);
|
const [state, setState] = useState(initialState);
|
||||||
|
|
||||||
// Define and memorize toggler function in case we pass down the comopnent,
|
// Define and memorize toggler function in case we pass down the component,
|
||||||
// This function change the boolean value to it's opposite value
|
// This function change the boolean value to it's opposite value
|
||||||
const toggle = useCallback((nextState?: boolean) => {
|
const toggle = useCallback((nextState?: boolean) => {
|
||||||
if (nextState !== undefined) {
|
if (nextState !== undefined) {
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import enLocale from "./locales/en.json";
|
|||||||
import zhLocale from "./locales/zh.json";
|
import zhLocale from "./locales/zh.json";
|
||||||
import viLocale from "./locales/vi.json";
|
import viLocale from "./locales/vi.json";
|
||||||
import frLocale from "./locales/fr.json";
|
import frLocale from "./locales/fr.json";
|
||||||
|
import nlLocale from "./locales/nl.json";
|
||||||
|
import svLocale from "./locales/sv.json";
|
||||||
|
import deLocale from "./locales/de.json";
|
||||||
|
|
||||||
i18n.use(initReactI18next).init({
|
i18n.use(initReactI18next).init({
|
||||||
resources: {
|
resources: {
|
||||||
@@ -19,8 +22,17 @@ i18n.use(initReactI18next).init({
|
|||||||
fr: {
|
fr: {
|
||||||
translation: frLocale,
|
translation: frLocale,
|
||||||
},
|
},
|
||||||
|
nl: {
|
||||||
|
translation: nlLocale,
|
||||||
|
},
|
||||||
|
sv: {
|
||||||
|
translation: svLocale,
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
translation: deLocale,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
lng: "en",
|
lng: "nl",
|
||||||
fallbackLng: "en",
|
fallbackLng: "en",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,20 @@ const applyStyles = async (sourceElement: HTMLElement, clonedElement: HTMLElemen
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sourceElement.tagName === "IMG") {
|
if (sourceElement.tagName === "IMG") {
|
||||||
|
const url = sourceElement.getAttribute("src") ?? "";
|
||||||
|
let covertFailed = false;
|
||||||
try {
|
try {
|
||||||
const url = await convertResourceToDataURL(sourceElement.getAttribute("src") ?? "");
|
(clonedElement as HTMLImageElement).src = await convertResourceToDataURL(url);
|
||||||
(clonedElement as HTMLImageElement).src = url;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// do nth
|
covertFailed = true;
|
||||||
|
}
|
||||||
|
// NOTE: Get image blob from backend to avoid CORS error.
|
||||||
|
if (covertFailed) {
|
||||||
|
try {
|
||||||
|
(clonedElement as HTMLImageElement).src = await convertResourceToDataURL(`/o/get/image?url=${url}`);
|
||||||
|
} catch (error) {
|
||||||
|
// do nth
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ console.log("hello world!")
|
|||||||
- [ ] finish my homework
|
- [ ] finish my homework
|
||||||
- [x] yahaha`,
|
- [x] yahaha`,
|
||||||
want: `<p>My task:</p>
|
want: `<p>My task:</p>
|
||||||
<p><span class='todo-block todo' data-value='TODO'></span>finish my homework</p>
|
<p class='li-container'><span class='todo-block todo' data-value='TODO'></span>finish my homework</p>
|
||||||
<p><span class='todo-block done' data-value='DONE'>✓</span>yahaha</p>`,
|
<p class='li-container'><span class='todo-block done' data-value='DONE'>✓</span>yahaha</p>`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -76,8 +76,8 @@ console.log("hello world!")
|
|||||||
* list 123
|
* list 123
|
||||||
1. 123123`,
|
1. 123123`,
|
||||||
want: `<p>This is a list</p>
|
want: `<p>This is a list</p>
|
||||||
<p><span class='ul-block'>•</span>list 123</p>
|
<p class='li-container'><span class='ul-block'>•</span>list 123</p>
|
||||||
<p><span class='ol-block'>1.</span>123123</p>`,
|
<p class='li-container'><span class='ol-block'>1.</span>123123</p>`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -157,43 +157,6 @@ console.log("hello world!")
|
|||||||
expect(unescape(marked(t.markdown))).toBe(t.want);
|
expect(unescape(marked(t.markdown))).toBe(t.want);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
test("parse table", () => {
|
|
||||||
const tests = [
|
|
||||||
{
|
|
||||||
markdown: `text above the table
|
|
||||||
| a | b | c |
|
|
||||||
|---|---|---|
|
|
||||||
| 1 | 2 | 3 |
|
|
||||||
| 4 | 5 | 6 |
|
|
||||||
text below the table
|
|
||||||
`,
|
|
||||||
want: `<p>text above the table</p>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>a</th><th>b</th><th>c</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr><td>1</td><td>2</td><td>3</td></tr><tr><td>4</td><td>5</td><td>6</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p>text below the table</p>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
markdown: `| a | b | c |
|
|
||||||
| 1 | 2 | 3 |
|
|
||||||
| 4 | 5 | 6 |`,
|
|
||||||
want: `<p>| a | b | c |</p>
|
|
||||||
<p>| 1 | 2 | 3 |</p>
|
|
||||||
<p>| 4 | 5 | 6 |</p>`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
for (const t of tests) {
|
|
||||||
expect(unescape(marked(t.markdown))).toBe(t.want);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
test("parse full width space", () => {
|
test("parse full width space", () => {
|
||||||
const tests = [
|
const tests = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const renderer = (rawStr: string): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "blockqoute",
|
name: "blockquote",
|
||||||
regex: BLOCKQUOTE_REG,
|
regex: BLOCKQUOTE_REG,
|
||||||
renderer,
|
renderer,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const renderer = (rawStr: string): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parsedContent = marked(matchResult[1], [], inlineElementParserList);
|
const parsedContent = marked(matchResult[1], [], inlineElementParserList);
|
||||||
return `<p><span class='todo-block done' data-value='DONE'>✓</span>${parsedContent}</p>${matchResult[2]}`;
|
return `<p class='li-container'><span class='todo-block done' data-value='DONE'>✓</span>${parsedContent}</p>${matchResult[2]}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { escape } from "lodash-es";
|
import { escape } from "lodash-es";
|
||||||
|
import { absolutifyLink } from "../../../helpers/utils";
|
||||||
|
|
||||||
export const IMAGE_REG = /!\[.*?\]\((.+?)\)/;
|
export const IMAGE_REG = /!\[.*?\]\((.+?)\)/;
|
||||||
|
|
||||||
@@ -8,8 +9,8 @@ const renderer = (rawStr: string): string => {
|
|||||||
return rawStr;
|
return rawStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: Get image blob from backend to avoid CORS.
|
const imageUrl = absolutifyLink(escape(matchResult[1]));
|
||||||
return `<img class='img' src='/o/get/image?url=${escape(matchResult[1])}' />`;
|
return `<img class='img' src='${imageUrl}' />`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const renderer = (rawStr: string): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parsedContent = marked(matchResult[2], [], inlineElementParserList);
|
const parsedContent = marked(matchResult[2], [], inlineElementParserList);
|
||||||
return `<p><span class='ol-block'>${matchResult[1]}.</span>${parsedContent}</p>${matchResult[3]}`;
|
return `<p class='li-container'><span class='ol-block'>${matchResult[1]}.</span>${parsedContent}</p>${matchResult[3]}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
/**
|
|
||||||
* Match markdown table
|
|
||||||
* example:
|
|
||||||
* | a | b | c |
|
|
||||||
* |---|---|---|
|
|
||||||
* | 1 | 2 | 3 |
|
|
||||||
* | 4 | 5 | 6 |
|
|
||||||
*/
|
|
||||||
export const TABLE_REG = /^(\|.*\|)(?:(?:\n(?:\|-*)+\|))((?:\n\|.*\|)+)(\n?)/;
|
|
||||||
|
|
||||||
const renderer = (rawStr: string): string => {
|
|
||||||
const matchResult = rawStr.match(TABLE_REG);
|
|
||||||
if (!matchResult) {
|
|
||||||
return rawStr;
|
|
||||||
}
|
|
||||||
const tableHeader = matchResult[1]
|
|
||||||
.split("|")
|
|
||||||
.filter((str) => str !== "")
|
|
||||||
.map((str) => str.trim());
|
|
||||||
const tableBody = matchResult[2]
|
|
||||||
.trim()
|
|
||||||
.split("\n")
|
|
||||||
.map((str) =>
|
|
||||||
str
|
|
||||||
.split("|")
|
|
||||||
.filter((str) => str !== "")
|
|
||||||
.map((str) => str.trim())
|
|
||||||
);
|
|
||||||
return `<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
${tableHeader.map((str) => `<th>${str}</th>`).join("")}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
${tableBody.map((row) => `<tr>${row.map((str) => `<td>${str}</td>`).join("")}</tr>`).join("")}
|
|
||||||
</tbody>
|
|
||||||
</table>${matchResult[3]}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "table",
|
|
||||||
regex: TABLE_REG,
|
|
||||||
renderer,
|
|
||||||
};
|
|
||||||
@@ -11,7 +11,7 @@ const renderer = (rawStr: string): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parsedContent = marked(matchResult[1], [], inlineElementParserList);
|
const parsedContent = marked(matchResult[1], [], inlineElementParserList);
|
||||||
return `<p><span class='todo-block todo' data-value='TODO'></span>${parsedContent}</p>${escape(matchResult[2])}`;
|
return `<p class='li-container'><span class='todo-block todo' data-value='TODO'></span>${parsedContent}</p>${escape(matchResult[2])}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const renderer = (rawStr: string): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parsedContent = marked(matchResult[1], [], inlineElementParserList);
|
const parsedContent = marked(matchResult[1], [], inlineElementParserList);
|
||||||
return `<p><span class='ul-block'>•</span>${parsedContent}</p>${escape(matchResult[2])}`;
|
return `<p class='li-container'><span class='ul-block'>•</span>${parsedContent}</p>${escape(matchResult[2])}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import Emphasis from "./Emphasis";
|
|||||||
import PlainLink from "./PlainLink";
|
import PlainLink from "./PlainLink";
|
||||||
import InlineCode from "./InlineCode";
|
import InlineCode from "./InlineCode";
|
||||||
import PlainText from "./PlainText";
|
import PlainText from "./PlainText";
|
||||||
import Table from "./Table";
|
|
||||||
import BoldEmphasis from "./BoldEmphasis";
|
import BoldEmphasis from "./BoldEmphasis";
|
||||||
import Blockquote from "./Blockquote";
|
import Blockquote from "./Blockquote";
|
||||||
import HorizontalRules from "./HorizontalRules";
|
import HorizontalRules from "./HorizontalRules";
|
||||||
@@ -24,20 +23,9 @@ export { DONE_LIST_REG } from "./DoneList";
|
|||||||
export { TAG_REG } from "./Tag";
|
export { TAG_REG } from "./Tag";
|
||||||
export { IMAGE_REG } from "./Image";
|
export { IMAGE_REG } from "./Image";
|
||||||
export { LINK_REG } from "./Link";
|
export { LINK_REG } from "./Link";
|
||||||
export { TABLE_REG } from "./Table";
|
|
||||||
export { HORIZONTAL_RULES_REG } from "./HorizontalRules";
|
export { HORIZONTAL_RULES_REG } from "./HorizontalRules";
|
||||||
|
|
||||||
// The order determines the order of execution.
|
// The order determines the order of execution.
|
||||||
export const blockElementParserList = [
|
export const blockElementParserList = [HorizontalRules, CodeBlock, Blockquote, TodoList, DoneList, OrderedList, UnorderedList, Paragraph];
|
||||||
HorizontalRules,
|
|
||||||
Table,
|
|
||||||
CodeBlock,
|
|
||||||
Blockquote,
|
|
||||||
TodoList,
|
|
||||||
DoneList,
|
|
||||||
OrderedList,
|
|
||||||
UnorderedList,
|
|
||||||
Paragraph,
|
|
||||||
];
|
|
||||||
export const inlineElementParserList = [Image, BoldEmphasis, Bold, Emphasis, Link, InlineCode, PlainLink, Strikethrough, Tag, PlainText];
|
export const inlineElementParserList = [Image, BoldEmphasis, Bold, Emphasis, Link, InlineCode, PlainLink, Strikethrough, Tag, PlainText];
|
||||||
export const parserList = [...blockElementParserList, ...inlineElementParserList];
|
export const parserList = [...blockElementParserList, ...inlineElementParserList];
|
||||||
|
|||||||
@@ -1,31 +1,13 @@
|
|||||||
.about-site-dialog {
|
.about-site-dialog {
|
||||||
@apply px-4;
|
|
||||||
|
|
||||||
> .dialog-container {
|
> .dialog-container {
|
||||||
@apply w-112 max-w-full;
|
@apply w-112 max-w-full;
|
||||||
|
|
||||||
> .dialog-content-container {
|
> .dialog-content-container {
|
||||||
@apply flex flex-col justify-start items-start leading-relaxed;
|
@apply flex flex-col justify-start items-start;
|
||||||
|
|
||||||
> .logo-img {
|
|
||||||
@apply h-16;
|
|
||||||
}
|
|
||||||
|
|
||||||
> p {
|
> p {
|
||||||
@apply my-1;
|
@apply my-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pre-text {
|
|
||||||
@apply font-mono mx-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .addtion-info-container {
|
|
||||||
@apply flex flex-row text-sm justify-start items-center;
|
|
||||||
|
|
||||||
> .github-badge-container {
|
|
||||||
@apply mr-4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.page-wrapper.auth {
|
.page-wrapper.auth {
|
||||||
@apply flex flex-row justify-center items-center w-full h-screen bg-white;
|
@apply flex flex-row justify-center items-center w-full h-screen bg-white dark:bg-zinc-800;
|
||||||
|
|
||||||
> .page-container {
|
> .page-container {
|
||||||
@apply w-80 max-w-full h-full py-4 flex flex-col justify-start items-center;
|
@apply w-80 max-w-full h-full py-4 flex flex-col justify-start items-center;
|
||||||
@@ -11,15 +11,19 @@
|
|||||||
@apply flex flex-col justify-start items-start w-full mb-4;
|
@apply flex flex-col justify-start items-start w-full mb-4;
|
||||||
|
|
||||||
> .title-container {
|
> .title-container {
|
||||||
@apply w-full flex flex-row justify-between items-center;
|
@apply w-full flex flex-row justify-start items-center;
|
||||||
|
|
||||||
> .logo-img {
|
> .logo-img {
|
||||||
@apply h-20 w-auto;
|
@apply h-20 w-auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .logo-text {
|
||||||
|
@apply text-6xl tracking-wide text-black dark:text-gray-200;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .slogan-text {
|
> .slogan-text {
|
||||||
@apply text-sm text-gray-700;
|
@apply text-sm text-gray-700 dark:text-gray-300;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +37,7 @@
|
|||||||
@apply absolute top-3 left-3 px-1 leading-10 flex-shrink-0 text-base cursor-text text-gray-400 bg-transparent transition-all select-none;
|
@apply absolute top-3 left-3 px-1 leading-10 flex-shrink-0 text-base cursor-text text-gray-400 bg-transparent transition-all select-none;
|
||||||
|
|
||||||
&.not-null {
|
&.not-null {
|
||||||
@apply text-sm top-0 z-10 leading-4 bg-white rounded;
|
@apply text-sm top-0 z-10 leading-4 bg-white dark:bg-zinc-800 rounded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +45,7 @@
|
|||||||
@apply py-2;
|
@apply py-2;
|
||||||
|
|
||||||
> input {
|
> input {
|
||||||
@apply w-full py-3 px-3 text-base shadow-inner rounded-lg border border-solid border-gray-400 hover:opacity-80;
|
@apply w-full py-3 px-3 text-base rounded-lg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,20 +58,8 @@
|
|||||||
> .action-btns-container {
|
> .action-btns-container {
|
||||||
@apply flex flex-row justify-end items-center w-full mt-2;
|
@apply flex flex-row justify-end items-center w-full mt-2;
|
||||||
|
|
||||||
> .btn {
|
> .requesting {
|
||||||
@apply flex flex-row justify-center items-center px-1 py-2 text-sm rounded hover:opacity-80;
|
@apply cursor-wait opacity-80;
|
||||||
|
|
||||||
&.signup-btn {
|
|
||||||
@apply px-3;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.signin-btn {
|
|
||||||
@apply bg-green-600 text-white px-3 shadow;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.requesting {
|
|
||||||
@apply cursor-wait opacity-80;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,25 +71,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .footer-container {
|
|
||||||
@apply w-full flex flex-col justify-start items-center;
|
|
||||||
|
|
||||||
> .language-container {
|
|
||||||
@apply mt-2 w-full flex flex-row justify-center items-center text-sm text-gray-400;
|
|
||||||
|
|
||||||
> .locale-item {
|
|
||||||
@apply px-2 cursor-pointer;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
@apply text-blue-600 font-bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .split-line {
|
|
||||||
@apply font-mono text-gray-400;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.dialog-wrapper {
|
.dialog-wrapper {
|
||||||
@apply fixed top-0 left-0 flex flex-col justify-start items-center w-full h-full pt-16 pb-8 z-100 overflow-x-hidden overflow-y-scroll bg-transparent transition-all hide-scrollbar;
|
@apply fixed top-0 left-0 flex flex-col justify-start items-center w-full h-full pt-16 pb-8 px-4 z-100 overflow-x-hidden overflow-y-scroll bg-transparent transition-all hide-scrollbar;
|
||||||
|
|
||||||
&.showup {
|
&.showup {
|
||||||
background-color: rgba(0, 0, 0, 0.6);
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .dialog-container {
|
> .dialog-container {
|
||||||
@apply flex flex-col justify-start items-start bg-white p-4 rounded-lg;
|
@apply max-w-full flex flex-col justify-start items-start bg-white dark:bg-zinc-800 dark:text-gray-200 p-4 rounded-lg;
|
||||||
|
|
||||||
> .dialog-header-container {
|
> .dialog-header-container {
|
||||||
@apply flex flex-row justify-between items-center w-full mb-4;
|
@apply flex flex-row justify-between items-center w-full mb-4;
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
@apply flex flex-col justify-center items-center w-6 h-6 rounded hover:bg-gray-100 hover:shadow;
|
@apply flex flex-col justify-center items-center w-6 h-6 rounded hover:bg-gray-100 dark:hover:bg-zinc-700 hover:shadow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
7
web/src/less/code-highlight.less
Normal file
7
web/src/less/code-highlight.less
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
html.dark {
|
||||||
|
@import (less) "highlight.js/styles/atom-one-dark.css";
|
||||||
|
}
|
||||||
|
|
||||||
|
html:not(.dark) {
|
||||||
|
@import (less) "highlight.js/styles/github.css";
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
@apply flex flex-row justify-end items-center w-full mt-4;
|
@apply flex flex-row justify-end items-center w-full mt-4;
|
||||||
|
|
||||||
> .btn {
|
> .btn {
|
||||||
@apply text-sm py-1 px-3 mr-2 rounded-md hover:opacity-80;
|
@apply text-sm py-1 px-3 mr-2 rounded-md hover:opacity-80 cursor-pointer;
|
||||||
|
|
||||||
&.confirm-btn {
|
&.confirm-btn {
|
||||||
@apply bg-red-100 border border-solid border-blue-600 text-blue-600;
|
@apply bg-red-100 border border-solid border-blue-600 text-blue-600;
|
||||||
|
|||||||
@@ -22,10 +22,7 @@
|
|||||||
@apply flex flex-row justify-around items-center w-full;
|
@apply flex flex-row justify-around items-center w-full;
|
||||||
|
|
||||||
> .day-item {
|
> .day-item {
|
||||||
@apply flex flex-col justify-center items-center;
|
@apply w-9 h-9 select-none flex flex-col justify-center items-center;
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
user-select: none;
|
|
||||||
color: gray;
|
color: gray;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
@@ -33,21 +30,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .day-item {
|
> .day-item {
|
||||||
@apply flex flex-col justify-center items-center;
|
@apply w-9 h-9 rounded-full text-sm select-none cursor-pointer flex flex-col justify-center items-center hover:bg-gray-200 dark:hover:bg-zinc-600;
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 50%;
|
|
||||||
font-size: 14px;
|
|
||||||
user-select: none;
|
|
||||||
cursor: pointer;
|
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
@apply bg-gray-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.current {
|
&.current {
|
||||||
@apply text-blue-600 bg-blue-100 text-base font-medium;
|
@apply text-blue-600 !bg-blue-100 text-base font-medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.null {
|
&.null {
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
@apply flex flex-col justify-start items-start relative h-8;
|
@apply flex flex-col justify-start items-start relative h-8;
|
||||||
|
|
||||||
> .current-value-container {
|
> .current-value-container {
|
||||||
@apply flex flex-row justify-between items-center w-full h-full rounded px-2 pr-1 bg-white border cursor-pointer select-none;
|
@apply flex flex-row justify-between items-center w-full h-full rounded px-2 pr-1 bg-white dark:bg-zinc-700 dark:border-zinc-600 border cursor-pointer select-none;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&.active {
|
&.active {
|
||||||
@apply bg-gray-100;
|
@apply bg-gray-100 dark:bg-zinc-700;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .value-text {
|
> .value-text {
|
||||||
@apply text-sm mr-0 truncate;
|
@apply text-sm mr-0 truncate dark:text-gray-300;
|
||||||
width: calc(100% - 20px);
|
width: calc(100% - 20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,19 +18,19 @@
|
|||||||
@apply flex flex-row justify-center items-center w-4 shrink-0;
|
@apply flex flex-row justify-center items-center w-4 shrink-0;
|
||||||
|
|
||||||
> .icon-img {
|
> .icon-img {
|
||||||
@apply w-4 h-auto opacity-40;
|
@apply w-4 h-auto opacity-40 dark:text-gray-300;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .items-wrapper {
|
> .items-wrapper {
|
||||||
@apply flex flex-col justify-start items-start absolute top-full left-0 w-auto p-1 mt-1 -ml-2 bg-white rounded-md overflow-y-auto z-1 hide-scrollbar;
|
@apply flex flex-col justify-start items-start absolute top-full left-0 w-auto p-1 mt-1 -ml-2 bg-white dark:bg-zinc-700 dark:border-zinc-600 rounded-md overflow-y-auto z-1 hide-scrollbar;
|
||||||
min-width: calc(100% + 16px);
|
min-width: calc(100% + 16px);
|
||||||
max-height: 256px;
|
max-height: 256px;
|
||||||
box-shadow: 0 0 8px 0 rgb(0 0 0 / 20%);
|
box-shadow: 0 0 8px 0 rgb(0 0 0 / 20%);
|
||||||
|
|
||||||
> .item-container {
|
> .item-container {
|
||||||
@apply flex flex-col justify-start items-start w-full px-3 text-sm select-none leading-8 cursor-pointer rounded whitespace-nowrap hover:bg-gray-100;
|
@apply flex flex-col justify-start items-start w-full px-3 text-sm select-none leading-8 cursor-pointer rounded whitespace-nowrap dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-600;
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
@apply text-green-600;
|
@apply text-green-600;
|
||||||
|
|||||||
@@ -8,22 +8,22 @@
|
|||||||
@apply flex flex-col justify-start items-start;
|
@apply flex flex-col justify-start items-start;
|
||||||
|
|
||||||
> .form-item-container {
|
> .form-item-container {
|
||||||
@apply w-full mt-2 py-1 flex flex-row justify-start items-start;
|
@apply w-full mt-2 py-1 flex sm:flex-row flex-col justify-start items-start;
|
||||||
|
|
||||||
> .normal-text {
|
> .normal-text {
|
||||||
@apply block flex-shrink-0 w-12 mr-3 text-right text-sm leading-8;
|
@apply block flex-shrink-0 w-12 mr-3 sm:text-right text-left text-sm leading-8;
|
||||||
color: gray;
|
color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .title-input {
|
> .title-input {
|
||||||
@apply w-full py-1 px-2 h-9 text-sm rounded border shadow-inner;
|
@apply w-full py-1 px-2 h-9 text-sm rounded border dark:border-zinc-700 dark:bg-zinc-800 shadow-inner;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .filters-wrapper {
|
> .filters-wrapper {
|
||||||
@apply w-full flex flex-col justify-start items-start;
|
@apply w-full flex flex-col justify-start items-start;
|
||||||
|
|
||||||
> .create-filter-btn {
|
> .create-filter-btn {
|
||||||
@apply text-sm py-1 px-2 rounded shadow flex flex-row justify-start items-center border cursor-pointer text-blue-500 hover:opacity-80;
|
@apply text-sm py-1 px-2 rounded shadow flex flex-row sm:justify-start justify-center items-center border dark:border-zinc-700 cursor-pointer text-blue-500 hover:opacity-80 sm:min-w-0 min-w-full sm:mb-0 mb-1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,14 +56,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.memo-filter-input-wrapper {
|
.memo-filter-input-wrapper {
|
||||||
@apply w-full mb-3 shrink-0 flex flex-row justify-start items-center;
|
@apply w-full mb-3 shrink-0 flex flex-row sm:justify-start justify-center items-center sm:flex-nowrap flex-wrap sm:gap-0 gap-3;
|
||||||
|
|
||||||
> .selector-wrapper {
|
> .selector-wrapper {
|
||||||
@apply mr-1 h-9 grow-0 shrink-0;
|
@apply mr-1 h-9 grow-0 shrink-0 sm:min-w-0 min-w-full;
|
||||||
|
|
||||||
&.relation-selector {
|
&.relation-selector {
|
||||||
@apply w-16;
|
@apply w-16;
|
||||||
margin-left: -68px;
|
@media only screen and (min-width: 640px) {
|
||||||
|
margin-left: -68px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.type-selector {
|
&.type-selector {
|
||||||
@@ -80,13 +82,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> input.value-inputer {
|
> input.value-inputer {
|
||||||
max-width: calc(100% - 152px);
|
@media only screen and (min-width: 640px) {
|
||||||
@apply h-9 px-2 shrink-0 grow mr-1 text-sm rounded border bg-transparent hover:bg-gray-50;
|
max-width: calc(100% - 152px);
|
||||||
|
}
|
||||||
|
@apply h-9 px-2 shrink-0 grow mr-1 text-sm rounded border bg-transparent hover:bg-gray-50 sm:min-w-0 min-w-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
> input.datetime-selector {
|
> input.datetime-selector {
|
||||||
max-width: calc(100% - 152px);
|
@media only screen and (min-width: 640px) {
|
||||||
@apply h-9 px-2 shrink-0 grow mr-1 text-sm rounded border bg-transparent hover:bg-gray-50;
|
max-width: calc(100% - 152px);
|
||||||
|
}
|
||||||
|
@apply h-9 px-2 shrink-0 grow mr-1 text-sm rounded border bg-transparent hover:bg-gray-50 sm:min-w-0 min-w-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .remove-btn {
|
> .remove-btn {
|
||||||
|
|||||||
@@ -8,18 +8,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .split-line {
|
> .split-line {
|
||||||
@apply h-full px-px bg-gray-50 absolute top-1 left-6 z-0 -ml-px;
|
@apply h-full px-px bg-gray-50 dark:bg-zinc-600 absolute top-1 left-6 z-0 -ml-px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .time-wrapper {
|
> .time-wrapper {
|
||||||
@apply mt-px mr-4 w-12 h-7 shrink-0 text-xs leading-6 text-center font-mono rounded-lg bg-gray-100 border-2 border-white text-gray-600 z-10;
|
@apply mt-px mr-4 w-12 h-7 shrink-0 text-xs leading-6 text-center font-mono rounded-lg bg-gray-100 dark:bg-zinc-600 border-2 border-white dark:border-zinc-800 text-gray-600 dark:text-gray-300 z-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .memo-container {
|
> .memo-container {
|
||||||
@apply w-full overflow-x-hidden flex flex-col justify-start items-start;
|
@apply w-full overflow-x-hidden flex flex-col justify-start items-start;
|
||||||
|
|
||||||
.memo-content-text {
|
.memo-content-text {
|
||||||
margin-top: 3px;
|
@apply mt-1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,20 @@
|
|||||||
@apply p-0 sm:py-16;
|
@apply p-0 sm:py-16;
|
||||||
|
|
||||||
> .dialog-container {
|
> .dialog-container {
|
||||||
@apply w-full sm:w-112 max-w-full grow sm:grow-0 bg-white p-0 rounded-none sm:rounded-lg;
|
@apply w-full sm:w-112 max-w-full grow sm:grow-0 p-0 pb-4 rounded-none sm:rounded-lg;
|
||||||
|
|
||||||
> .dialog-header-container {
|
> .dialog-header-container {
|
||||||
@apply relative flex flex-row justify-between items-center w-full p-6 pb-0 mb-0;
|
@apply relative flex flex-row justify-between items-center w-full p-6 pb-0 mb-0;
|
||||||
|
|
||||||
> .title-text {
|
> .title-text {
|
||||||
@apply px-2 py-1 -ml-2 cursor-pointer select-none rounded hover:bg-gray-100;
|
@apply px-2 py-1 -ml-2 cursor-pointer select-none rounded hover:bg-gray-100 dark:hover:bg-zinc-700;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .btns-container {
|
> .btns-container {
|
||||||
@apply flex flex-row justify-start items-center;
|
@apply flex flex-row justify-start items-center;
|
||||||
|
|
||||||
> .btn-text {
|
> .btn-text {
|
||||||
@apply w-6 h-6 mr-2 rounded cursor-pointer select-none text-gray-600 last:mr-0 hover:bg-gray-200 p-0.5;
|
@apply w-6 h-6 mr-2 rounded cursor-pointer select-none last:mr-0 hover:bg-gray-200 dark:hover:bg-zinc-700 p-0.5;
|
||||||
|
|
||||||
> .icon-img {
|
> .icon-img {
|
||||||
@apply w-full h-auto;
|
@apply w-full h-auto;
|
||||||
@@ -28,19 +28,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .date-picker {
|
> .date-picker {
|
||||||
@apply absolute top-12 left-4 mt-2 bg-white shadow z-20 mx-auto border border-gray-200 rounded-lg mb-6;
|
@apply absolute top-12 left-4 mt-2 bg-white dark:bg-zinc-700 shadow z-20 mx-auto border dark:border-zinc-800 rounded-lg mb-6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .dialog-content-container {
|
> .dialog-content-container {
|
||||||
@apply w-full h-auto flex flex-col justify-start items-start p-6 pb-0;
|
@apply w-full h-auto flex flex-col justify-start items-start p-6 pb-0 bg-white dark:bg-zinc-800;
|
||||||
|
|
||||||
> .date-card-container {
|
> .date-card-container {
|
||||||
@apply flex flex-col justify-center items-center m-auto pb-6 select-none;
|
@apply flex flex-col justify-center items-center m-auto pb-6 select-none;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
> .year-text {
|
> .year-text {
|
||||||
@apply m-auto font-bold text-gray-600 text-center leading-6 mb-2;
|
@apply m-auto font-bold text-gray-600 dark:text-gray-300 text-center leading-6 mb-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .date-container {
|
> .date-container {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
@apply flex flex-col justify-start items-start relative w-full h-auto bg-white;
|
@apply flex flex-col justify-start items-start relative w-full h-auto bg-white;
|
||||||
|
|
||||||
> .common-editor-inputer {
|
> .common-editor-inputer {
|
||||||
@apply w-full h-full ~"max-h-[300px]" my-1 text-base resize-none overflow-x-hidden overflow-y-auto bg-transparent outline-none whitespace-pre-wrap;
|
@apply w-full h-full ~"max-h-[300px]" my-1 text-base resize-none overflow-x-hidden overflow-y-auto bg-transparent outline-none whitespace-pre-wrap break-all;
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
padding-left: 2px;
|
padding-left: 2px;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.page-wrapper.explore {
|
.page-wrapper.explore {
|
||||||
@apply relative top-0 w-full h-screen overflow-y-auto overflow-x-hidden;
|
@apply relative top-0 w-full h-screen overflow-y-auto overflow-x-hidden dark:bg-zinc-800;
|
||||||
background-color: #f6f5f4;
|
background-color: #f6f5f4;
|
||||||
|
|
||||||
> .page-container {
|
> .page-container {
|
||||||
@@ -16,17 +16,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .title-text {
|
> .title-text {
|
||||||
@apply text-xl sm:text-3xl font-mono text-gray-700;
|
@apply text-xl sm:text-4xl font-mono text-gray-700 dark:text-gray-200;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .action-button-container {
|
> .action-button-container {
|
||||||
> .btn {
|
> .link-btn {
|
||||||
@apply block text-gray-600 font-mono text-base py-1 border px-3 leading-8 rounded-xl hover:opacity-80 hover:underline;
|
@apply block text-gray-600 dark:text-gray-200 dark:border-gray-600 font-mono text-base py-1 border px-3 leading-8 rounded-xl hover:opacity-80;
|
||||||
|
|
||||||
> .icon {
|
|
||||||
@apply text-lg;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,7 +31,7 @@
|
|||||||
@apply relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4 sm:pr-6;
|
@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 {
|
> .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;
|
@apply flex flex-col justify-start items-start w-full p-4 mt-2 bg-white dark:bg-zinc-700 rounded-lg border border-white dark:border-zinc-800 hover:border-gray-200 dark:hover:border-zinc-600;
|
||||||
|
|
||||||
> .memo-header {
|
> .memo-header {
|
||||||
@apply mb-2 w-full flex flex-row justify-start items-center text-sm text-gray-400;
|
@apply mb-2 w-full flex flex-row justify-start items-center text-sm text-gray-400;
|
||||||
@@ -44,14 +40,6 @@
|
|||||||
@apply ml-2 hover:text-green-600 hover:underline;
|
@apply ml-2 hover:text-green-600 hover:underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .memo-content {
|
|
||||||
@apply cursor-default;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
@apply cursor-default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
.github-badge-container {
|
|
||||||
@apply h-7 flex flex-row justify-start items-center border rounded cursor-pointer hover:opacity-80;
|
|
||||||
|
|
||||||
> .github-icon {
|
|
||||||
@apply w-auto h-full px-2 border-r rounded-l flex flex-row justify-center items-center text-xs text-gray-800 bg-gray-100;
|
|
||||||
|
|
||||||
> .icon-img {
|
|
||||||
@apply mr-1 w-4 h-4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .count-text {
|
|
||||||
@apply w-auto h-full flex flex-row justify-center items-center px-3 text-xs font-bold text-gray-800;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
.page-wrapper.home {
|
.page-wrapper.home {
|
||||||
@apply relative top-0 w-full h-screen overflow-y-auto overflow-x-hidden;
|
@apply relative top-0 w-full h-screen overflow-y-auto overflow-x-hidden dark:bg-zinc-800;
|
||||||
background-color: #f6f5f4;
|
background-color: #f6f5f4;
|
||||||
|
|
||||||
> .banner-wrapper {
|
> .banner-wrapper {
|
||||||
@@ -17,15 +17,15 @@
|
|||||||
@apply relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4 sm:pr-6;
|
@apply relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4 sm:pr-6;
|
||||||
|
|
||||||
> .memos-editor-wrapper {
|
> .memos-editor-wrapper {
|
||||||
@apply sticky top-0 w-full h-full flex flex-col justify-start items-start z-10;
|
@apply sticky top-0 w-full h-full flex flex-col justify-start items-start z-10 dark:bg-zinc-800;
|
||||||
background-color: #f6f5f4;
|
background-color: #f6f5f4;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .addtion-btn-container {
|
> .addition-btn-container {
|
||||||
@apply fixed bottom-12 left-1/2 -translate-x-1/2;
|
@apply fixed bottom-12 left-1/2 -translate-x-1/2;
|
||||||
|
|
||||||
> .btn {
|
> .btn {
|
||||||
@apply bg-blue-600 text-white px-4 py-2 rounded-3xl shadow-2xl hover:opacity-80;
|
@apply bg-blue-600 dark:bg-blue-800 text-white dark:text-gray-200 px-4 py-2 rounded-3xl shadow-2xl hover:opacity-80;
|
||||||
|
|
||||||
> .icon {
|
> .icon {
|
||||||
@apply text-lg mr-1;
|
@apply text-lg mr-1;
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
.memo-content-wrapper {
|
.memo-content-wrapper {
|
||||||
@apply w-full flex flex-col justify-start items-start;
|
@apply w-full flex flex-col justify-start items-start text-gray-800 dark:text-gray-200;
|
||||||
|
|
||||||
> .memo-content-text {
|
> .memo-content-text {
|
||||||
@apply w-full break-words text-base leading-7;
|
@apply w-full max-w-full word-break text-base leading-6;
|
||||||
|
|
||||||
> p {
|
> p {
|
||||||
@apply w-full h-auto mb-1 last:mb-0 text-base leading-6 whitespace-pre-wrap break-words;
|
@apply w-full h-auto mb-1 last:mb-0 text-base;
|
||||||
min-height: 24px;
|
min-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .li-container {
|
||||||
|
@apply w-full flex flex-row flex-nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.img {
|
.img {
|
||||||
@apply block max-w-full rounded cursor-pointer hover:shadow;
|
@apply block max-w-full rounded cursor-pointer hover:shadow;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-span {
|
.tag-span {
|
||||||
@apply inline-block w-auto font-mono text-blue-600 cursor-pointer;
|
@apply inline-block w-auto font-mono text-blue-600 dark:text-blue-400 cursor-pointer;
|
||||||
}
|
|
||||||
|
|
||||||
.memo-link-text {
|
|
||||||
@apply inline-block text-blue-600 cursor-pointer font-bold border-none no-underline hover:opacity-80;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
@apply text-blue-600 cursor-pointer underline break-all hover:opacity-80 decoration-1;
|
@apply text-blue-600 dark:text-blue-400 cursor-pointer underline break-all hover:opacity-80 decoration-1;
|
||||||
|
|
||||||
code {
|
code {
|
||||||
@apply underline decoration-1;
|
@apply underline decoration-1;
|
||||||
}
|
}
|
||||||
@@ -31,31 +32,23 @@
|
|||||||
.ol-block,
|
.ol-block,
|
||||||
.ul-block,
|
.ul-block,
|
||||||
.todo-block {
|
.todo-block {
|
||||||
@apply inline-block box-border text-right w-8 mr-px font-mono select-none whitespace-nowrap;
|
@apply shrink-0 inline-block box-border text-right w-8 mr-px font-mono text-sm leading-6 select-none whitespace-nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ol-block {
|
||||||
|
@apply opacity-80 mt-px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ul-block {
|
.ul-block {
|
||||||
@apply text-center;
|
@apply text-center mt-px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo-block {
|
.todo-block {
|
||||||
@apply w-4 h-4 leading-4 border rounded box-border text-lg cursor-pointer shadow-inner hover:opacity-80;
|
@apply w-4 h-4 leading-4 mx-2 mt-1 border rounded box-border text-lg cursor-pointer shadow-inner hover:opacity-80;
|
||||||
transform: translateY(2px);
|
|
||||||
margin-left: 6px;
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
list-style-type: none;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
@apply font-bold mr-1;
|
|
||||||
content: "•";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
@apply w-full my-1 p-3 rounded bg-gray-100 whitespace-pre-wrap;
|
@apply w-full my-1 p-3 rounded bg-gray-100 dark:bg-zinc-600 whitespace-pre-wrap;
|
||||||
|
|
||||||
code {
|
code {
|
||||||
@apply block;
|
@apply block;
|
||||||
@@ -63,7 +56,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
@apply bg-gray-100 px-1 rounded text-sm font-mono leading-6 inline-block;
|
@apply break-all bg-gray-100 dark:bg-zinc-600 px-1 rounded text-sm font-mono leading-6 inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
@@ -79,11 +72,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
@apply border-l-4 pl-2 text-gray-400;
|
@apply border-l-4 pl-2 text-gray-400 dark:text-gray-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
@apply my-1;
|
@apply my-1 dark:border-zinc-600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +84,7 @@
|
|||||||
@apply w-full relative flex flex-row justify-start items-center;
|
@apply w-full relative flex flex-row justify-start items-center;
|
||||||
|
|
||||||
> .btn {
|
> .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;
|
@apply flex flex-row justify-start items-center pl-2 pr-1 py-1 my-1 text-xs rounded-lg border bg-gray-100 dark:bg-zinc-600 border-gray-200 dark:border-zinc-600 opacity-80 shadow hover:opacity-60 cursor-pointer;
|
||||||
|
|
||||||
&.expand-btn {
|
&.expand-btn {
|
||||||
@apply mt-2;
|
@apply mt-2;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
.page-wrapper.memo-detail {
|
.page-wrapper.memo-detail {
|
||||||
@apply relative top-0 w-full h-screen overflow-y-auto overflow-x-hidden;
|
@apply relative top-0 w-full h-screen overflow-y-auto overflow-x-hidden dark:bg-zinc-800;
|
||||||
background-color: #f6f5f4;
|
background-color: #f6f5f4;
|
||||||
|
|
||||||
> .page-container {
|
> .page-container {
|
||||||
@apply relative w-full min-h-screen mx-auto flex flex-col justify-start items-center pb-8;
|
@apply relative w-full min-h-screen mx-auto flex flex-col justify-start items-center pb-8;
|
||||||
|
|
||||||
> .page-header {
|
> .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 pt-6 mb-2;
|
@apply sticky top-0 z-10 max-w-2xl w-full min-h-full flex flex-row justify-between items-center px-4 pt-6 mb-2 dark:bg-zinc-800;
|
||||||
background-color: #f6f5f4;
|
background-color: #f6f5f4;
|
||||||
|
|
||||||
> .title-container {
|
> .title-container {
|
||||||
@@ -16,6 +16,10 @@
|
|||||||
@apply h-12 sm:h-14 w-auto mr-1;
|
@apply h-12 sm:h-14 w-auto mr-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .logo-text {
|
||||||
|
@apply text-4xl tracking-wide text-black dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
> .title-text {
|
> .title-text {
|
||||||
@apply text-xl sm:text-3xl font-mono text-gray-700;
|
@apply text-xl sm:text-3xl font-mono text-gray-700;
|
||||||
}
|
}
|
||||||
@@ -23,7 +27,7 @@
|
|||||||
|
|
||||||
> .action-button-container {
|
> .action-button-container {
|
||||||
> .btn {
|
> .btn {
|
||||||
@apply block text-gray-600 font-mono text-base py-1 border px-3 leading-8 rounded-xl hover:opacity-80 hover:underline;
|
@apply block text-gray-600 dark:text-gray-300 font-mono text-base py-1 border px-3 leading-8 rounded-xl hover:opacity-80 hover:underline;
|
||||||
|
|
||||||
> .icon {
|
> .icon {
|
||||||
@apply text-lg;
|
@apply text-lg;
|
||||||
@@ -36,7 +40,7 @@
|
|||||||
@apply relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4;
|
@apply relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4;
|
||||||
|
|
||||||
> .memo-container {
|
> .memo-container {
|
||||||
@apply flex flex-col justify-start items-start w-full p-4 pt-3 mt-2 bg-white rounded-lg border border-white hover:border-gray-200;
|
@apply flex flex-col justify-start items-start w-full p-4 mt-2 bg-white dark:bg-zinc-700 rounded-lg border border-white dark:border-zinc-800 hover:border-gray-200 dark:hover:border-zinc-700;
|
||||||
|
|
||||||
> .memo-header {
|
> .memo-header {
|
||||||
@apply mb-2 w-full flex flex-row justify-between items-center;
|
@apply mb-2 w-full flex flex-row justify-between items-center;
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
.memo-editor-container {
|
.memo-editor-container {
|
||||||
@apply transition-all relative w-full flex flex-col justify-start items-start bg-white px-4 rounded-lg border-2 border-gray-200;
|
@apply relative w-full flex flex-col justify-start items-start bg-white dark:bg-zinc-700 px-4 rounded-lg border-2 border-gray-200 dark:border-zinc-600;
|
||||||
|
|
||||||
&.fullscreen {
|
&.fullscreen {
|
||||||
@apply fixed w-full h-full top-0 left-0 z-1000 border-none rounded-none sm:p-8;
|
@apply transition-all fixed w-full h-full top-0 left-0 z-1000 border-none rounded-none sm:p-8 dark:bg-zinc-800;
|
||||||
background-color: #f6f5f4;
|
|
||||||
|
|
||||||
> .memo-editor {
|
> .memo-editor {
|
||||||
@apply p-4 mb-4 rounded-lg border shadow-lg flex flex-col flex-grow justify-start items-start relative w-full h-full bg-white;
|
@apply p-4 mb-4 rounded-lg border shadow-lg flex flex-col flex-grow justify-start items-start relative w-full h-full bg-white dark:bg-zinc-700 dark:border-zinc-600;
|
||||||
|
|
||||||
> .common-editor-inputer {
|
> .common-editor-inputer {
|
||||||
@apply flex-grow w-full !h-full max-h-full;
|
@apply flex-grow w-full !h-full max-h-full;
|
||||||
@@ -18,9 +17,8 @@
|
|||||||
top: unset !important;
|
top: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-picker-react {
|
.items-wrapper {
|
||||||
@apply !bottom-8;
|
@apply mb-1 bottom-full top-auto;
|
||||||
top: unset !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +27,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .memo-editor {
|
> .memo-editor {
|
||||||
@apply mt-4 flex flex-col justify-start items-start relative w-full h-auto bg-white;
|
@apply mt-4 flex flex-col justify-start items-start relative w-full h-auto bg-inherit dark:text-gray-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .common-tools-wrapper {
|
> .common-tools-wrapper {
|
||||||
@@ -39,7 +37,7 @@
|
|||||||
@apply flex flex-row justify-start items-center;
|
@apply flex flex-row justify-start items-center;
|
||||||
|
|
||||||
> .action-btn {
|
> .action-btn {
|
||||||
@apply flex flex-row justify-center items-center p-1 w-auto h-auto mr-1 select-none rounded cursor-pointer opacity-60 hover:opacity-90 hover:bg-gray-300 hover:shadow;
|
@apply flex flex-row justify-center items-center p-1 w-auto h-auto mr-1 select-none rounded cursor-pointer dark:text-gray-200 opacity-60 hover:opacity-90 hover:bg-gray-300 dark:hover:bg-zinc-800 hover:shadow;
|
||||||
|
|
||||||
&.tag-action {
|
&.tag-action {
|
||||||
@apply relative;
|
@apply relative;
|
||||||
@@ -63,20 +61,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.resource-btn {
|
||||||
|
@apply relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
> .resource-action-list {
|
||||||
|
@apply flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .resource-action-list {
|
||||||
|
@apply hidden flex-col justify-start items-start absolute top-6 left-0 mt-1 p-1 z-1 rounded w-auto overflow-auto font-mono shadow bg-zinc-200 dark:bg-zinc-600;
|
||||||
|
|
||||||
|
> .resource-action-item {
|
||||||
|
@apply w-full flex text-black dark:text-gray-300 cursor-pointer rounded text-sm leading-6 px-2 truncate hover:bg-zinc-300 dark:hover:bg-zinc-700 shrink-0;
|
||||||
|
|
||||||
|
> .icon-img {
|
||||||
|
@apply w-4 mr-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> .icon-img {
|
> .icon-img {
|
||||||
@apply w-5 h-5 mx-auto flex flex-row justify-center items-center;
|
@apply w-5 h-5 mx-auto flex flex-row justify-center items-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .tip-text {
|
> .tip-text {
|
||||||
@apply hidden ml-1 text-xs leading-5 text-gray-700 border border-gray-300 rounded-xl px-2;
|
@apply hidden ml-1 text-xs leading-5 text-gray-600 dark:text-gray-300 border border-gray-300 dark:border-zinc-500 rounded-xl px-2;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-picker-react {
|
|
||||||
@apply absolute shadow left-6 top-8;
|
|
||||||
|
|
||||||
li.emoji::before {
|
|
||||||
@apply hidden;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,7 +98,7 @@
|
|||||||
@apply w-full flex flex-row justify-start flex-wrap;
|
@apply w-full flex flex-row justify-start flex-wrap;
|
||||||
|
|
||||||
> .resource-container {
|
> .resource-container {
|
||||||
@apply mt-1 mr-1 flex flex-row justify-start items-center flex-nowrap bg-gray-100 px-2 py-1 rounded cursor-pointer hover:bg-gray-200;
|
@apply max-w-full mt-1 mr-1 flex flex-row justify-start items-center flex-nowrap bg-gray-100 px-2 py-1 rounded cursor-pointer hover:bg-gray-200;
|
||||||
|
|
||||||
> .icon-img {
|
> .icon-img {
|
||||||
@apply w-4 h-auto mr-1 text-gray-500;
|
@apply w-4 h-auto mr-1 text-gray-500;
|
||||||
@@ -103,7 +115,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .editor-footer-container {
|
> .editor-footer-container {
|
||||||
@apply w-full flex flex-row justify-between items-center border-t border-t-gray-100 py-3 mt-2;
|
@apply w-full flex flex-row justify-between items-center border-t border-t-gray-100 dark:border-t-zinc-500 py-3 mt-2;
|
||||||
|
|
||||||
> .visibility-selector {
|
> .visibility-selector {
|
||||||
@apply h-8;
|
@apply h-8;
|
||||||
@@ -121,7 +133,7 @@
|
|||||||
@apply grow-0 shrink-0 flex flex-row justify-end items-center;
|
@apply grow-0 shrink-0 flex flex-row justify-end items-center;
|
||||||
|
|
||||||
> .cancel-btn {
|
> .cancel-btn {
|
||||||
@apply mr-4 text-sm text-gray-600 hover:opacity-80;
|
@apply mr-4 text-sm text-gray-500 hover:opacity-80 dark:text-gray-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .confirm-btn {
|
> .confirm-btn {
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
.filter-query-container {
|
.filter-query-container {
|
||||||
@apply flex flex-row justify-start items-start w-full flex-wrap p-2 pb-1 text-sm font-mono leading-7;
|
@apply flex flex-row justify-start items-start w-full flex-wrap p-2 pb-1 text-sm font-mono leading-7 dark:text-gray-300;
|
||||||
|
|
||||||
> .tip-text {
|
> .tip-text {
|
||||||
@apply mr-2;
|
@apply mr-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .filter-item-container {
|
> .filter-item-container {
|
||||||
@apply flex flex-row justify-start items-center px-2 mr-2 cursor-pointer bg-gray-200 rounded whitespace-nowrap truncate hover:line-through;
|
@apply flex flex-row justify-start items-center px-2 mr-2 cursor-pointer dark:text-gray-300 bg-gray-200 dark:bg-zinc-700 rounded whitespace-nowrap truncate hover:line-through;
|
||||||
max-width: 256px;
|
max-width: 256px;
|
||||||
|
|
||||||
> .icon-text {
|
> .icon-text {
|
||||||
@apply w-4 h-auto mr-1 text-gray-500;
|
@apply w-4 h-auto mr-1 text-gray-500 dark:text-gray-400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
.memo-wrapper {
|
.memo-wrapper {
|
||||||
@apply relative flex flex-col justify-start items-start w-full p-4 pt-3 mt-2 bg-white rounded-lg border border-white hover:border-gray-200;
|
@apply relative flex flex-col justify-start items-start w-full p-4 pt-3 mt-2 bg-white dark:bg-zinc-700 rounded-lg border border-white dark:border-zinc-600 hover:border-gray-200 dark:hover:border-zinc-600;
|
||||||
|
|
||||||
&.archived-memo {
|
|
||||||
@apply border-gray-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.pinned {
|
&.pinned {
|
||||||
@apply border-gray-200 border-2;
|
@apply border-gray-200 border-2 dark:border-zinc-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.archived {
|
||||||
|
@apply border-gray-200 dark:border-zinc-600;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .corner-container {
|
> .corner-container {
|
||||||
@@ -54,14 +54,14 @@
|
|||||||
@apply hidden flex-col justify-start items-center absolute top-2 -right-4 flex-nowrap hover:flex p-3;
|
@apply hidden flex-col justify-start items-center absolute top-2 -right-4 flex-nowrap hover:flex p-3;
|
||||||
|
|
||||||
> .more-action-btns-container {
|
> .more-action-btns-container {
|
||||||
@apply w-28 h-auto p-1 z-1 whitespace-nowrap rounded-lg bg-white;
|
@apply w-28 h-auto p-1 z-1 whitespace-nowrap rounded-lg bg-white dark:bg-zinc-700;
|
||||||
box-shadow: 0 0 8px 0 rgb(0 0 0 / 20%);
|
box-shadow: 0 0 8px 0 rgb(0 0 0 / 20%);
|
||||||
|
|
||||||
> .btns-container {
|
> .btns-container {
|
||||||
@apply w-full flex flex-row justify-around items-center border-b border-gray-100 p-1 mb-1;
|
@apply w-full flex flex-row justify-around items-center border-b border-gray-100 dark:border-zinc-600 p-1 mb-1;
|
||||||
|
|
||||||
> .btn {
|
> .btn {
|
||||||
@apply relative w-6 h-6 p-1 text-gray-600 cursor-pointer select-none;
|
@apply relative w-6 h-6 p-1 text-gray-600 dark:text-gray-300 cursor-pointer select-none;
|
||||||
|
|
||||||
&:hover > .tip-text {
|
&:hover > .tip-text {
|
||||||
@apply block;
|
@apply block;
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .btn {
|
> .btn {
|
||||||
@apply w-full text-sm leading-6 py-1 px-3 rounded justify-start cursor-pointer;
|
@apply w-full text-sm leading-6 py-1 px-3 rounded justify-start cursor-pointer dark:text-gray-300;
|
||||||
|
|
||||||
&.archive-btn {
|
&.archive-btn {
|
||||||
@apply text-orange-600;
|
@apply text-orange-600;
|
||||||
@@ -88,13 +88,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
@apply flex flex-row justify-center items-center px-2 leading-6 text-sm rounded hover:bg-gray-200;
|
@apply flex flex-row justify-center items-center px-2 leading-6 text-sm rounded hover:bg-gray-200 dark:hover:bg-zinc-600;
|
||||||
|
|
||||||
&.more-action-btn {
|
&.more-action-btn {
|
||||||
@apply w-8 -mr-2 opacity-60 cursor-default hover:bg-transparent;
|
@apply w-8 -mr-2 opacity-60 cursor-default hover:bg-transparent;
|
||||||
|
|
||||||
> .icon-img {
|
> .icon-img {
|
||||||
@apply w-4 h-auto;
|
@apply w-4 h-auto dark:text-gray-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -118,30 +118,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .expand-btn-container {
|
|
||||||
@apply w-full relative flex flex-row justify-start items-center;
|
|
||||||
|
|
||||||
> .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;
|
|
||||||
|
|
||||||
&.expand-btn {
|
|
||||||
@apply mt-2;
|
|
||||||
|
|
||||||
> .icon-img {
|
|
||||||
@apply rotate-90;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.fold-btn {
|
|
||||||
> .icon-img {
|
|
||||||
@apply -rotate-90;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .icon-img {
|
|
||||||
@apply w-4 h-auto ml-1 transition-all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.section-header-container,
|
.section-header-container,
|
||||||
.memos-header-container {
|
.memos-header-container {
|
||||||
@apply sticky top-4 flex flex-row justify-between items-center w-full h-10 flex-nowrap mt-4 mb-2 shrink-0 z-1;
|
@apply sticky top-4 flex flex-row justify-between items-center w-full h-10 flex-nowrap mt-4 mb-2 shrink-0 z-10;
|
||||||
|
|
||||||
> .title-container {
|
> .title-container {
|
||||||
@apply flex flex-row justify-start items-center mr-2 shrink-0 overflow-hidden;
|
@apply flex flex-row justify-start items-center mr-2 shrink-0 overflow-hidden;
|
||||||
@@ -9,12 +9,12 @@
|
|||||||
@apply flex sm:hidden flex-row justify-center items-center w-6 h-6 mr-1 shrink-0 bg-transparent;
|
@apply flex sm:hidden flex-row justify-center items-center w-6 h-6 mr-1 shrink-0 bg-transparent;
|
||||||
|
|
||||||
> .icon-img {
|
> .icon-img {
|
||||||
@apply w-5 h-auto;
|
@apply w-5 h-auto dark:text-gray-200;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .title-text {
|
> .title-text {
|
||||||
@apply font-bold text-lg leading-10 mr-2 text-ellipsis shrink-0 cursor-pointer overflow-hidden text-gray-700;
|
@apply font-bold text-lg leading-10 mr-2 text-ellipsis shrink-0 cursor-pointer overflow-hidden text-gray-700 dark:text-gray-200;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
@apply fixed top-8 right-8 flex flex-col justify-start items-center;
|
@apply fixed top-8 right-8 flex flex-col justify-start items-center;
|
||||||
|
|
||||||
> .btn {
|
> .btn {
|
||||||
@apply mb-3 last:mb-0 w-8 h-8 p-1 cursor-pointer rounded opacity-90 bg-gray-300 z-10 shadow-md hover:opacity-70;
|
@apply mb-3 last:mb-0 w-8 h-8 p-1 cursor-pointer rounded opacity-90 bg-gray-300 dark:bg-zinc-600 z-10 shadow-md hover:opacity-70;
|
||||||
|
|
||||||
> .icon-img {
|
> .icon-img {
|
||||||
@apply w-6 h-auto;
|
@apply w-6 h-auto;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
@apply text-sm cursor-pointer px-3 py-1 rounded flex flex-row justify-center items-center border border-red-600 text-red-600 bg-red-100 hover:opacity-80;
|
@apply text-sm cursor-pointer px-3 py-1 rounded flex flex-row justify-center items-center border border-red-600 text-red-600 bg-red-100 hover:opacity-80;
|
||||||
|
|
||||||
> .icon-img {
|
> .icon-img {
|
||||||
@apply w-4 h-auto mr-1;
|
@apply w-4 h-auto mr-1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
@apply flex flex-col justify-start items-start w-full;
|
@apply flex flex-col justify-start items-start w-full;
|
||||||
|
|
||||||
> .fields-container {
|
> .fields-container {
|
||||||
@apply px-2 py-2 w-full grid grid-cols-7 border-b;
|
@apply px-2 py-2 w-full grid grid-cols-7 border-b dark:border-b-zinc-600;
|
||||||
|
|
||||||
> .field-text {
|
> .field-text {
|
||||||
@apply font-mono text-gray-400;
|
@apply font-mono text-gray-400;
|
||||||
|
|||||||
51
web/src/less/resources-selector-dialog.less
Normal file
51
web/src/less/resources-selector-dialog.less
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
.resources-selector-dialog {
|
||||||
|
@apply px-4;
|
||||||
|
|
||||||
|
> .dialog-container {
|
||||||
|
@apply w-112 max-w-full mb-8;
|
||||||
|
|
||||||
|
> .dialog-content-container {
|
||||||
|
@apply flex flex-col justify-start items-start w-full;
|
||||||
|
|
||||||
|
> .loading-text-container {
|
||||||
|
@apply flex flex-col justify-center items-center w-full h-32;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .resource-table-container {
|
||||||
|
@apply flex flex-col justify-start items-start w-full;
|
||||||
|
|
||||||
|
> .fields-container {
|
||||||
|
@apply px-2 py-2 w-full grid grid-cols-7 border-b dark:border-b-gray-500;
|
||||||
|
|
||||||
|
> .field-text {
|
||||||
|
@apply font-mono text-gray-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .tip-text {
|
||||||
|
@apply w-full text-center text-base my-6 mt-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .resource-container {
|
||||||
|
@apply px-2 py-2 w-full grid grid-cols-7 dark:bg-zinc-700;
|
||||||
|
|
||||||
|
> .buttons-container {
|
||||||
|
@apply w-full flex flex-row justify-end items-center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-text {
|
||||||
|
@apply w-full truncate text-base pr-2 last:pr-0;
|
||||||
|
|
||||||
|
&.id-text {
|
||||||
|
@apply col-span-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.name-text {
|
||||||
|
@apply col-span-4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user